Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ca7e7c830 | ||
|
|
9a0db56ac3 | ||
|
|
c1ae5875ba | ||
|
|
1d0ae427bf | ||
|
|
3dde2d19e2 | ||
|
|
d5ec6d7b85 | ||
|
|
13856b71a0 | ||
|
|
c6658771ca | ||
|
|
406f36584d | ||
|
|
898750d058 | ||
|
|
09e9e934d3 | ||
|
|
3d170267b7 | ||
|
|
3ca7e3cf88 | ||
|
|
1378d1445a | ||
|
|
16d304dbb4 | ||
|
|
aff5db7759 | ||
|
|
0697aa9755 | ||
|
|
995d3bd4b9 | ||
|
|
3bf5204e79 | ||
|
|
0b1ce6e94a | ||
|
|
a66ccd7385 | ||
|
|
7a3f125802 | ||
|
|
4a8dcce3d4 | ||
|
|
adcc3eb43f | ||
|
|
8bbd1a7cf4 | ||
|
|
467ff26278 | ||
|
|
d6a267afa4 | ||
|
|
743e934436 | ||
|
|
f89ac2231a | ||
|
|
1877a18ebb | ||
|
|
c99f85d9d5 | ||
|
|
228942c60a | ||
|
|
fecd567eab | ||
|
|
602840519b | ||
|
|
959adf1075 | ||
|
|
422f681fe6 | ||
|
|
474d298fd3 | ||
|
|
4052136217 | ||
|
|
3193e02867 | ||
|
|
c7122b3fff | ||
|
|
8775194d26 | ||
|
|
b3f27cc96e | ||
|
|
3313a1ed27 | ||
|
|
da66622994 | ||
|
|
446cdc32d7 | ||
|
|
b08d2b0dc0 | ||
|
|
e051c212d1 | ||
|
|
7645a600ef | ||
|
|
9e56b5cd15 | ||
|
|
dcfcd1d8b2 | ||
|
|
b2dc4b45c2 | ||
|
|
0314921728 | ||
|
|
7d339677e4 | ||
|
|
c9d259d7f1 | ||
|
|
eae2670855 | ||
|
|
e857f19b18 | ||
|
|
a11816884c | ||
|
|
f686cb0ceb | ||
|
|
5aac4b82fc | ||
|
|
99c3ef8a57 | ||
|
|
0a17aad9d6 | ||
|
|
965995ad20 | ||
|
|
a5067cb1dd | ||
|
|
1f9c0736aa | ||
|
|
a27671560d | ||
|
|
5c767deaf3 | ||
|
|
c157d4e0d1 | ||
|
|
655f42734a | ||
|
|
55b01ab101 | ||
|
|
c6ed42d3e6 | ||
|
|
0562051989 | ||
|
|
f68e056e47 | ||
|
|
457490276c | ||
|
|
49ea9e5da0 | ||
|
|
9f3fb7aff6 | ||
|
|
1033389c38 | ||
|
|
58e32c4459 | ||
|
|
936285847c | ||
|
|
fab32a6d51 | ||
|
|
5b345affec | ||
|
|
32c3b694b3 | ||
|
|
4dce13855a | ||
|
|
1ce9858893 | ||
|
|
71c9632d61 | ||
|
|
cce17edaa6 | ||
|
|
deeef8f67a | ||
|
|
25c76a748f | ||
|
|
2109df8145 | ||
|
|
ae01a49cc4 | ||
|
|
a4f5e013cb | ||
|
|
d4579852bb | ||
|
|
edd95ad626 | ||
|
|
dec307b531 | ||
|
|
00b7085370 | ||
|
|
17c1998119 | ||
|
|
1c05cd981f | ||
|
|
e9c3795ffc | ||
|
|
c3917082ee | ||
|
|
6265580d75 | ||
|
|
65fc3d0c4f | ||
|
|
1c864ba072 | ||
|
|
ef9159666c | ||
|
|
541f2e2619 | ||
|
|
c7f248a092 | ||
|
|
7937f8ddf5 | ||
|
|
74926704bd | ||
|
|
9196bda537 | ||
|
|
4c1e607b8e | ||
|
|
26224ae366 | ||
|
|
d3e9511aca | ||
|
|
b624cebc08 | ||
|
|
7e4d437a1d | ||
|
|
37d6857b82 | ||
|
|
f8d7c92d51 | ||
|
|
ea9999a1f9 | ||
|
|
d969fab284 | ||
|
|
97fe52c2a7 | ||
|
|
8a816f7f35 | ||
|
|
6409841cca | ||
|
|
6348166dac | ||
|
|
48417b9b7b | ||
|
|
738000ff39 | ||
|
|
9b5445b686 | ||
|
|
abf58b2c20 | ||
|
|
fd97f6c321 | ||
|
|
abd37a093a | ||
|
|
0f0cf55509 | ||
|
|
95a1496389 | ||
|
|
47cc9ebe2b | ||
|
|
3ebbdb60ef | ||
|
|
e8306ea59a | ||
|
|
6f7c922ac0 | ||
|
|
74abecaae7 | ||
|
|
ad5b74d8c7 | ||
|
|
5f84e8cfeb | ||
|
|
519ed0ad5f | ||
|
|
61096142d5 | ||
|
|
7c7c36f8aa | ||
|
|
dbb614dce1 | ||
|
|
5a4825d419 | ||
|
|
d87cf91cdf | ||
|
|
0782f40fe0 | ||
|
|
b7a5b484b5 | ||
|
|
1e472ea463 | ||
|
|
816a8539ea | ||
|
|
c81e510bfa | ||
|
|
85b68ad135 | ||
|
|
7d5d74d0c6 | ||
|
|
e0ecd5e695 | ||
|
|
5e0687c138 | ||
|
|
b827167a3d | ||
|
|
20711c9d10 | ||
|
|
15c1042991 | ||
|
|
1275e38995 | ||
|
|
312a2e0cde | ||
|
|
35ce95857a | ||
|
|
6b78f56298 | ||
|
|
b43c8ceae5 | ||
|
|
704d4e9cc0 | ||
|
|
96471e3a92 | ||
|
|
af3719403d | ||
|
|
e0764d6f0c | ||
|
|
908f47f75e | ||
|
|
a3af719087 | ||
|
|
7f13d6646b | ||
|
|
e8b4c38451 | ||
|
|
38ba6802a0 | ||
|
|
381eead7b6 | ||
|
|
df953e5ea7 | ||
|
|
2fd2954a06 | ||
|
|
a8bb5c1570 | ||
|
|
a33377d1f2 | ||
|
|
e515542add | ||
|
|
48b6bd4ea1 | ||
|
|
9cf23f69a6 | ||
|
|
4104ca082d | ||
|
|
32f8ffad46 | ||
|
|
a5e04a7b81 | ||
|
|
57018bef47 | ||
|
|
0b65431044 | ||
|
|
991729b684 | ||
|
|
1605eb1775 | ||
|
|
94ccee0b76 | ||
|
|
8b94fe5b0b | ||
|
|
abec1abcff | ||
|
|
127938fa38 | ||
|
|
1187ae4f04 | ||
|
|
8efdc536ee | ||
|
|
78f080b6cc | ||
|
|
cc8bcfd924 | ||
|
|
806ca3f64a | ||
|
|
4a16fa13ee | ||
|
|
b88d7c4fd2 | ||
|
|
c0838468b1 | ||
|
|
90cfe3d0b7 | ||
|
|
baf1d22bc4 | ||
|
|
7ca5744559 | ||
|
|
57e728991b | ||
|
|
e08ba4d421 | ||
|
|
ba91c60e95 | ||
|
|
389b93178e | ||
|
|
37aaf153f8 | ||
|
|
2249f4ee0e | ||
|
|
eadfff349a | ||
|
|
cb4cd696a0 | ||
|
|
c85c074eee | ||
|
|
b26c478d4a | ||
|
|
2d83de7c31 | ||
|
|
20d93dd085 | ||
|
|
24fc436870 | ||
|
|
3a6c23aa9d | ||
|
|
0ee8b2dd82 | ||
|
|
7cce90bed8 | ||
|
|
c806a5d412 | ||
|
|
14e8b9212f | ||
|
|
67e96fb84f | ||
|
|
c39c19f215 | ||
|
|
88d7493870 | ||
|
|
404bc88ec8 | ||
|
|
e2355ca2ff | ||
|
|
aab271ac75 | ||
|
|
0da0a1f386 | ||
|
|
59316a866d | ||
|
|
d3087bd763 | ||
|
|
df7b63c491 | ||
|
|
dff893ba4b | ||
|
|
c539702a1b | ||
|
|
6fad149410 | ||
|
|
a072fb7bea | ||
|
|
08a3850dcf | ||
|
|
030fba4630 | ||
|
|
559aefd6b6 | ||
|
|
2ee5d9ceed | ||
|
|
20ed42ab8a | ||
|
|
815b90845a | ||
|
|
e9846ea644 | ||
|
|
02065094ca | ||
|
|
9f6124d617 | ||
|
|
76229a894c | ||
|
|
03de1598c8 | ||
|
|
3a13ff0e10 | ||
|
|
63345f52ce | ||
|
|
0bff282d3d | ||
|
|
a81462425d | ||
|
|
e9ae43b1f1 | ||
|
|
dc31f0d5c9 | ||
|
|
76f11b6a6f | ||
|
|
d6ea2ef1f9 | ||
|
|
14db21211f | ||
|
|
f354f65adb | ||
|
|
83e8ea3735 | ||
|
|
0e9e9811eb | ||
|
|
420d77de9f | ||
|
|
9361719a67 | ||
|
|
b09a5868d4 | ||
|
|
56c48e5595 | ||
|
|
4d6cb547ea | ||
|
|
95ecd29018 | ||
|
|
5d26a106e9 | ||
|
|
95707560ef | ||
|
|
c54cb88db8 | ||
|
|
95afaa5112 | ||
|
|
fe608e32f1 | ||
|
|
82fc1c067d | ||
|
|
94890e2924 | ||
|
|
9a76b1ff27 | ||
|
|
8db8693132 | ||
|
|
59cc5b9114 | ||
|
|
c1acd0b0b5 | ||
|
|
fb6538f042 | ||
|
|
20fe5430dd | ||
|
|
7de07319d9 | ||
|
|
8e3c19651e | ||
|
|
2740bdeb77 | ||
|
|
59ab49e0c5 | ||
|
|
bc4aa43834 | ||
|
|
c7f3f2a229 | ||
|
|
ffeabbf1f1 | ||
|
|
c9ddb7aab4 | ||
|
|
997552fb7e | ||
|
|
7d9595945d | ||
|
|
70157fa8de | ||
|
|
22e1e6344f | ||
|
|
3952b6e3a8 | ||
|
|
dd2a09399e | ||
|
|
7f7068a556 | ||
|
|
a79a2014b0 | ||
|
|
4c41f25314 | ||
|
|
c26eb2bc69 | ||
|
|
e93e5effa6 | ||
|
|
7acc39154b | ||
|
|
19cae84a01 | ||
|
|
0bcda9eb46 | ||
|
|
7556ef21c4 | ||
|
|
9290c26c0b | ||
|
|
5388020b35 | ||
|
|
017d688e32 | ||
|
|
4102029d6d | ||
|
|
d286f93a7e | ||
|
|
207b4d0033 | ||
|
|
351a59149f | ||
|
|
7880c2f3d9 | ||
|
|
644866a24b | ||
|
|
43fee4878f | ||
|
|
1d8ead2cbb | ||
|
|
4cbf5ce979 | ||
|
|
1eb53b247c | ||
|
|
f53aed9a08 | ||
|
|
5db3c123a0 | ||
|
|
e1e9215f2c | ||
|
|
23f58e63aa | ||
|
|
2cad6f33f5 | ||
|
|
308086b876 | ||
|
|
9bdf08af1e | ||
|
|
ddcead7c85 | ||
|
|
89b5b64943 | ||
|
|
931808d808 | ||
|
|
b9a6401ef3 | ||
|
|
59a9b3a926 | ||
|
|
96270b1706 | ||
|
|
af784dd0d0 | ||
|
|
1a1f2133f7 | ||
|
|
3f1be961a6 | ||
|
|
8f2afdfcff | ||
|
|
532ba6ec2f | ||
|
|
8678c0dcf2 | ||
|
|
2116cfd789 | ||
|
|
45d9033252 | ||
|
|
96df35f728 | ||
|
|
16b0335fcf | ||
|
|
b69dadc80c | ||
|
|
7619e48145 | ||
|
|
fbc667798d | ||
|
|
4e22585faf | ||
|
|
5d3660ffb8 | ||
|
|
4149876e04 | ||
|
|
e6e4e36537 | ||
|
|
5b7149b967 | ||
|
|
f558be7193 | ||
|
|
a9159e1a72 | ||
|
|
b0e3ae7684 | ||
|
|
242df8a81a | ||
|
|
333fd0ef80 | ||
|
|
5defff2bb0 | ||
|
|
bdcbe6c948 | ||
|
|
cc192c840c | ||
|
|
e5ece58d17 | ||
|
|
2391b26bd6 | ||
|
|
c7fad05bae | ||
|
|
1d9d31f906 | ||
|
|
d584c101d4 | ||
|
|
ab41feff42 | ||
|
|
cb7975515a | ||
|
|
91f684ae2e | ||
|
|
55c55e9bda | ||
|
|
453ba6eb75 | ||
|
|
5aeaba4636 | ||
|
|
df9499b723 | ||
|
|
f84be1e109 | ||
|
|
31872f4f77 | ||
|
|
54188bf8f2 | ||
|
|
9fb2bc4c7e | ||
|
|
5d8a56cf66 | ||
|
|
833ac2e32e | ||
|
|
50173a3987 | ||
|
|
9d5202c19f | ||
|
|
e1bbd8b326 | ||
|
|
06f81b9b09 | ||
|
|
150baa0e30 | ||
|
|
413d962db0 | ||
|
|
74118049ce | ||
|
|
05086fa76d | ||
|
|
c1ffd26c80 | ||
|
|
a8b0f81b68 | ||
|
|
d83b508ce4 | ||
|
|
fe622579cf | ||
|
|
edcdeaf018 | ||
|
|
9d4eb9662e | ||
|
|
42e88fe6f5 | ||
|
|
61165dd71d | ||
|
|
83120afb63 | ||
|
|
4fca632efb | ||
|
|
03b20a70bb | ||
|
|
20ee9c8ce6 | ||
|
|
9c0e1b1dc8 | ||
|
|
f29e350b1a | ||
|
|
bbe0f5d750 | ||
|
|
371295d9fe | ||
|
|
cc8a4ddf19 | ||
|
|
f3ff037f1c | ||
|
|
3b36bbc88e | ||
|
|
80b53119ed | ||
|
|
4d88c2b1bb | ||
|
|
29582f6c50 | ||
|
|
623c54cf58 | ||
|
|
6eee8b89e1 | ||
|
|
c74ef3eca8 | ||
|
|
372b6376fc | ||
|
|
0c3bfc933c | ||
|
|
2414db1c61 | ||
|
|
008fe17035 | ||
|
|
2be95fdb51 | ||
|
|
e5842d5a49 | ||
|
|
9a59ca95a1 | ||
|
|
421f6f8b6d | ||
|
|
a5ee6b05e8 | ||
|
|
c9465d89f5 | ||
|
|
097a8eaf26 | ||
|
|
648b671210 | ||
|
|
75aeed9747 | ||
|
|
c87f7f39c7 | ||
|
|
32b10038d9 | ||
|
|
204cbcbed0 | ||
|
|
da5a04b3ba | ||
|
|
126a6dbbed | ||
|
|
ab43d4a6cf | ||
|
|
5f6e11f709 | ||
|
|
cf61abcf03 | ||
|
|
9549819571 | ||
|
|
95b85c13eb | ||
|
|
d46efe8410 | ||
|
|
501ecac4d5 | ||
|
|
0dce5bc9a6 | ||
|
|
9092c8b7a6 | ||
|
|
a5858495ef | ||
|
|
88984378e8 | ||
|
|
470c3d6c24 | ||
|
|
6133e7e730 | ||
|
|
c5cbafea60 | ||
|
|
167e25e859 | ||
|
|
f47bd8a407 | ||
|
|
464be411d1 | ||
|
|
d3b6eb36d9 | ||
|
|
71458f5167 | ||
|
|
597730389f | ||
|
|
56b227f467 | ||
|
|
213b03ea56 | ||
|
|
959c4d0698 | ||
|
|
57b3bd9ff0 | ||
|
|
68f4038879 | ||
|
|
6010008409 | ||
|
|
4b2e857f08 | ||
|
|
0280a34d00 | ||
|
|
811707b611 | ||
|
|
9152550bda | ||
|
|
6cb4d24600 | ||
|
|
49287453eb | ||
|
|
e7c108d843 | ||
|
|
6c9bf6cb09 | ||
|
|
efc27c8e21 | ||
|
|
7c5ccc906e | ||
|
|
69a6ff6053 | ||
|
|
605d0c6a92 | ||
|
|
9d54ddf456 | ||
|
|
c6afd53e73 | ||
|
|
fd9bb88033 | ||
|
|
316607b333 | ||
|
|
af36a0ac2a | ||
|
|
7356335106 | ||
|
|
d8c96769ea | ||
|
|
d71815aa3d | ||
|
|
9a4286620f | ||
|
|
0eac7b5d12 | ||
|
|
af63c3aa3a | ||
|
|
72a8199453 | ||
|
|
090b83bf9c | ||
|
|
6f70da51f9 | ||
|
|
ed0a4c8cf4 | ||
|
|
5fa066de6b | ||
|
|
1454ea8358 | ||
|
|
ab264955f5 | ||
|
|
389d430dbd | ||
|
|
8b98932ae8 | ||
|
|
62f39a3280 | ||
|
|
b116186f42 | ||
|
|
40eeee7137 | ||
|
|
54eab87356 | ||
|
|
d828f7bc7b | ||
|
|
ae3e135e06 | ||
|
|
5ba8bdad10 | ||
|
|
bffbfd7af9 | ||
|
|
a8c1e8d2cb | ||
|
|
230d6b20ab | ||
|
|
109a14dd74 | ||
|
|
1ac1ff3c95 | ||
|
|
7e522ff136 | ||
|
|
90c261ddd1 | ||
|
|
b9bdc29dc2 | ||
|
|
13968fe3aa | ||
|
|
825df1981d | ||
|
|
dd56f4f8a6 | ||
|
|
b9ef10a73d | ||
|
|
0f68a7c703 | ||
|
|
f5b24b62f1 | ||
|
|
f9eca35422 | ||
|
|
394a74e566 | ||
|
|
870b73c101 | ||
|
|
d89a32f620 | ||
|
|
29410b951d | ||
|
|
461cde3282 | ||
|
|
94b54f3dce | ||
|
|
6e20ce4db3 | ||
|
|
e76d520cac | ||
|
|
9a3b8545ca | ||
|
|
bb66be7c3a | ||
|
|
8514eb904d | ||
|
|
a6c8867ec4 | ||
|
|
5753c2993c | ||
|
|
ec6637728a | ||
|
|
3198daa60a | ||
|
|
1a6dffab74 | ||
|
|
d54d9ae5cb | ||
|
|
e543ee3f55 | ||
|
|
f7526d4cdc | ||
|
|
cf136fe7df | ||
|
|
5d9d9b9cc4 | ||
|
|
6a05df2c35 | ||
|
|
34cfdb8073 | ||
|
|
259ed570ab | ||
|
|
c86302df58 | ||
|
|
3d9c9689e3 | ||
|
|
d4398764a1 | ||
|
|
fb573d8205 | ||
|
|
da5097ab78 | ||
|
|
59b5aabfe5 | ||
|
|
8355f02de0 | ||
|
|
bd4b334185 | ||
|
|
bbe0a3f567 | ||
|
|
41620dcd70 | ||
|
|
e865b31b62 | ||
|
|
92ce7c83ca | ||
|
|
753ba956fe | ||
|
|
0130676b42 | ||
|
|
0ec00ede70 | ||
|
|
42dd877b97 | ||
|
|
00e8ae691c | ||
|
|
1cc429c7ca | ||
|
|
bf822d7cf7 | ||
|
|
5dc1d14f73 | ||
|
|
15a93a9dc0 | ||
|
|
0c0cd97a6e | ||
|
|
b132aba1e3 | ||
|
|
7d97e5d104 | ||
|
|
7c8d75f5a4 | ||
|
|
1b5522958a | ||
|
|
593e70fa43 | ||
|
|
6475e03c01 | ||
|
|
2d4ca3100e | ||
|
|
3f0e8138b1 | ||
|
|
9e6d6ff95a | ||
|
|
6ae6020b73 | ||
|
|
4b14673aaf | ||
|
|
bd7365e944 | ||
|
|
8c38e4c628 | ||
|
|
2b4e476153 | ||
|
|
0f0f5cbc28 | ||
|
|
bed74dc66c | ||
|
|
ba196dcdf4 | ||
|
|
9f8ca0255b | ||
|
|
b6a156b0d6 | ||
|
|
d032b3c677 | ||
|
|
4c2d1063ec | ||
|
|
2e7e2d261c | ||
|
|
9750c69742 | ||
|
|
d67dd700e9 | ||
|
|
7f599638d7 | ||
|
|
bf04854e68 | ||
|
|
2bcf9ee39e | ||
|
|
7a90035278 | ||
|
|
a7108f318a | ||
|
|
a3eaccb45b | ||
|
|
e0c97da9fd | ||
|
|
3843002113 | ||
|
|
1ec9e41459 | ||
|
|
a6231ac593 | ||
|
|
ea91ee91ec | ||
|
|
d4c0c30283 | ||
|
|
9e2c46a6f8 | ||
|
|
ce31bcc6e0 | ||
|
|
d11e22026c | ||
|
|
a96fe298b8 | ||
|
|
8e28c2487f | ||
|
|
fda6f75333 | ||
|
|
e414450f3c | ||
|
|
95e15d7df4 | ||
|
|
7e826a5952 | ||
|
|
43e2644f0e | ||
|
|
a0ef16bef4 | ||
|
|
f5d83095cc | ||
|
|
a94bb407e5 | ||
|
|
6611774aa9 | ||
|
|
e1eb40c129 | ||
|
|
2e532be34c | ||
|
|
b9d0cfc12a | ||
|
|
3d88bc35bb | ||
|
|
8c9c8fd28f | ||
|
|
5e685b7f26 | ||
|
|
c76b562705 | ||
|
|
4c9f8d3dca | ||
|
|
0f26ae5cfc | ||
|
|
035469240d | ||
|
|
7ee81f7425 | ||
|
|
024c90d688 | ||
|
|
1253272626 | ||
|
|
73b938cfd9 | ||
|
|
ee843543bf | ||
|
|
fa8a479aac | ||
|
|
bac14db432 | ||
|
|
f6242ea868 | ||
|
|
16db35655a | ||
|
|
53336aedc9 | ||
|
|
eaddb67932 | ||
|
|
c7b7dd9500 | ||
|
|
9901bf738d | ||
|
|
a3d8c6b3b3 | ||
|
|
948928f740 | ||
|
|
71e98c035e | ||
|
|
af107b15c0 | ||
|
|
8687bb9487 | ||
|
|
a70e53364d | ||
|
|
11a1c37765 | ||
|
|
68a8944810 | ||
|
|
b280b238df | ||
|
|
0f39aad429 | ||
|
|
ff78297ee5 | ||
|
|
f75bbdcecc | ||
|
|
694e31a7fe | ||
|
|
c113670cda | ||
|
|
1b3c399335 | ||
|
|
45cbe20449 | ||
|
|
4d606510f6 | ||
|
|
a6639552cf | ||
|
|
2a088ec555 | ||
|
|
1c46d63367 | ||
|
|
f2b7fae694 | ||
|
|
43986676d8 | ||
|
|
2649e79d47 | ||
|
|
f83d3cd769 | ||
|
|
f124910f94 | ||
|
|
c1712c3e1e | ||
|
|
d817c80629 | ||
|
|
8d830dbf96 | ||
|
|
0396e96334 | ||
|
|
7b23c7bc79 | ||
|
|
d3ca50259a | ||
|
|
499a65d5fb | ||
|
|
0b61f9856c | ||
|
|
c8409dd258 | ||
|
|
e5ea4cd79f | ||
|
|
4d54c7f6ba | ||
|
|
0f4a5c26df | ||
|
|
a8b09e2195 | ||
|
|
3ed6e38ad3 | ||
|
|
2028b0ef9c | ||
|
|
aa7f9b91c7 | ||
|
|
d6598310b2 | ||
|
|
ce6c06987c | ||
|
|
90955d7916 | ||
|
|
b3bbd21f1f | ||
|
|
735a407f0e | ||
|
|
8461b0418c | ||
|
|
e46ab8bf9e | ||
|
|
330548b24e | ||
|
|
cf5ccca42d | ||
|
|
a18bd0729b | ||
|
|
662b42c40f | ||
|
|
b69c400a08 | ||
|
|
f1eb2da57c | ||
|
|
99be810b5e | ||
|
|
03541a6f85 | ||
|
|
5fe1ef899d | ||
|
|
6a81f4d240 | ||
|
|
3c6aa489e8 | ||
|
|
ba7043fa92 | ||
|
|
bfb6c99484 | ||
|
|
a6b26a0cd9 | ||
|
|
0c7b0f285e | ||
|
|
5e1ae56740 | ||
|
|
b17bac050b | ||
|
|
b1952a7b37 | ||
|
|
cc1fc1e4be | ||
|
|
6c07924d9b | ||
|
|
835668d781 | ||
|
|
8f7c09d2cc | ||
|
|
837939e27c | ||
|
|
058a8e2335 | ||
|
|
a5af5265be | ||
|
|
8a724587ca | ||
|
|
bef9c0ad88 | ||
|
|
8d29775410 | ||
|
|
620c0e4c73 | ||
|
|
b5c92c1564 | ||
|
|
3c3c28aaa5 | ||
|
|
a80fc34ef1 | ||
|
|
08d7ff138b | ||
|
|
0c172e5d08 | ||
|
|
bce2d1ea5c | ||
|
|
cbc1e26b6f | ||
|
|
3b5ddcafa6 | ||
|
|
c6a5624708 | ||
|
|
23cc568bac | ||
|
|
7ff679c3c8 | ||
|
|
c873352ee9 | ||
|
|
a4164a86af | ||
|
|
c21671e86b | ||
|
|
d9bdc2b1f7 | ||
|
|
19b6633f4a | ||
|
|
2ad3e7a291 | ||
|
|
1fa63a528d | ||
|
|
b80c787b60 | ||
|
|
c8652012c5 | ||
|
|
e70b43be26 | ||
|
|
2d182807b2 | ||
|
|
675af3a5aa | ||
|
|
4ba4c3e749 | ||
|
|
11e2277c9e | ||
|
|
66642fe621 | ||
|
|
3189a22f26 | ||
|
|
366e359d83 | ||
|
|
f7d8a473aa | ||
|
|
8276bf5d8c | ||
|
|
371150dfa8 | ||
|
|
38d71fd6bb | ||
|
|
c627ca96c9 | ||
|
|
09167bb302 | ||
|
|
a3c24b1b02 | ||
|
|
9f4a3cf8a8 | ||
|
|
702d27393e | ||
|
|
698d3a311d | ||
|
|
c92f8e3b51 | ||
|
|
7c502ecd01 | ||
|
|
a237a31d23 | ||
|
|
5f4fc77258 | ||
|
|
8bd4df68f5 | ||
|
|
29cff02f3c | ||
|
|
d1bc41e1f1 | ||
|
|
2fd1bf4c29 | ||
|
|
edd89fb040 | ||
|
|
08d6b58c8c | ||
|
|
e3fc9f5116 | ||
|
|
6dcd808cc8 | ||
|
|
df6230fc01 | ||
|
|
9f502ae6ae | ||
|
|
2a979d2b15 | ||
|
|
ae484e3b67 | ||
|
|
7206832edb | ||
|
|
288b32ed79 | ||
|
|
53bc3b354f | ||
|
|
30565c2b50 | ||
|
|
c400c7be9d | ||
|
|
7acd9d2491 | ||
|
|
dc26dba1d0 | ||
|
|
66cbb23a38 | ||
|
|
663fc176d8 | ||
|
|
b9ec22677e | ||
|
|
7d55c7f37f | ||
|
|
f12882a389 | ||
|
|
8cfde9d5c6 | ||
|
|
10180a74b6 | ||
|
|
abd30c397d | ||
|
|
a230027974 | ||
|
|
59f6837914 | ||
|
|
7546af9067 | ||
|
|
1aa3860706 | ||
|
|
fd098a65c5 | ||
|
|
77f7a56df9 | ||
|
|
4fe879085f | ||
|
|
96914ec4f6 | ||
|
|
a1c6b6439c | ||
|
|
b91dba3141 | ||
|
|
718c0a668a | ||
|
|
c62963f455 | ||
|
|
cc39a37beb | ||
|
|
1bc8450110 | ||
|
|
bec0ca6dea | ||
|
|
7c7296a57d | ||
|
|
9684ce4a1b | ||
|
|
c0df55ee62 | ||
|
|
b598e2613e | ||
|
|
99e578b1bc | ||
|
|
3766491c28 | ||
|
|
71f13b24cb | ||
|
|
4a3d20576e | ||
|
|
ecbc243c6b | ||
|
|
c012ca1183 | ||
|
|
820acd90e7 | ||
|
|
e904a5b42d | ||
|
|
cb051c033f | ||
|
|
565fc1f7b6 | ||
|
|
f6b8fae903 | ||
|
|
5819af5e7c | ||
|
|
f4057fb8f8 | ||
|
|
783a36be84 | ||
|
|
091d6e00b1 | ||
|
|
d721d00139 | ||
|
|
f7797d4b6f | ||
|
|
f810373227 | ||
|
|
c808225f78 | ||
|
|
0b06bf82d8 | ||
|
|
6969107c68 | ||
|
|
6347de62f0 | ||
|
|
1f09c122de | ||
|
|
f3de209f99 | ||
|
|
d29c833866 | ||
|
|
edac522d06 | ||
|
|
3b1a890423 | ||
|
|
4d4864819a | ||
|
|
151e7e04eb | ||
|
|
03c634658b | ||
|
|
10c54509aa | ||
|
|
8c74a559c3 | ||
|
|
d3ae8045c0 | ||
|
|
b9753beeac | ||
|
|
6a602adc4d | ||
|
|
c6835ac4f9 | ||
|
|
7af4332d04 | ||
|
|
af5b5e0f67 | ||
|
|
e6368f5355 | ||
|
|
1fe9826871 | ||
|
|
01ea047521 | ||
|
|
f24a73412a | ||
|
|
4def209c62 | ||
|
|
a4a7a1e507 | ||
|
|
560725431d | ||
|
|
ffde4987e9 | ||
|
|
3334371ae3 | ||
|
|
8b1808fa2e | ||
|
|
7d86d31844 | ||
|
|
0cbfa62731 | ||
|
|
db735b58ec | ||
|
|
17fe11e8f5 | ||
|
|
fc65d8a535 | ||
|
|
6ade056bf8 | ||
|
|
a5ebf08a8c | ||
|
|
ca2eccc762 | ||
|
|
efd76a1407 | ||
|
|
48785e93f0 | ||
|
|
20ddf60c48 | ||
|
|
1b9b0226f2 | ||
|
|
a1f8ff7463 | ||
|
|
aafe7f44e7 | ||
|
|
8909224a13 | ||
|
|
f5d01bef93 | ||
|
|
f7de97c844 | ||
|
|
2136fddec5 | ||
|
|
312152de78 | ||
|
|
4fe7d10872 | ||
|
|
9ec10dc5dc | ||
|
|
2672459287 | ||
|
|
8a8918c049 | ||
|
|
98e0a92745 | ||
|
|
2b21f5f48f | ||
|
|
69ddc939b3 | ||
|
|
17ad6362c3 | ||
|
|
0f22c9e31a | ||
|
|
07f9b30ed5 | ||
|
|
7756becb35 | ||
|
|
cf2ac010bb | ||
|
|
76c8e98c02 | ||
|
|
fe66b44b10 | ||
|
|
97695e4b5a | ||
|
|
59653f4030 | ||
|
|
530b632c46 | ||
|
|
c8f1a10b8b | ||
|
|
84570c5024 | ||
|
|
624d21c4dc | ||
|
|
8db54d015a | ||
|
|
9204c720a4 | ||
|
|
daf81b5d78 | ||
|
|
c2c3f55925 | ||
|
|
ba520f1d90 | ||
|
|
81d1dbdcf0 | ||
|
|
23cb2655d1 | ||
|
|
9c4f94238d | ||
|
|
d28723916d | ||
|
|
65d4c9c8ee | ||
|
|
3a1f7a13a1 | ||
|
|
69d97a508c | ||
|
|
e7dc23ae26 | ||
|
|
4cffe30910 | ||
|
|
6f0dfba4aa | ||
|
|
6d617216c7 | ||
|
|
13d5e5e6af | ||
|
|
c697413554 | ||
|
|
994ae3e2f5 | ||
|
|
69ab292394 | ||
|
|
1985d31a07 | ||
|
|
b813185dc3 | ||
|
|
6daf04c9fc | ||
|
|
fd49ec4f97 | ||
|
|
84a4921629 | ||
|
|
b4aeaff783 | ||
|
|
caf73b89ea | ||
|
|
1c406557a4 | ||
|
|
d39eac9512 | ||
|
|
2530d31d58 | ||
|
|
c6894a1e95 | ||
|
|
05fdea3267 | ||
|
|
e0f7d9cd32 | ||
|
|
91b9fe23cc | ||
|
|
4e4e2907d9 | ||
|
|
b94f54f56d | ||
|
|
12932a1d33 | ||
|
|
7beef30d20 | ||
|
|
f1e4e3c753 | ||
|
|
d1ba758c6f | ||
|
|
c0472056ac | ||
|
|
1780c479d5 | ||
|
|
59f8465ece | ||
|
|
256f72dd08 | ||
|
|
836c7769f6 | ||
|
|
ebe880bd39 | ||
|
|
10fea79a35 | ||
|
|
451600448e | ||
|
|
7fccd6261e | ||
|
|
2436dab797 | ||
|
|
56538623a5 | ||
|
|
42a667c0ef | ||
|
|
5df95ad0fa | ||
|
|
345598016a | ||
|
|
6da38e537b | ||
|
|
e65f664821 | ||
|
|
cfa1eedae7 | ||
|
|
130c8d0e89 | ||
|
|
c4a2695b69 | ||
|
|
246069cc48 | ||
|
|
36a7d4a510 | ||
|
|
1b4a53f572 | ||
|
|
61a9d80e2c | ||
|
|
31568a5806 | ||
|
|
9aa73f5a5c | ||
|
|
35943feb08 | ||
|
|
73de21f936 | ||
|
|
4f1af87ba0 | ||
|
|
d129bd3e6b | ||
|
|
c2c52e8747 | ||
|
|
7e2ab49f89 | ||
|
|
3192d41fe3 | ||
|
|
ad5b260460 | ||
|
|
e57cb0ce51 | ||
|
|
9514357391 | ||
|
|
6fd0a40cc6 | ||
|
|
350d63231f | ||
|
|
cc28f1d5b5 | ||
|
|
4b73af9250 | ||
|
|
6e0efe521d | ||
|
|
041ab30778 | ||
|
|
678be4b92f | ||
|
|
74d759a45b | ||
|
|
295499c17b | ||
|
|
043d1c88c8 | ||
|
|
55e9b93b1c | ||
|
|
aa5e3e9697 | ||
|
|
fbf7ff6ebd | ||
|
|
3a5e8a827c | ||
|
|
6176dfaaf7 | ||
|
|
76c361e9bc | ||
|
|
ccb8981634 | ||
|
|
99b55d03c1 | ||
|
|
651fa6e2d1 | ||
|
|
4b258da0a0 | ||
|
|
9a66605616 | ||
|
|
da13895b72 | ||
|
|
c657a1d2cc | ||
|
|
eac8896f3e | ||
|
|
113f4f948b | ||
|
|
1b8dc1f75e | ||
|
|
001236e8e6 | ||
|
|
8025b8e24b | ||
|
|
e48bf012a6 | ||
|
|
94e32694b2 | ||
|
|
cbcf033b15 | ||
|
|
7da2671ec5 | ||
|
|
b6ea4c7003 | ||
|
|
49378061bf | ||
|
|
38013a9298 | ||
|
|
5ffbd16800 | ||
|
|
bcb29ef792 | ||
|
|
d20adb20d3 | ||
|
|
4f28cdd58f | ||
|
|
4b6b5c2f0e | ||
|
|
c31269a358 | ||
|
|
55092326a4 | ||
|
|
10dbcf63b7 | ||
|
|
32fad77104 | ||
|
|
05d2fbbbb9 | ||
|
|
916733a1e8 | ||
|
|
83b4b58cb2 | ||
|
|
11f53c0e03 | ||
|
|
2fa8aee055 | ||
|
|
d2848e12f3 | ||
|
|
36ba840fe8 | ||
|
|
819d41a75c | ||
|
|
3a6c3dc51d | ||
|
|
db721107ae | ||
|
|
5b9be471e3 | ||
|
|
e8f8617412 | ||
|
|
eb472037db | ||
|
|
81b7afd08d | ||
|
|
7a71ecf177 | ||
|
|
a12e485a27 | ||
|
|
af521b2893 | ||
|
|
aab6f68492 | ||
|
|
f67c3d5284 | ||
|
|
46385a8445 | ||
|
|
9f617c33f5 | ||
|
|
6aef034880 | ||
|
|
c4941a01d6 | ||
|
|
7380423eee | ||
|
|
f35073ffb5 | ||
|
|
f89ea56d86 | ||
|
|
a240b8ca9f | ||
|
|
230ec2bc8e | ||
|
|
a3c2642b93 | ||
|
|
de755d5495 | ||
|
|
08054f3026 | ||
|
|
026239903d | ||
|
|
a320fe4cc4 | ||
|
|
b122d4c5e4 | ||
|
|
ec37b32eaa | ||
|
|
f893eeea1f | ||
|
|
8d559dba02 | ||
|
|
d167c712a6 | ||
|
|
8b224cb240 | ||
|
|
651aedaa50 | ||
|
|
a36b0ebed2 | ||
|
|
5a9e8683e4 | ||
|
|
843b29eff0 | ||
|
|
7bad0be2c4 | ||
|
|
5561011122 | ||
|
|
192a0c987e | ||
|
|
85a9a21e57 | ||
|
|
610864d674 | ||
|
|
06d0fa6e22 | ||
|
|
fb9a6fd314 | ||
|
|
5cab8ad41e | ||
|
|
d9f2b964c9 | ||
|
|
ac78b69ec2 | ||
|
|
516975688f | ||
|
|
7f07439388 | ||
|
|
bf87c4ac52 | ||
|
|
272132af51 | ||
|
|
04dd179631 | ||
|
|
57554c8d4b | ||
|
|
74a20bf11d | ||
|
|
16c96419d3 | ||
|
|
0b444bc5a2 | ||
|
|
7846e9e196 | ||
|
|
a74b971b98 | ||
|
|
6707ce1ed9 | ||
|
|
de421f93b5 | ||
|
|
1762e78cb3 | ||
|
|
2631f3a007 | ||
|
|
f70ec8a090 | ||
|
|
a4f22671c6 | ||
|
|
71c23df971 | ||
|
|
1bb2593c5b | ||
|
|
45d5ef4602 | ||
|
|
7b5b2596b9 | ||
|
|
23a805d576 | ||
|
|
f8cf3c22a1 | ||
|
|
8e5725946c | ||
|
|
99bb0d7a93 | ||
|
|
9695ddf8de | ||
|
|
50cff0bcba | ||
|
|
1832cf5924 | ||
|
|
10fba0a2b2 | ||
|
|
e2050fd47a | ||
|
|
f72da5973f | ||
|
|
0ebeffac32 | ||
|
|
dcfe724803 | ||
|
|
821020e7b6 | ||
|
|
33efc38f4c | ||
|
|
f01c0408e1 | ||
|
|
365ae0ef8c | ||
|
|
a6e593e3a1 | ||
|
|
67d38d6333 | ||
|
|
513d101fc4 | ||
|
|
5e1304f699 | ||
|
|
955a06d689 | ||
|
|
59e1a470e3 | ||
|
|
16952f5695 | ||
|
|
67feca075b | ||
|
|
12368375cf | ||
|
|
284ffb7aa0 | ||
|
|
1aefb26575 | ||
|
|
670d0f8462 | ||
|
|
3bbe422421 | ||
|
|
d68e988c15 | ||
|
|
74fd35047c | ||
|
|
21a9d19105 | ||
|
|
f662e983a4 | ||
|
|
7db379492a | ||
|
|
4a82daccec | ||
|
|
21e44bbc90 | ||
|
|
42a40a844a | ||
|
|
cd7c515097 | ||
|
|
9595626a27 | ||
|
|
db105c90b5 | ||
|
|
74a305c32a | ||
|
|
3af115ce37 | ||
|
|
b4e7934396 | ||
|
|
0a54037460 | ||
|
|
dd8da57cfb | ||
|
|
9b6f2d00ca | ||
|
|
2852821d2c | ||
|
|
673bc75cd5 | ||
|
|
3fad76aae1 | ||
|
|
2cb70a41fb | ||
|
|
ef4f2ed791 | ||
|
|
c9a73a366d | ||
|
|
7973179ddf | ||
|
|
6b656bdc48 | ||
|
|
428c1a1799 | ||
|
|
8892e2b273 | ||
|
|
2e8e6dd39e | ||
|
|
3ac16b15c6 | ||
|
|
ec3dfb908c | ||
|
|
b5484bb13b | ||
|
|
c5a43efafb | ||
|
|
23c2c0689c | ||
|
|
948d64237e | ||
|
|
3433e88bb8 | ||
|
|
28278c091d | ||
|
|
04a68d0b95 | ||
|
|
9b0c0d1870 | ||
|
|
86734d2f5f | ||
|
|
7d5a73828d | ||
|
|
0fd8a78972 | ||
|
|
fca78cf419 | ||
|
|
5f98c8c8bb | ||
|
|
dc4a791a9e | ||
|
|
d89c8cc2d2 | ||
|
|
9b24c683c3 | ||
|
|
2acea40204 | ||
|
|
f014f59cf2 | ||
|
|
6221f4b616 | ||
|
|
32d557ee07 | ||
|
|
ba6eb17a38 | ||
|
|
935436ce49 | ||
|
|
877c5a827c | ||
|
|
eedcff785e | ||
|
|
60865163db | ||
|
|
c32a14a959 | ||
|
|
3ddab57ab5 | ||
|
|
20957106df | ||
|
|
f528f3bf40 | ||
|
|
14fd0d484e | ||
|
|
6fc60f1387 | ||
|
|
5ad46279c2 | ||
|
|
f4894ccb9d | ||
|
|
2b04292424 | ||
|
|
775b58d0fd | ||
|
|
6d09d0984e | ||
|
|
6ce5106271 | ||
|
|
fdbe38496a | ||
|
|
abae32fd70 | ||
|
|
e4a1fa5bb4 | ||
|
|
1b8cf6b1e5 | ||
|
|
135f15dd9f | ||
|
|
1cd4470187 | ||
|
|
3a2f5a77ef | ||
|
|
4201ac9203 | ||
|
|
6c3178a831 | ||
|
|
d48591a4cd | ||
|
|
271885a85e | ||
|
|
57e6765bfd | ||
|
|
9a46596b6c | ||
|
|
27f6ff74fc | ||
|
|
03df019638 | ||
|
|
23750e1222 | ||
|
|
ba0a7ddfa8 | ||
|
|
31327d685b | ||
|
|
bb6bd86e71 | ||
|
|
4ba897660b | ||
|
|
c3fc22c6a9 | ||
|
|
8748b8c277 | ||
|
|
e934d6c983 | ||
|
|
dfb2d3f2bf | ||
|
|
c2a87e2474 | ||
|
|
9229727544 | ||
|
|
cc8dd77d90 | ||
|
|
de9233fba5 | ||
|
|
05cda699a5 | ||
|
|
358a520305 | ||
|
|
57cb98f13a | ||
|
|
33aeb24dbe | ||
|
|
973510a3df | ||
|
|
ae8f67d507 | ||
|
|
a48336db4f | ||
|
|
e69ef0ce25 | ||
|
|
e01c31fb1d | ||
|
|
36d1096976 | ||
|
|
6901228a15 | ||
|
|
8100662eb5 | ||
|
|
ee4c03a427 | ||
|
|
adb2ed0614 | ||
|
|
e78fd8ba94 | ||
|
|
754ecadbb1 | ||
|
|
ee89815628 | ||
|
|
8a9527bf38 | ||
|
|
e476460ab4 | ||
|
|
b556c51307 | ||
|
|
fac812f1dd | ||
|
|
c66cea41bd | ||
|
|
8a3cdfb14c | ||
|
|
a58de31d4b | ||
|
|
04e6897a0d | ||
|
|
59df08cd93 | ||
|
|
c70898ced7 | ||
|
|
ecc38621df | ||
|
|
56327eba54 | ||
|
|
e776532947 | ||
|
|
35f2d89a23 | ||
|
|
3515ce1034 | ||
|
|
6c1046c060 | ||
|
|
7c76c919bc | ||
|
|
54954f276b | ||
|
|
7fd8e0721f | ||
|
|
d075dd27bc | ||
|
|
ace8be3df4 | ||
|
|
c60df2c26b | ||
|
|
cd030c1de0 | ||
|
|
01893b053d | ||
|
|
79e4aa5aaf | ||
|
|
3f4dbb6978 | ||
|
|
1df5b0c629 | ||
|
|
83dbeddb12 | ||
|
|
a8522320ed | ||
|
|
bcaf3de164 | ||
|
|
cbe9f7474e | ||
|
|
3a4b4e129f | ||
|
|
4535b546f4 | ||
|
|
7d0bbc6952 | ||
|
|
ea844e03f9 | ||
|
|
c7a893b02d | ||
|
|
ca75ecbc93 | ||
|
|
3f9d16ab8c | ||
|
|
fb7ff7b891 | ||
|
|
3cf5b97201 | ||
|
|
2f5a9a50af | ||
|
|
8153d3bda9 | ||
|
|
e796e0f35e | ||
|
|
4537fe0390 | ||
|
|
87944a4934 | ||
|
|
33b9d37250 | ||
|
|
ebeec04f83 | ||
|
|
798327eabb | ||
|
|
702dd21eec | ||
|
|
e5754c70da | ||
|
|
b95119bb0e | ||
|
|
ae8600c882 | ||
|
|
271fb4ce87 | ||
|
|
37cee9cebd | ||
|
|
0b2e9c3f62 | ||
|
|
f2741f9725 | ||
|
|
e4a7ba0bb5 | ||
|
|
6ac8519a9f | ||
|
|
01158c2320 | ||
|
|
bbb78ec749 | ||
|
|
90976cc7e0 | ||
|
|
cfe801f280 | ||
|
|
ca3bc69a61 | ||
|
|
43236049ac | ||
|
|
3c797252d8 | ||
|
|
543238be4b | ||
|
|
1b539ade9d | ||
|
|
c0f82b1ae7 | ||
|
|
5b02393c79 | ||
|
|
c54a3f2053 | ||
|
|
e90ec531e5 | ||
|
|
6ac2274c29 | ||
|
|
ea9f547ada | ||
|
|
28cc7c0700 | ||
|
|
6761028afd | ||
|
|
27f87c3a25 | ||
|
|
223c45130f | ||
|
|
3513c1a431 | ||
|
|
5b4185840f | ||
|
|
390f6e016d | ||
|
|
e10e1e0613 | ||
|
|
ba41e6bc5e | ||
|
|
05562b5c2a | ||
|
|
7d9cce131b | ||
|
|
112b096c56 | ||
|
|
aecb100498 | ||
|
|
d5b4aa5d44 | ||
|
|
cbe5e3a9ff | ||
|
|
ff3fa31c25 | ||
|
|
7baf8eba6e | ||
|
|
53b0223257 | ||
|
|
91cc83c054 | ||
|
|
b1031be748 | ||
|
|
c6b76f5200 | ||
|
|
08269b7ce9 | ||
|
|
c65ef8107d | ||
|
|
9fdf4f16c6 | ||
|
|
e821f7cb09 | ||
|
|
5c9deb129d | ||
|
|
fa1c11db5c | ||
|
|
69b118eaec | ||
|
|
f088bfa114 | ||
|
|
f4c25003a0 | ||
|
|
43ae5a24e5 | ||
|
|
fd27f16714 | ||
|
|
beea7d0ae2 | ||
|
|
eebd373aef | ||
|
|
b10330548e | ||
|
|
475d7f50f1 | ||
|
|
b689e68d46 | ||
|
|
87adbde965 | ||
|
|
5057691fb3 | ||
|
|
d7f5109a65 | ||
|
|
97f08bf74e | ||
|
|
1b3e4e5bd6 | ||
|
|
0032242da6 | ||
|
|
0df1b62c3e | ||
|
|
8927260b75 | ||
|
|
edd4add76e | ||
|
|
2d543e69ff | ||
|
|
275c14f556 | ||
|
|
f45e0abbf4 | ||
|
|
2d8248461a | ||
|
|
e4847bbfcf | ||
|
|
127e069041 | ||
|
|
87bf7c8d8d | ||
|
|
513897406d | ||
|
|
4ace33bd80 | ||
|
|
c8ba03ae2b | ||
|
|
784e22afa7 | ||
|
|
eedb9f148c | ||
|
|
0256b8a971 | ||
|
|
95ea6610b1 | ||
|
|
fd246b7bc6 | ||
|
|
98c6651793 | ||
|
|
3327899cf6 | ||
|
|
8c3904cdcc | ||
|
|
108df2293b | ||
|
|
336d7ba221 | ||
|
|
0fdac8a308 | ||
|
|
effc6d725a | ||
|
|
7adc517872 | ||
|
|
e385a85755 | ||
|
|
279b2d6f80 | ||
|
|
71a845c1ed | ||
|
|
8a908b06b6 | ||
|
|
d94b3385c7 | ||
|
|
1b72e43cce | ||
|
|
2e8d1a7702 | ||
|
|
6eeb2e922d | ||
|
|
36878c81aa | ||
|
|
e4ac791569 | ||
|
|
8f591f99bd | ||
|
|
fc21e76507 | ||
|
|
ef118d3736 | ||
|
|
47523db331 | ||
|
|
7a2c52b34c | ||
|
|
3ff2284ccc | ||
|
|
e7ce68d3cb | ||
|
|
b78f5407cc | ||
|
|
b7997dfbce | ||
|
|
d8943c266e | ||
|
|
3051332165 | ||
|
|
dc2911f7d8 | ||
|
|
6a2ffa4f30 | ||
|
|
56bd2e0d95 | ||
|
|
d521d10a85 | ||
|
|
c844a77f02 | ||
|
|
b3e3192876 | ||
|
|
21e28ab038 | ||
|
|
47540e7f91 | ||
|
|
3a406744c2 | ||
|
|
8a0faa72c1 | ||
|
|
5d768fe64e | ||
|
|
7166a809c3 | ||
|
|
c11d947099 | ||
|
|
723f216e6b | ||
|
|
1000cc1ab0 | ||
|
|
d77a09042d | ||
|
|
5310de88d5 | ||
|
|
ba33b927a7 | ||
|
|
72ca0f20a1 | ||
|
|
bb15d72032 | ||
|
|
cabba82118 | ||
|
|
353d2794c9 | ||
|
|
faede16662 | ||
|
|
665b0c4390 | ||
|
|
feb15a3c64 | ||
|
|
050fa30a35 | ||
|
|
3314bf93b0 | ||
|
|
7eed142a6f | ||
|
|
ac4de8b9fd | ||
|
|
64a92b7768 | ||
|
|
cb49700ded | ||
|
|
243c9b83b9 | ||
|
|
b3d083877f | ||
|
|
5d7c9cd15d | ||
|
|
96987763a5 | ||
|
|
57bb719cb8 | ||
|
|
981127c355 | ||
|
|
770a7411aa | ||
|
|
e428cc7117 | ||
|
|
832c029db6 | ||
|
|
b700ad6b0c | ||
|
|
575f27cde1 | ||
|
|
48aab94fdf | ||
|
|
19bcf453a5 | ||
|
|
0210b9fd3d | ||
|
|
a20331dfa8 | ||
|
|
088b2f146f | ||
|
|
e9847918a9 | ||
|
|
fcbfbd060e | ||
|
|
f2ff523109 | ||
|
|
9cee8fa49f | ||
|
|
13038437fd | ||
|
|
8c641555cf | ||
|
|
4df2af82d1 | ||
|
|
50ce3617be | ||
|
|
fb78b8f573 | ||
|
|
f8eae1f534 | ||
|
|
1a0fd11e83 | ||
|
|
d5e2bc455f | ||
|
|
23bb926acf | ||
|
|
038cc97ecf | ||
|
|
b778d0a014 | ||
|
|
de99213d8a | ||
|
|
5a1ebe906d | ||
|
|
c23b9511b8 | ||
|
|
6ee39aa302 | ||
|
|
e3fb36c63f | ||
|
|
dd6c192a93 | ||
|
|
798c67eaef | ||
|
|
33d27f74fe | ||
|
|
680bcb8129 | ||
|
|
90ee87062f | ||
|
|
924743c622 | ||
|
|
56865d602f | ||
|
|
dc32151501 | ||
|
|
b9382ce966 | ||
|
|
2de5c36df5 | ||
|
|
3972683583 | ||
|
|
fcafca2fdb | ||
|
|
635351d58d | ||
|
|
dc3caea2f4 | ||
|
|
9bef8b432b | ||
|
|
6e45ea835f | ||
|
|
f1e4364ffa | ||
|
|
a560eff153 | ||
|
|
e9bc1524f1 | ||
|
|
cc868656a8 | ||
|
|
96e6554b37 | ||
|
|
f869ddbbd2 | ||
|
|
6ebdfb0a26 | ||
|
|
e2ac171a92 | ||
|
|
9abca03e50 | ||
|
|
63f4a3dc5c | ||
|
|
34ceb1a29d | ||
|
|
8230c28084 | ||
|
|
a85d3b504f | ||
|
|
6ac3f68dfc | ||
|
|
e37322d147 | ||
|
|
f2dd7e4295 | ||
|
|
647bbb38e8 | ||
|
|
ff899db6b0 | ||
|
|
8bf2a2fad3 | ||
|
|
e9061ebc8b | ||
|
|
5edd7a7b72 | ||
|
|
bd343f49f6 | ||
|
|
f5a16ae005 | ||
|
|
837c7adaa7 | ||
|
|
a27df90fc5 | ||
|
|
c18b0510bc | ||
|
|
7bf06684c4 | ||
|
|
f79465038b | ||
|
|
54fe311a3d | ||
|
|
3b8a0989de | ||
|
|
5890f0c782 | ||
|
|
b39508922f | ||
|
|
13034db3f9 | ||
|
|
a21e303a8a | ||
|
|
4bdacb66a6 | ||
|
|
2fea07fc6c | ||
|
|
9741ba4b07 | ||
|
|
d2d0875c36 | ||
|
|
494d40c8b5 | ||
|
|
0b78033a87 | ||
|
|
66bb27c4dd | ||
|
|
f00afbe1b5 | ||
|
|
7da9d0f283 | ||
|
|
ddd0f83936 | ||
|
|
c660b1d3f8 | ||
|
|
2022af8a53 | ||
|
|
b3564d9d27 | ||
|
|
28d6f1f19a | ||
|
|
588fe5cb5f | ||
|
|
e138815c7b | ||
|
|
9bc10fb27d | ||
|
|
7d39d00a6a | ||
|
|
f448229e22 | ||
|
|
d74d800d48 | ||
|
|
69ded66c92 | ||
|
|
3c8d17b564 | ||
|
|
e61af71415 | ||
|
|
7d1f50519c | ||
|
|
6f5cc4d59d | ||
|
|
220cd144f5 | ||
|
|
820c5a563a | ||
|
|
b2594122a8 | ||
|
|
1ddcc23b89 | ||
|
|
d57a4655a6 | ||
|
|
df4620518c | ||
|
|
b8ca9a3b09 | ||
|
|
1ad8beb49e | ||
|
|
134f96be08 | ||
|
|
cb03d9841d | ||
|
|
fe922e0bed | ||
|
|
53c63db150 | ||
|
|
5ddb69077f | ||
|
|
1fb9fa0af9 | ||
|
|
d35fb9a486 | ||
|
|
37fb9a0d65 | ||
|
|
af7cf393dc | ||
|
|
7a20af462b | ||
|
|
0939ed819e | ||
|
|
5da4ad7a62 | ||
|
|
123c863e3d | ||
|
|
6fc0b13dc7 | ||
|
|
59bf8d3ebc | ||
|
|
805393b184 | ||
|
|
5d0d77aff4 | ||
|
|
d2e4ddc876 | ||
|
|
5b7ca8c751 | ||
|
|
16c7558497 | ||
|
|
9b6b3f8938 | ||
|
|
331d8d1bb6 | ||
|
|
e5cca24740 | ||
|
|
6f40335322 | ||
|
|
b6de9d4ea2 | ||
|
|
5f8f01e0ab | ||
|
|
0d69e944f6 | ||
|
|
1dc4e3f730 | ||
|
|
14d1bccd14 | ||
|
|
5de150b06e | ||
|
|
e34dd39b9a | ||
|
|
e7d5faeeac | ||
|
|
46a7269087 | ||
|
|
dc31674721 | ||
|
|
afe6bc7456 | ||
|
|
378fd04463 | ||
|
|
3e751ba5c6 | ||
|
|
8fc1ec7bb8 | ||
|
|
e5326a3c4d | ||
|
|
fe279234d8 | ||
|
|
ef9b041310 | ||
|
|
9db576d4bc | ||
|
|
a84971abfc | ||
|
|
a7a79db842 | ||
|
|
d5da14fb1e | ||
|
|
0d2e287f67 | ||
|
|
c329f1cafb | ||
|
|
ab1a2f6fcb | ||
|
|
463dc56702 | ||
|
|
88e23cd0d2 | ||
|
|
9776aafab6 | ||
|
|
fbb5a03e5d | ||
|
|
7c11ce53f3 | ||
|
|
66f4c022d5 | ||
|
|
4b36450f9a | ||
|
|
36b58bd1bb | ||
|
|
0296b6f73d | ||
|
|
447afaa76f | ||
|
|
a0191aa7c2 | ||
|
|
e7c9fcf767 | ||
|
|
fc017515c5 | ||
|
|
6d75c4d34f | ||
|
|
258a6628e4 | ||
|
|
228695d511 | ||
|
|
c2af530abb | ||
|
|
69bc826e30 | ||
|
|
5d0944ed19 | ||
|
|
43f4da5dd3 | ||
|
|
c6ab2cec35 | ||
|
|
b0dc689ba7 | ||
|
|
e11b1f37b9 | ||
|
|
3e5fac4df6 | ||
|
|
e63fa40822 | ||
|
|
e01096bff0 | ||
|
|
2a0cfb1125 | ||
|
|
d4f0a04ccd | ||
|
|
7cbe24b35b | ||
|
|
b7da21e5ce | ||
|
|
0ae1411f0a | ||
|
|
66889cac27 | ||
|
|
67bd2ac386 | ||
|
|
1f278c2a49 | ||
|
|
1674f1828f | ||
|
|
472a0a039f |
No files matched your search
@@ -1 +0,0 @@
|
||||
*.exe filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -1,28 +0,0 @@
|
||||
# 公告
|
||||
|
||||
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
|
||||
|
||||
**如果你在阅读完本公告后仍然希望在此处发布 issue,请将公告内容删去,并按照下面的模板填入相关信息。**
|
||||
|
||||
|
||||
|
||||
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
|
||||
|
||||
如果你希望在 QQ 群中反馈问题,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
|
||||
|
||||
|
||||
|
||||
# 问题提交
|
||||
|
||||
*完整地填下面的问题提交表对我们很重要,这可以加快我们分析问题原因的速度。*
|
||||
|
||||
* 启动器版本:
|
||||
* 操作系统:
|
||||
* Java 版本:
|
||||
* 错误截图(最好请将整个电脑屏幕的截图发上来):
|
||||
* 游戏版本(如果是启动通过启动器自带的自动安装功能安装的游戏):
|
||||
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
|
||||
* 游戏崩溃报告(如果有):
|
||||
* 启动器崩溃报告(如果有):
|
||||
* 启动器日志文件(出现问题后点击启动器设置中的【导出启动器日志】按钮):
|
||||
* 问题描述(如何触发问题):
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Bug 反馈 | Bug Feedback
|
||||
description:
|
||||
反馈一个 HMCL 错误。| Bug Feedback.
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
提交前请确认:
|
||||
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
|
||||
* 你的启动器版本是**最新的快照版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新快照版本。
|
||||
|
||||
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
|
||||
|
||||
Before submitting, please confirm:
|
||||
* The issue is indeed **an error of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
|
||||
* Your launcher version is **the latest snapshot version**. You can click [here](https://github.com/burningtnt/HMCL-Snapshot-Update/raw/master/datas/HMCL-dev.jar) to download the latest snapshot version.
|
||||
|
||||
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台 | Platform
|
||||
description: 请输入您遇到 BUG 的平台。 Please enter the platform on which you encountered the bug.
|
||||
placeholder: e.g. Windows 11
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-report
|
||||
attributes:
|
||||
label: 问题描述 | Problem Description
|
||||
description: 请尽可能地详细描述你所遇到的问题,并描述如何重新触发这个问题。 Please describe the problem you met in as much detail as possible. Besides, please describe how to trigger this problem again.
|
||||
placeholder: |
|
||||
1. 点击 HMCL 上的某个按钮 | Click a button named ...
|
||||
2. 向下翻页 | Scroll down
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: hmcl-crash-report-or-logs
|
||||
attributes:
|
||||
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
|
||||
description: |
|
||||
如果您的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
|
||||
如果您的启动器没有崩溃,请在遇到问题后不要退出启动器,在启动器的【设置】>【通用】>【调试】一栏中点击“导出启动器日志”,并将导出的日志拖到下方的输入栏中。
|
||||
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
|
||||
|
||||
If your launcher crashes, please fill it in (or drag the file in) below.
|
||||
If your launcher doesn't crash, please DO NOT EXIT your launcher, click "Export Launcher Log" in the [Settings] > [General] > [Debug] column of the launcher, and drag the exported log to the input field below.
|
||||
**Attention: The crash report or the log file is the key to solve the problem. Please update it!**
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: QQ 群 | QQ Groups
|
||||
url: https://docs.hmcl.net/groups.html
|
||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ groups of Hello Minecraft! Laucher.
|
||||
- name: Discord 频道 | Discord Channel
|
||||
url: https://discord.gg/jVvC7HfM6U
|
||||
about: Hello Minecraft! Launcher 的官方 Discord 频道。| The official Discord channel of Hello Minecraft! Launcher.
|
||||
- name: 其他反馈 | Others
|
||||
url: https://github.com/HMCL-dev/HMCL/discussions/new/choose
|
||||
about: 通过 Discussions 反馈其他问题。| Report other problems in Discussions.
|
||||
@@ -0,0 +1,22 @@
|
||||
name: 新功能 | Feature Request
|
||||
description: 为 HMCL 提出新功能 | A new feature for HMCL
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 请确认 Issues 列表无重复的项目。| Please make sure that no duplicated issues has already been delivered.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 描述 | Description
|
||||
description: 请详细描述你想加入的新功能。| Please describe the new feature detaily.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reason
|
||||
attributes:
|
||||
label: 原因 | Reason
|
||||
description: 请描述该功能带来的好处及原因。| Please describe why you want to add the feature into HMCL.
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Build HMCLauncher
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'HMCLauncher/**'
|
||||
- '.github/workflows/build-launcher.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'HMCLauncher/**'
|
||||
- '.github/workflows/build-launcher.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
with:
|
||||
msbuild-architecture: x86
|
||||
- name: Build HMCLauncher
|
||||
run: msbuild /p:Configuration=Release /t:Rebuild /verbosity:detailed .\HMCLauncher\
|
||||
- name: Copy HMCLauncher to assets
|
||||
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\src\main\resources\assets\HMCLauncher.exe
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
run: .\gradlew makeExecutables --no-daemon
|
||||
env:
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=$("${{ github.sha }}".SubString(0, 7))" >> $env:GITHUB_ENV
|
||||
- name: Copy HMCLauncher to libs
|
||||
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\build\libs\HMCLauncher.exe
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCLauncher-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs/*.exe
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Check Style
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.java'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.java'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain --no-daemon
|
||||
- name: Check style test
|
||||
run: ./gradlew checkstyleTest --no-daemon
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Check Translations
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.properties'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.properties'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style test
|
||||
run: ./gradlew checkTranslations --no-daemon
|
||||
@@ -0,0 +1,135 @@
|
||||
name: Check Update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 * * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
dev-check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Fetch last version
|
||||
run: |
|
||||
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
|
||||
|
||||
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
|
||||
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
|
||||
|
||||
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
|
||||
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
|
||||
|
||||
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
|
||||
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
|
||||
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
|
||||
- name: Check for existing tags
|
||||
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.exe | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.jar | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.sh | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.sh
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
stable-check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
needs: dev-check-update
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Fetch last version
|
||||
run: |
|
||||
wget -O ci.json https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json
|
||||
|
||||
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
|
||||
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
|
||||
|
||||
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
|
||||
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
|
||||
|
||||
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
|
||||
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
|
||||
echo "HMCL_TAG_NAME=release-$HMCL_VERSION" >> $GITHUB_ENV
|
||||
- name: Check for existing tags
|
||||
run: if ! git tag -l | grep -q "$HMCL_TAG_NAME"; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "**This version is a stable version.**" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/stable.html" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.exe | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.jar | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.sh | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.sh
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Sync to Gitee
|
||||
|
||||
on:
|
||||
push
|
||||
|
||||
jobs:
|
||||
run:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror GitHub to Gitee
|
||||
uses: Yikun/hub-mirror-action@v1.4
|
||||
with:
|
||||
src: github/HMCL-dev
|
||||
dst: gitee/huanghongxun
|
||||
static_list: 'HMCL'
|
||||
force_update: true
|
||||
debug: true
|
||||
dst_key: ${{ secrets.GITEE_SYNC_BOT_PRIVATE_KEY }}
|
||||
dst_token: ${{ secrets.GITEE_SYNC_BOT_TOKEN }}
|
||||
cache_path: /github/workspace/hub-mirror-cache
|
||||
@@ -1,6 +1,11 @@
|
||||
name: Java CI
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'HMCLauncher/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -8,11 +13,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 8
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-package: jdk+fx
|
||||
distribution: 'zulu'
|
||||
java-version: 8
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
run: ./gradlew build --no-daemon
|
||||
env:
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
@@ -10,27 +10,29 @@ hs_err_pid*
|
||||
*.2
|
||||
*.log
|
||||
.mine*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
*.json
|
||||
minecraft-exported-crash-info*
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
/HMCL/build/
|
||||
/HMCLCore/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
|
||||
|
||||
# idea
|
||||
.idea
|
||||
/out/
|
||||
/HMCL/out/
|
||||
/HMCLCore/out/
|
||||
/HMCLTransformerDiscoveryService/out/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
|
||||
|
||||
# eclipse
|
||||
/bin/
|
||||
/HMCL/bin/
|
||||
/HMCLCore/bin/
|
||||
/HMCLTransformerDiscoveryService/bin/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
@@ -38,4 +40,15 @@ NVIDIA
|
||||
# netbeans
|
||||
.nb-gradle
|
||||
|
||||
*.exe
|
||||
*.exe
|
||||
!/HMCL/src/main/resources/assets/HMCLauncher.exe
|
||||
|
||||
# macos
|
||||
.DS_Store
|
||||
|
||||
# vscode
|
||||
.vscode/
|
||||
|
||||
# test
|
||||
/hmcl.json
|
||||
/.hmcl.json
|
||||
@@ -0,0 +1,2 @@
|
||||
/data.csv
|
||||
/data.json
|
||||
@@ -1,14 +0,0 @@
|
||||
language: java
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
before_install:
|
||||
- chmod +x gradlew
|
||||
script: "bash ./gradlew clean build --stacktrace"
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
cache:
|
||||
directories:
|
||||
- "$HOME/.gradle/caches/"
|
||||
@@ -1,210 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '7.0.0'
|
||||
}
|
||||
|
||||
import java.nio.file.FileSystems
|
||||
import java.security.KeyFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.jar.Pack200
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import java.util.zip.ZipFile
|
||||
import java.nio.file.Files
|
||||
|
||||
import org.tukaani.xz.LZMA2Options
|
||||
import org.tukaani.xz.XZOutputStream
|
||||
|
||||
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
|
||||
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
|
||||
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
|
||||
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
|
||||
version = versionroot + '.' + buildnumber
|
||||
|
||||
mainClassName = 'org.jackhuang.hmcl.Main'
|
||||
|
||||
dependencies {
|
||||
implementation project(":HMCLCore")
|
||||
implementation rootProject.files("lib/JFoenix.jar")
|
||||
}
|
||||
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
return MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
}
|
||||
|
||||
def createChecksum(File file) {
|
||||
def algorithm = "SHA-1"
|
||||
def suffix = "sha1"
|
||||
new File(file.parentFile, file.name + "." + suffix).text = digest(algorithm, file.bytes).encodeHex().toString() + "\n"
|
||||
}
|
||||
|
||||
def attachSignature(File jar) {
|
||||
def keyLocation = System.getenv("HMCL_SIGNATURE_KEY");
|
||||
if (keyLocation == null)
|
||||
return
|
||||
def privatekey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(new File(keyLocation).bytes))
|
||||
|
||||
def signer = Signature.getInstance("SHA512withRSA")
|
||||
signer.initSign(privatekey)
|
||||
new ZipFile(jar).withCloseable { zip ->
|
||||
zip.stream()
|
||||
.sorted(Comparator.comparing { it.name })
|
||||
.filter { it.name != "META-INF/hmcl_signature" }
|
||||
.forEach {
|
||||
signer.update(digest("SHA-512", it.name.getBytes("UTF-8")))
|
||||
signer.update(digest("SHA-512", zip.getInputStream(it).bytes))
|
||||
}
|
||||
}
|
||||
def signature = signer.sign()
|
||||
|
||||
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), [:]).withCloseable { zipfs ->
|
||||
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).withCloseable { it << signature }
|
||||
}
|
||||
}
|
||||
|
||||
ext.packer = Pack200.newPacker()
|
||||
packer.properties()["pack.effort"] = "9"
|
||||
ext.unpacker = Pack200.newUnpacker()
|
||||
|
||||
// Pack200 does not guarantee that unpacked .class file is bit-wise same as the .class file before packing
|
||||
// because of shrinking. So we should pack .class files and unpack it to make sure that after unpacking
|
||||
// .class files remain the same.
|
||||
def repack(File file) {
|
||||
def packed = new ByteArrayOutputStream()
|
||||
new JarFile(file).withCloseable { packer.pack(it, packed) }
|
||||
new JarOutputStream(file.newOutputStream()).withCloseable { unpacker.unpack(new ByteArrayInputStream(packed.toByteArray()), it) }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
java11 {
|
||||
java {
|
||||
srcDirs = ['src/main/java11']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileJava11Java {
|
||||
javaCompiler = javaToolchains.compilerFor {
|
||||
languageVersion = JavaLanguageVersion.of(11)
|
||||
}
|
||||
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
dependsOn shadowJar
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
classifier = null
|
||||
|
||||
manifest {
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
|
||||
'Main-Class': mainClassName,
|
||||
'Multi-Release': 'true',
|
||||
'Implementation-Version': version,
|
||||
'Add-Opens': [
|
||||
'java.base/java.lang',
|
||||
'java.base/java.lang.reflect',
|
||||
'javafx.graphics/javafx.css',
|
||||
'javafx.base/com.sun.javafx.runtime',
|
||||
'javafx.controls/com.sun.javafx.scene.control.behavior',
|
||||
'javafx.controls/javafx.scene.control.skin',
|
||||
'javafx.controls/com.sun.javafx.scene.control',
|
||||
'javafx.base/com.sun.javafx.binding',
|
||||
'javafx.base/com.sun.javafx.event',
|
||||
'javafx.graphics/com.sun.javafx.stage'
|
||||
].join(" "),
|
||||
'Add-Exports': [
|
||||
'java.base/jdk.internal.loader',
|
||||
'javafx.controls/com.sun.javafx.scene.control.behavior',
|
||||
'javafx.controls/javafx.scene.control.skin',
|
||||
'javafx.controls/com.sun.javafx.scene.control',
|
||||
'javafx.base/com.sun.javafx.binding',
|
||||
'javafx.graphics/com.sun.javafx.stage',
|
||||
'javafx.base/com.sun.javafx.event'
|
||||
].join(" ")
|
||||
}
|
||||
|
||||
doLast {
|
||||
repack(jar.archivePath) // see repack()
|
||||
attachSignature(jar.archivePath)
|
||||
createChecksum(jar.archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
def createExecutable(String suffix, String header) {
|
||||
def output = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + suffix)
|
||||
output.bytes = new File(project.projectDir, header).bytes
|
||||
output << jar.archivePath.bytes
|
||||
createChecksum(output)
|
||||
}
|
||||
|
||||
processResources {
|
||||
ext.convertToBSS = { String resource ->
|
||||
// exclude resource
|
||||
doFirst {
|
||||
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
|
||||
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
|
||||
bssFile.parentFile.mkdirs()
|
||||
exec {
|
||||
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
|
||||
}
|
||||
}
|
||||
}
|
||||
from "build/compiled-resources"
|
||||
|
||||
convertToBSS "assets/css/root.css"
|
||||
convertToBSS "assets/css/blue.css"
|
||||
|
||||
into('META-INF/versions/11') {
|
||||
from sourceSets.java11.output
|
||||
}
|
||||
dependsOn java11Classes
|
||||
}
|
||||
|
||||
task makePack(dependsOn: jar) {
|
||||
ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack")
|
||||
doLast {
|
||||
outputPath.newOutputStream().withCloseable { out ->
|
||||
new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) }
|
||||
}
|
||||
createChecksum(outputPath)
|
||||
}
|
||||
}
|
||||
|
||||
task makePackXz(dependsOn: makePack) doLast {
|
||||
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
|
||||
// Our CI server does not have enough memory space to compress file at highest level.
|
||||
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(5)).withCloseable { it << makePack.outputPath.bytes }
|
||||
createChecksum(packXz)
|
||||
}
|
||||
|
||||
task makePackGz(dependsOn: makePack) doLast {
|
||||
def packGz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".gz")
|
||||
new GZIPOutputStream(packGz.newOutputStream()).withCloseable { it << makePack.outputPath.bytes }
|
||||
createChecksum(packGz)
|
||||
}
|
||||
|
||||
|
||||
task makeExecutables(dependsOn: jar) doLast {
|
||||
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
|
||||
}
|
||||
|
||||
build.dependsOn makePackXz
|
||||
build.dependsOn makePackGz
|
||||
build.dependsOn makeExecutables
|
||||
@@ -0,0 +1,252 @@
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.security.KeyFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
plugins {
|
||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||
}
|
||||
|
||||
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|
||||
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
|
||||
.isNullOrEmpty())
|
||||
|
||||
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
|
||||
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
|
||||
if (number != null) {
|
||||
(number - offset).toString()
|
||||
} else {
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
|
||||
val prefix = if (isOfficial) "dev" else "unofficial"
|
||||
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
|
||||
}
|
||||
}
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
|
||||
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
|
||||
|
||||
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||
|
||||
version = "$versionRoot.$buildNumber"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
implementation("libs:JFoenix")
|
||||
}
|
||||
|
||||
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
|
||||
fun createChecksum(file: File) {
|
||||
val algorithms = linkedMapOf(
|
||||
"MD5" to "md5",
|
||||
"SHA-1" to "sha1",
|
||||
"SHA-256" to "sha256",
|
||||
"SHA-512" to "sha512"
|
||||
)
|
||||
|
||||
algorithms.forEach { (algorithm, ext) ->
|
||||
File(file.parentFile, "${file.name}.$ext").writeText(
|
||||
digest(algorithm, file.readBytes()).joinToString(separator = "", postfix = "\n") { "%02x".format(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun attachSignature(jar: File) {
|
||||
val keyLocation = System.getenv("HMCL_SIGNATURE_KEY")
|
||||
if (keyLocation == null) {
|
||||
logger.warn("Missing signature key")
|
||||
return
|
||||
}
|
||||
|
||||
val privatekey = KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(File(keyLocation).readBytes()))
|
||||
val signer = Signature.getInstance("SHA512withRSA")
|
||||
signer.initSign(privatekey)
|
||||
ZipFile(jar).use { zip ->
|
||||
zip.stream()
|
||||
.sorted(Comparator.comparing { it.name })
|
||||
.filter { it.name != "META-INF/hmcl_signature" }
|
||||
.forEach {
|
||||
signer.update(digest("SHA-512", it.name.toByteArray()))
|
||||
signer.update(digest("SHA-512", zip.getInputStream(it).readBytes()))
|
||||
}
|
||||
}
|
||||
val signature = signer.sign()
|
||||
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), emptyMap<String, Any>()).use { zipfs ->
|
||||
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).use { it.write(signature) }
|
||||
}
|
||||
}
|
||||
|
||||
val java11 = sourceSets.create("java11") {
|
||||
java {
|
||||
srcDir("src/main/java11")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
|
||||
if (JavaVersion.current() < JavaVersion.VERSION_11) {
|
||||
javaCompiler.set(javaToolchains.compilerFor {
|
||||
languageVersion.set(JavaLanguageVersion.of(11))
|
||||
})
|
||||
}
|
||||
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||
sourceCompatibility = "11"
|
||||
targetCompatibility = "11"
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
enabled = false
|
||||
dependsOn(tasks["shadowJar"])
|
||||
}
|
||||
|
||||
val jarPath = tasks.jar.get().archiveFile.get().asFile
|
||||
|
||||
tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
|
||||
archiveClassifier.set(null as String?)
|
||||
|
||||
exclude("**/package-info.class")
|
||||
exclude("META-INF/maven/**")
|
||||
|
||||
minimize {
|
||||
exclude(dependency("com.google.code.gson:.*:.*"))
|
||||
exclude(dependency("libs:JFoenix:.*"))
|
||||
}
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2024 huangyuhui.",
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
"Implementation-Version" to project.version,
|
||||
"Microsoft-Auth-Id" to microsoftAuthId,
|
||||
"Microsoft-Auth-Secret" to microsoftAuthSecret,
|
||||
"CurseForge-Api-Key" to curseForgeApiKey,
|
||||
"Build-Channel" to versionType,
|
||||
"Class-Path" to "pack200.jar",
|
||||
"Add-Opens" to listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
"java.base/jdk.internal.loader",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.event",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.graphics/com.sun.javafx.stage",
|
||||
"javafx.graphics/com.sun.prism",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"jdk.attach/sun.tools.attach"
|
||||
).joinToString(" ")
|
||||
)
|
||||
|
||||
System.getenv("GITHUB_SHA")?.also {
|
||||
attributes("GitHub-SHA" to it)
|
||||
}
|
||||
}
|
||||
|
||||
doLast {
|
||||
attachSignature(jarPath)
|
||||
createChecksum(jarPath)
|
||||
}
|
||||
}
|
||||
|
||||
fun createExecutable(suffix: String, header: String) {
|
||||
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + suffix)
|
||||
|
||||
output.outputStream().use {
|
||||
it.write(File(project.projectDir, header).readBytes())
|
||||
it.write(jarPath.readBytes())
|
||||
}
|
||||
|
||||
createChecksum(output)
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
into("META-INF/versions/11") {
|
||||
from(sourceSets["java11"].output)
|
||||
}
|
||||
dependsOn(tasks["java11Classes"])
|
||||
}
|
||||
|
||||
val makeExecutables = tasks.create("makeExecutables") {
|
||||
dependsOn(tasks.jar)
|
||||
doLast {
|
||||
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
|
||||
createExecutable("sh", "src/main/resources/assets/HMCLauncher.sh")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn(makeExecutables)
|
||||
}
|
||||
|
||||
fun parseToolOptions(options: String?): List<String> {
|
||||
if (options == null)
|
||||
return listOf()
|
||||
|
||||
val builder = StringBuilder()
|
||||
val result = mutableListOf<String>()
|
||||
|
||||
var offset = 0
|
||||
|
||||
loop@ while (offset < options.length) {
|
||||
val ch = options[offset]
|
||||
if (Character.isWhitespace(ch)) {
|
||||
if (builder.isNotEmpty()) {
|
||||
result += builder.toString()
|
||||
builder.clear()
|
||||
}
|
||||
|
||||
while (offset < options.length && Character.isWhitespace(options[offset])) {
|
||||
offset++
|
||||
}
|
||||
|
||||
continue@loop
|
||||
}
|
||||
|
||||
if (ch == '\'' || ch == '"') {
|
||||
offset++
|
||||
|
||||
while (offset < options.length) {
|
||||
val ch2 = options[offset++]
|
||||
if (ch2 != ch) {
|
||||
builder.append(ch2)
|
||||
} else {
|
||||
continue@loop
|
||||
}
|
||||
}
|
||||
|
||||
throw GradleException("Unmatched quote in $options")
|
||||
}
|
||||
|
||||
builder.append(ch)
|
||||
offset++
|
||||
}
|
||||
|
||||
if (builder.isNotEmpty()) {
|
||||
result += builder.toString()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
tasks.create<JavaExec>("run") {
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
group = "application"
|
||||
|
||||
classpath = files(jarPath)
|
||||
workingDir = rootProject.rootDir
|
||||
|
||||
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
|
||||
jvmArgs(vmOptions)
|
||||
|
||||
doFirst {
|
||||
logger.quiet("HMCL_JAVA_OPTS: $vmOptions")
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="#5865F2" fill-rule="nonzero">
|
||||
␍</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 -0.5 52 56" shape-rendering="crispEdges" xmlns="http://www.w3.org/2000/svg"><path stroke="#38342a" d="M28 0h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m4 0h4M24 5h4m4 0h4M24 6h4m4 0h4M24 7h4m4 0h4M24 8h4m8 0h4M24 9h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-20 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-28 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-36 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8M8 24h4m28 0h4m4 0h4M8 25h4m28 0h4m4 0h4M8 26h4m28 0h4m4 0h4M8 27h4m28 0h4m4 0h4M4 28h4m32 0h8M4 29h4m32 0h8M4 30h4m32 0h8M4 31h4m32 0h8M0 32h4m32 0h4M0 33h4m32 0h4M0 34h4m32 0h4M0 35h4m32 0h4M0 36h4m28 0h4M0 37h4m28 0h4M0 38h4m28 0h4M0 39h4m28 0h4M4 40h4m20 0h4M4 41h4m20 0h4M4 42h4m20 0h4M4 43h4m20 0h4M8 44h4m12 0h4M8 45h4m12 0h4M8 46h4m12 0h4M8 47h4m12 0h4m-16 1h4m4 0h8m-16 1h4m4 0h8m-16 1h4m4 0h8m-16 1h4m4 0h8m-12 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#dbd0b4" d="M28 4h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m-12 1h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-16 1h8m8 0h4m-20 1h8m8 0h4m-20 1h8m8 0h4m-20 1h8m8 0h4m-28 1h8m4 0h8m-20 1h8m4 0h8m-20 1h8m4 0h8m-20 1h8m4 0h8m-24 1h16m-16 1h16m-16 1h16m-16 1h16M8 28h8m4 0h12M8 29h8m4 0h12M8 30h8m4 0h12M8 31h8m4 0h12M8 32h12m8 0h4M8 33h12m8 0h4M8 34h12m8 0h4M8 35h12m8 0h4m-20 1h12m-12 1h12m-12 1h12m-12 1h12m-8 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#c6bca5" d="M28 8h4m-4 1h4m-4 1h4m-4 1h4m-12 5h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h8m-8 1h8m-8 1h8m-8 1h8m-20 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#bcb29c" d="M32 12h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h8m-8 1h8m-8 1h8m-8 1h8m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-12 1h8m-8 1h8m-8 1h8m-8 1h8m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#807a6d" d="M44 24h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#aea694" d="M4 32h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#9a927e" d="M4 36h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 81 KiB |
@@ -0,0 +1,123 @@
|
||||
#
|
||||
# Hello Minecraft! Launcher
|
||||
# Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import json
|
||||
import codecs
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
S = ';'
|
||||
|
||||
MOD_SEPARATOR = ','
|
||||
|
||||
CURSEFORGE_PATTERN1 = re.compile(
|
||||
r'^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons)/+(?P<modid>[\w-]+)(/(.*?))?$')
|
||||
CURSEFORGE_PATTERN2 = re.compile(
|
||||
r'^/projects/(?P<modid>[\w-]+)(/(.*?))?$')
|
||||
CURSEFORGE_PATTERN3 = re.compile(
|
||||
r'^/mc-mods/minecraft/(?P<modid>[\w-]+)(/(.*?))?$')
|
||||
CURSEFORGE_PATTERN4 = re.compile(
|
||||
r'^/legacy/mc-mods/minecraft/(\d+)-(?P<modid>[\w-]+)'
|
||||
)
|
||||
|
||||
|
||||
def parseCurseforge(url):
|
||||
res = urlparse(url)
|
||||
if res.scheme not in ['http', 'https']:
|
||||
return ''
|
||||
for pattern in [CURSEFORGE_PATTERN1, CURSEFORGE_PATTERN2, CURSEFORGE_PATTERN3, CURSEFORGE_PATTERN4]:
|
||||
match = pattern.match(res.path)
|
||||
if match:
|
||||
return match.group('modid')
|
||||
return ''
|
||||
|
||||
|
||||
MCMOD_PATTERN = re.compile(
|
||||
r'^http(s)?://www\.mcmod\.cn/(class|modpack)/(?P<modid>\d+)\.html$')
|
||||
|
||||
|
||||
def parseMcmod(url):
|
||||
match = MCMOD_PATTERN.match(url)
|
||||
if match:
|
||||
return match.group('modid')
|
||||
return ''
|
||||
|
||||
skip = [
|
||||
'Minecraft',
|
||||
'The Building Game'
|
||||
]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
json_name = sys.argv[1] or 'data.json'
|
||||
|
||||
with codecs.open(json_name, mode='r', encoding='utf-8-sig') as jsonfile, codecs.open('data.csv', mode='w', encoding='utf-8') as outfile:
|
||||
data = json.load(jsonfile)
|
||||
|
||||
for mod in data:
|
||||
chinese_name = mod['name']['main']
|
||||
sub_name = mod['name']['sub']
|
||||
abbr = mod['name']['abbr']
|
||||
|
||||
if sub_name in skip:
|
||||
continue
|
||||
|
||||
if S in chinese_name:
|
||||
print('Error! ' + chinese_name)
|
||||
exit(1)
|
||||
|
||||
if S in sub_name:
|
||||
print('Error! ' + chinese_name)
|
||||
exit(1)
|
||||
|
||||
black_lists = [
|
||||
'Wood Converter'
|
||||
]
|
||||
|
||||
curseforge_id = ''
|
||||
mcmod_id = ''
|
||||
links = mod['links']['list']
|
||||
if 'curseforge' in links and links['curseforge'] and sub_name not in black_lists and chinese_name not in black_lists:
|
||||
for link in links['curseforge']:
|
||||
curseforge_id = parseCurseforge(link['url'])
|
||||
if curseforge_id != '':
|
||||
break
|
||||
if curseforge_id == '':
|
||||
print('Error curseforge ' + chinese_name)
|
||||
exit(1)
|
||||
if 'mcmod' in links and links['mcmod']:
|
||||
mcmod_id = parseMcmod(links['mcmod'][0]['url'])
|
||||
if mcmod_id == '':
|
||||
print('Error mcmod ' + chinese_name)
|
||||
exit(1)
|
||||
|
||||
mod_id = []
|
||||
if 'modid' in mod and 'list' in mod['modid']:
|
||||
for id in mod['modid']['list']:
|
||||
if MOD_SEPARATOR in id:
|
||||
print('Error mod id!' + id)
|
||||
exit(1)
|
||||
|
||||
mod_id.append(id)
|
||||
mod_ids = MOD_SEPARATOR.join([str(id) for id in mod_id])
|
||||
|
||||
outfile.write(
|
||||
f'{curseforge_id}{S}{mcmod_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
|
||||
|
||||
print('Success!')
|
||||
@@ -1,2 +0,0 @@
|
||||
rootProject.name = 'HMCL'
|
||||
|
||||
@@ -19,33 +19,41 @@ package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.CrashReporter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.net.*;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Launcher extends Application {
|
||||
@@ -57,12 +65,43 @@ public final class Launcher extends Application {
|
||||
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
|
||||
try {
|
||||
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
|
||||
LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get prism pipeline", e);
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
} catch (SambaException ignored) {
|
||||
Main.showWarningAndContinue(i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to load config", e);
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
|
||||
LOG.error("Failed to load config", e);
|
||||
checkConfigInTempDir();
|
||||
checkConfigOwner();
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
|
||||
}
|
||||
|
||||
// https://lapcatsoftware.com/articles/app-translocation.html
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
|
||||
&& ConfigHolder.isNewlyCreated()
|
||||
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
|
||||
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
|
||||
return;
|
||||
} else {
|
||||
checkConfigInTempDir();
|
||||
}
|
||||
|
||||
if (ConfigHolder.isOwnerChanged()) {
|
||||
if (showAlert(AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
|
||||
return;
|
||||
}
|
||||
|
||||
if (Metadata.HMCL_DIRECTORY.toString().indexOf('=') >= 0) {
|
||||
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
|
||||
}
|
||||
|
||||
// runLater to ensure ConfigHolder.init() finished initialization
|
||||
@@ -72,8 +111,6 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
initIcon();
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
@@ -83,20 +120,100 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
super.stop();
|
||||
Controllers.onApplicationStop();
|
||||
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
|
||||
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
||||
}
|
||||
|
||||
private void initIcon() {
|
||||
Toolkit toolkit = Toolkit.getDefaultToolkit();
|
||||
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
private static boolean isConfigInTempDir() {
|
||||
String configPath = ConfigHolder.configLocation().toString();
|
||||
|
||||
String tmpdir = System.getProperty("java.io.tmpdir");
|
||||
if (StringUtils.isNotBlank(tmpdir) && configPath.startsWith(tmpdir))
|
||||
return true;
|
||||
|
||||
String[] tempFolderNames = {"Temp", "Cache", "Caches"};
|
||||
for (String name : tempFolderNames) {
|
||||
if (configPath.contains(File.separator + name + File.separator))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
return configPath.contains("\\Temporary Internet Files\\")
|
||||
|| configPath.contains("\\INetCache\\")
|
||||
|| configPath.contains("\\$Recycle.Bin\\")
|
||||
|| configPath.contains("\\recycler\\");
|
||||
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
return configPath.startsWith("/tmp/")
|
||||
|| configPath.startsWith("/var/tmp/")
|
||||
|| configPath.startsWith("/var/cache/")
|
||||
|| configPath.startsWith("/dev/shm/")
|
||||
|| configPath.contains("/Trash/");
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
return configPath.startsWith("/var/folders/")
|
||||
|| configPath.startsWith("/private/var/folders/")
|
||||
|| configPath.startsWith("/tmp/")
|
||||
|| configPath.startsWith("/private/tmp/")
|
||||
|| configPath.startsWith("/var/tmp/")
|
||||
|| configPath.startsWith("/private/var/tmp/")
|
||||
|| configPath.contains("/.Trash/");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkConfigInTempDir() {
|
||||
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
|
||||
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
|
||||
Main.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkConfigOwner() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
return;
|
||||
|
||||
String userName = System.getProperty("user.name");
|
||||
String owner;
|
||||
try {
|
||||
owner = Files.getOwner(ConfigHolder.configLocation()).getName();
|
||||
} catch (IOException ioe) {
|
||||
LOG.warning("Failed to get file owner", ioe);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Files.isWritable(ConfigHolder.configLocation()) || userName.equals("root") || userName.equals(owner))
|
||||
return;
|
||||
|
||||
ArrayList<String> files = new ArrayList<>();
|
||||
files.add(ConfigHolder.configLocation().toString());
|
||||
if (Files.exists(Metadata.HMCL_DIRECTORY))
|
||||
files.add(Metadata.HMCL_DIRECTORY.toString());
|
||||
|
||||
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
|
||||
if (Files.exists(mcDir))
|
||||
files.add(mcDir.toString());
|
||||
|
||||
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
|
||||
|
||||
if (showAlert(AlertType.ERROR,
|
||||
i18n("fatal.config_loading_failure.unix", owner, command),
|
||||
copyAndExit, ButtonType.CLOSE) == copyAndExit) {
|
||||
Clipboard.getSystemClipboard()
|
||||
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
|
||||
}
|
||||
Main.exit(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
Controllers.onApplicationStop();
|
||||
LOG.shutdown();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
LOG.shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,17 +222,31 @@ public final class Launcher extends Application {
|
||||
|
||||
try {
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
|
||||
LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
|
||||
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
|
||||
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
|
||||
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
|
||||
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
|
||||
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
|
||||
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
|
||||
LOG.info("Java Home: " + System.getProperty("java.home"));
|
||||
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
|
||||
LOG.info("Current Directory: " + System.getProperty("user.dir"));
|
||||
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
|
||||
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
|
||||
LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
|
||||
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
|
||||
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
|
||||
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
|
||||
LOG.info("Physical memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
|
||||
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
|
||||
.filter(bean -> bean.getName().equals("Metaspace"))
|
||||
.findAny()
|
||||
.map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB")
|
||||
.orElse("Unknown"));
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
|
||||
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
|
||||
}
|
||||
|
||||
launch(args);
|
||||
launch(Launcher.class, args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
@@ -147,29 +278,5 @@ public final class Launcher extends Application {
|
||||
});
|
||||
}
|
||||
|
||||
public static List<File> getCurrentJarFiles() {
|
||||
List<File> result = new LinkedList<>();
|
||||
if (Launcher.class.getClassLoader() instanceof URLClassLoader) {
|
||||
URL[] urls = ((URLClassLoader) Launcher.class.getClassLoader()).getURLs();
|
||||
for (URL u : urls)
|
||||
try {
|
||||
File f = new File(u.toURI());
|
||||
if (f.isFile() && (f.getName().endsWith(".exe") || f.getName().endsWith(".jar")))
|
||||
result.add(f);
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
File jarFile = new File(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath());
|
||||
String ext = FileUtils.getExtension(jarFile);
|
||||
if ("jar".equals(ext) || "exe".equals(ext))
|
||||
result.add(jarFile);
|
||||
}
|
||||
if (result.isEmpty())
|
||||
return null;
|
||||
else
|
||||
return result;
|
||||
}
|
||||
|
||||
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
|
||||
}
|
||||
@@ -17,11 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import javax.swing.*;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -33,39 +39,50 @@ import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.logging.Level;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Main {
|
||||
|
||||
private Main() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.setProperty("java.net.useSystemProxies", "true");
|
||||
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
System.setProperty("javafx.autoproxy.disable", "true");
|
||||
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
||||
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
||||
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
|
||||
LOG.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
thread(() -> {
|
||||
fixLetsEncrypt();
|
||||
checkDSTRootCAX3();
|
||||
}, "CA Certificate Check", true);
|
||||
if (JavaRuntime.CURRENT_VERSION < 9)
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
initIcon();
|
||||
|
||||
checkJavaFX();
|
||||
|
||||
// Fix title bar not displaying in GTK systems
|
||||
System.setProperty("jdk.gtk.version", "2");
|
||||
verifyJavaFX();
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
|
||||
public static void exit(int exitCode) {
|
||||
LOG.shutdown();
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
private static void initIcon() {
|
||||
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon@8x.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
String currentDirectory = new File("").getAbsolutePath();
|
||||
if (currentDirectory.contains("!")) {
|
||||
@@ -79,34 +96,28 @@ public final class Main {
|
||||
try {
|
||||
SelfDependencyPatcher.patch();
|
||||
} catch (SelfDependencyPatcher.PatchException e) {
|
||||
LOG.log(Level.SEVERE, "unable to patch JVM", e);
|
||||
LOG.error("unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.log(Level.SEVERE, "unable to patch JVM", e);
|
||||
LOG.error("unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
} catch (CancellationException e) {
|
||||
LOG.error("User cancels downloading JavaFX", e);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDSTRootCAX3() {
|
||||
TrustManagerFactory tmf;
|
||||
/**
|
||||
* Check if JavaFX exists but is incomplete
|
||||
*/
|
||||
private static void verifyJavaFX() {
|
||||
try {
|
||||
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore) null);
|
||||
} catch (NoSuchAlgorithmException | KeyStoreException e) {
|
||||
LOG.log(Level.WARNING, "Failed to init TrustManagerFactory", e);
|
||||
// don't know what to do here
|
||||
return;
|
||||
Class.forName("javafx.beans.binding.Binding"); // javafx.base
|
||||
Class.forName("javafx.stage.Stage"); // javafx.graphics
|
||||
Class.forName("javafx.scene.control.Skin"); // javafx.controls
|
||||
} catch (Exception e) {
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) {
|
||||
if ("CN=DST Root CA X3, O=Digital Signature Trust Co.".equals((cert.getSubjectDN().getName()))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,8 +126,17 @@ public final class Main {
|
||||
static void showErrorAndExit(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("A fatal error has occurred, forcibly exiting.");
|
||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
System.exit(1);
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.ERROR, message).showAndWait();
|
||||
exit(1);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
SwingUtils.showErrorDialog(message);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,10 +145,19 @@ public final class Main {
|
||||
static void showWarningAndContinue(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("Potential issues have been detected.");
|
||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.WARNING, message).showAndWait();
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
SwingUtils.showWarningDialog(message);
|
||||
}
|
||||
|
||||
static void fixLetsEncrypt() {
|
||||
private static void fixLetsEncrypt() {
|
||||
try {
|
||||
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
|
||||
@@ -138,8 +167,9 @@ public final class Main {
|
||||
}
|
||||
|
||||
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks");
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
merged.load(null, new char[0]);
|
||||
@@ -154,8 +184,9 @@ public final class Main {
|
||||
tls.init(null, instance.getTrustManagers(), null);
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
|
||||
LOG.info("Added Lets Encrypt root certificates as additional trust");
|
||||
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | KeyManagementException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to load lets encrypt certificate. Expect problems", e);
|
||||
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
|
||||
KeyManagementException e) {
|
||||
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
@@ -29,25 +30,53 @@ import java.nio.file.Paths;
|
||||
public final class Metadata {
|
||||
private Metadata() {}
|
||||
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
|
||||
public static final String NAME = "HMCL";
|
||||
public static final String FULL_NAME = "Hello Minecraft! Launcher";
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
|
||||
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
|
||||
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
|
||||
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
|
||||
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
|
||||
public static final String CHANGELOG_URL = "https://hmcl.huangyuhui.net/changelog/";
|
||||
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
|
||||
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
|
||||
public static final String CONTACT_URL = "https://docs.hmcl.net/help.html";
|
||||
public static final String HELP_URL = "https://docs.hmcl.net";
|
||||
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
|
||||
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
||||
public static final String EULA_URL = "https://docs.hmcl.net/eula/hmcl.html";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
|
||||
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
|
||||
public static final Path HMCL_DIRECTORY;
|
||||
|
||||
private static Path getHMCLDirectory() {
|
||||
String home = System.getProperty("user.home", ".");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
// to fulfill XDG standard.
|
||||
return Paths.get(home, ".cache", "hmcl");
|
||||
static {
|
||||
String hmclHome = System.getProperty("hmcl.home");
|
||||
if (hmclHome == null) {
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
String xdgData = System.getenv("XDG_DATA_HOME");
|
||||
if (StringUtils.isNotBlank(xdgData)) {
|
||||
HMCL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath();
|
||||
} else {
|
||||
HMCL_DIRECTORY = Paths.get(System.getProperty("user.home", "."), ".local", "share", "hmcl").toAbsolutePath();
|
||||
}
|
||||
} else {
|
||||
HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
} else {
|
||||
HMCL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
|
||||
}
|
||||
return OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
|
||||
public static boolean isStable() {
|
||||
return "stable".equals(BUILD_CHANNEL);
|
||||
}
|
||||
|
||||
public static boolean isDev() {
|
||||
return "dev".equals(BUILD_CHANNEL);
|
||||
}
|
||||
|
||||
public static boolean isNightly() {
|
||||
return !isStable() && !isDev();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.jackhuang.hmcl.countly;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class CrashReport {
|
||||
|
||||
private final Thread thread;
|
||||
private final Throwable throwable;
|
||||
private final String stackTrace;
|
||||
|
||||
public CrashReport(Thread thread, Throwable throwable) {
|
||||
this.thread = thread;
|
||||
this.throwable = throwable;
|
||||
stackTrace = StringUtils.getStackTrace(throwable);
|
||||
}
|
||||
|
||||
public Throwable getThrowable() {
|
||||
return this.throwable;
|
||||
}
|
||||
|
||||
public boolean shouldBeReport() {
|
||||
if (!stackTrace.contains("org.jackhuang"))
|
||||
return false;
|
||||
|
||||
if (throwable instanceof VirtualMachineError)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getDisplayText() {
|
||||
return "---- Hello Minecraft! Crash Report ----\n" +
|
||||
" Version: " + Metadata.VERSION + "\n" +
|
||||
" Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) + "\n" +
|
||||
" Thread: " + thread + "\n" +
|
||||
"\n Content: \n " +
|
||||
stackTrace + "\n\n" +
|
||||
"-- System Details --\n" +
|
||||
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
|
||||
" System Architecture: " + Architecture.SYSTEM_ARCH_NAME + "\n" +
|
||||
" Java Architecture: " + Architecture.CURRENT_ARCH_NAME + "\n" +
|
||||
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
|
||||
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
|
||||
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
|
||||
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
|
||||
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,16 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -59,15 +59,60 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
|
||||
private void generateOptionsTxt() {
|
||||
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
|
||||
if (optionsFile.exists())
|
||||
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
|
||||
|
||||
if (optionsFile.exists()) {
|
||||
return;
|
||||
try {
|
||||
// TODO: Dirty implementation here
|
||||
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA)
|
||||
FileUtils.writeText(optionsFile, "lang:zh_cn\nforceUnicodeFont:true\n");
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
|
||||
}
|
||||
|
||||
if (configFolder.isDirectory()) {
|
||||
if (findFiles(configFolder, "options.txt")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!I18n.isUseChinese()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String lang;
|
||||
/*
|
||||
1.0- :没有语言选项,遇到这些版本时不设置
|
||||
1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
|
||||
1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文
|
||||
1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
|
||||
1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文
|
||||
*/
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
|
||||
if (gameVersion.compareTo("1.1") < 0) {
|
||||
lang = null;
|
||||
} else if (gameVersion.compareTo("1.11") < 0) {
|
||||
lang = "zh_CN";
|
||||
} else {
|
||||
lang = "zh_cn";
|
||||
}
|
||||
|
||||
if (lang != null) {
|
||||
try {
|
||||
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to generate options.txt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean findFiles(File folder, String fileName) {
|
||||
File[] fs = folder.listFiles();
|
||||
if (fs != null) {
|
||||
for (File f : fs) {
|
||||
if (f.isDirectory())
|
||||
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
|
||||
return true;
|
||||
if (f.getName().equals(fileName))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,32 +20,42 @@ package org.jackhuang.hmcl.game;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.ProxyManager;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final Profile profile;
|
||||
@@ -54,6 +64,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
|
||||
private final Set<String> beingModpackVersions = new HashSet<>();
|
||||
|
||||
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
|
||||
|
||||
public HMCLGameRepository(Profile profile, File baseDirectory) {
|
||||
super(baseDirectory);
|
||||
this.profile = profile;
|
||||
@@ -88,6 +100,13 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<Version> getDisplayVersions() {
|
||||
return getVersions().stream()
|
||||
.filter(v -> !v.isHidden())
|
||||
.sorted(Comparator.comparing((Version v) -> Lang.requireNonNullElse(v.getReleaseTime(), Instant.EPOCH))
|
||||
.thenComparing(v -> VersionNumber.asVersion(v.getId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshVersionsImpl() {
|
||||
localVersionSettings.clear();
|
||||
@@ -104,8 +123,11 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
if (!file.exists() && !versions.isEmpty())
|
||||
FileUtils.writeText(file, PROFILE);
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
LOG.warning("Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
}
|
||||
|
||||
public void changeDirectory(File newDirectory) {
|
||||
@@ -124,13 +146,26 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
File srcDir = getVersionRoot(srcId);
|
||||
File dstDir = getVersionRoot(dstId);
|
||||
Path srcDir = getVersionRoot(srcId).toPath();
|
||||
Path dstDir = getVersionRoot(dstId).toPath();
|
||||
|
||||
Version fromVersion = getVersion(srcId);
|
||||
|
||||
if (Files.exists(dstDir)) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir, dstDir);
|
||||
|
||||
Path fromJson = dstDir.resolve(srcId + ".json");
|
||||
Path fromJar = dstDir.resolve(srcId + ".jar");
|
||||
Path toJson = dstDir.resolve(dstId + ".json");
|
||||
Path toJar = dstDir.resolve(dstId + ".jar");
|
||||
|
||||
if (Files.exists(fromJar)) {
|
||||
Files.move(fromJar, toJar);
|
||||
}
|
||||
Files.move(fromJson, toJson);
|
||||
|
||||
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
|
||||
|
||||
if (dstDir.exists()) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
oldVersionSetting.setUsesGlobal(false);
|
||||
@@ -141,17 +176,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
File srcGameDir = getRunDirectory(srcId);
|
||||
File dstGameDir = getRunDirectory(dstId);
|
||||
|
||||
List<String> blackList = new ArrayList<>(Arrays.asList(
|
||||
"regex:(.*?)\\.log",
|
||||
"usernamecache.json", "usercache.json", // Minecraft
|
||||
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
|
||||
"backup", "pack.json", "launcher.jar", "cache", // HMCL
|
||||
".curseclient", // Curse
|
||||
".fabric", ".mixin.out", // Fabric
|
||||
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
|
||||
"downloads", // Curse
|
||||
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
|
||||
));
|
||||
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
|
||||
blackList.add(srcId + ".jar");
|
||||
blackList.add(srcId + ".json");
|
||||
if (!copySaves)
|
||||
@@ -204,6 +229,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
*
|
||||
* @return corresponding version setting, null if the version has no its own version setting.
|
||||
*/
|
||||
@Nullable
|
||||
public VersionSetting getLocalVersionSetting(String id) {
|
||||
if (!localVersionSettings.containsKey(id))
|
||||
loadLocalVersionSetting(id);
|
||||
@@ -213,6 +239,15 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return setting;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public VersionSetting getLocalVersionSettingOrCreate(String id) {
|
||||
VersionSetting vs = getLocalVersionSetting(id);
|
||||
if (vs == null) {
|
||||
vs = createLocalVersionSetting(id);
|
||||
}
|
||||
return vs;
|
||||
}
|
||||
|
||||
public VersionSetting getVersionSetting(String id) {
|
||||
VersionSetting vs = getLocalVersionSetting(id);
|
||||
if (vs == null || vs.isUsesGlobal()) {
|
||||
@@ -223,25 +258,92 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return vs;
|
||||
}
|
||||
|
||||
public File getVersionIconFile(String id) {
|
||||
return new File(getVersionRoot(id), "icon.png");
|
||||
public Optional<File> getVersionIconFile(String id) {
|
||||
File root = getVersionRoot(id);
|
||||
|
||||
File iconFile = new File(root, "icon.png");
|
||||
if (iconFile.exists()) {
|
||||
return Optional.of(iconFile);
|
||||
}
|
||||
|
||||
iconFile = new File(root, "icon.jpg");
|
||||
if (iconFile.exists()) {
|
||||
return Optional.of(iconFile);
|
||||
}
|
||||
|
||||
iconFile = new File(root, "icon.bmp");
|
||||
if (iconFile.exists()) {
|
||||
return Optional.of(iconFile);
|
||||
}
|
||||
|
||||
iconFile = new File(root, "icon.gif");
|
||||
if (iconFile.exists()) {
|
||||
return Optional.of(iconFile);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public void setVersionIconFile(String id, File iconFile) throws IOException {
|
||||
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
|
||||
if (!ext.equals("png") && !ext.equals("jpg") && !ext.equals("bmp") && !ext.equals("gif")) {
|
||||
throw new IllegalArgumentException("Unsupported icon file: " + ext);
|
||||
}
|
||||
|
||||
deleteIconFile(id);
|
||||
|
||||
FileUtils.copyFile(iconFile, new File(getVersionRoot(id), "icon." + ext));
|
||||
}
|
||||
|
||||
public void deleteIconFile(String id) {
|
||||
File root = getVersionRoot(id);
|
||||
|
||||
new File(root, "icon.png").delete();
|
||||
new File(root, "icon.jpg").delete();
|
||||
new File(root, "icon.bmp").delete();
|
||||
new File(root, "icon.gif").delete();
|
||||
}
|
||||
|
||||
public Image getVersionIconImage(String id) {
|
||||
if (id == null || !isLoaded())
|
||||
return newImage("/assets/img/grass.png");
|
||||
return VersionIconType.DEFAULT.getIcon();
|
||||
|
||||
Version version = getVersion(id).resolve(this);
|
||||
File iconFile = getVersionIconFile(id);
|
||||
if (iconFile.exists())
|
||||
return new Image("file:" + iconFile.getAbsolutePath());
|
||||
else if (version.getMainClass() != null &&
|
||||
(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|
||||
|| version.getMainClass().startsWith("net.fabricmc")
|
||||
|| LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass())))
|
||||
return newImage("/assets/img/furnace.png");
|
||||
else
|
||||
return newImage("/assets/img/grass.png");
|
||||
VersionSetting vs = getLocalVersionSettingOrCreate(id);
|
||||
VersionIconType iconType = vs != null ? Lang.requireNonNullElse(vs.getVersionIcon(), VersionIconType.DEFAULT) : VersionIconType.DEFAULT;
|
||||
|
||||
if (iconType == VersionIconType.DEFAULT) {
|
||||
Version version = getVersion(id).resolve(this);
|
||||
Optional<File> iconFile = getVersionIconFile(id);
|
||||
if (iconFile.isPresent()) {
|
||||
try (InputStream inputStream = new FileInputStream(iconFile.get())) {
|
||||
return new Image(inputStream);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load version icon of " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (LibraryAnalyzer.isModded(this, version)) {
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null);
|
||||
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
|
||||
return VersionIconType.FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
|
||||
return VersionIconType.FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
|
||||
return VersionIconType.NEO_FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
|
||||
return VersionIconType.QUILT.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
|
||||
return VersionIconType.OPTIFINE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
|
||||
return VersionIconType.CHICKEN.getIcon();
|
||||
else
|
||||
return VersionIconType.FURNACE.getIcon();
|
||||
}
|
||||
|
||||
return VersionIconType.DEFAULT.getIcon();
|
||||
} else {
|
||||
return iconType.getIcon();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean saveVersionSetting(String id) {
|
||||
@@ -255,7 +357,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
|
||||
LOG.error("Unable to save version setting of " + id, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -281,32 +383,50 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
vs.setUsesGlobal(true);
|
||||
}
|
||||
|
||||
public boolean forbidsVersion(String id) {
|
||||
return FORBIDDEN.contains(id);
|
||||
}
|
||||
|
||||
public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException {
|
||||
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
|
||||
VersionSetting vs = getVersionSetting(version);
|
||||
|
||||
JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionName(Metadata.TITLE)
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setVersionName(version)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory(vs.getMaxMemory())
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
|
||||
vs.getMaxMemory() * 1024L * 1024L,
|
||||
OperatingSystem.getPhysicalMemoryStatus().getAvailable(),
|
||||
vs.isAutoMemory()
|
||||
) / 1024 / 1024))
|
||||
.setMinMemory(vs.getMinMemory())
|
||||
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
|
||||
.setEnvironmentVariables(
|
||||
Lang.mapOf(StringUtils.tokenize(vs.getEnvironmentVariables())
|
||||
.stream()
|
||||
.map(it -> {
|
||||
int idx = it.indexOf('=');
|
||||
return idx >= 0 ? pair(it.substring(0, idx), it.substring(idx + 1)) : pair(it, "");
|
||||
})
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
)
|
||||
.setWidth(vs.getWidth())
|
||||
.setHeight(vs.getHeight())
|
||||
.setFullscreen(vs.isFullscreen())
|
||||
.setServerIp(vs.getServerIp())
|
||||
.setWrapper(vs.getWrapper())
|
||||
.setPrecalledCommand(vs.getPreLaunchCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs());
|
||||
.setPreLaunchCommand(vs.getPreLaunchCommand())
|
||||
.setPostExitCommand(vs.getPostExitCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
.setProcessPriority(vs.getProcessPriority())
|
||||
.setRenderer(vs.getRenderer())
|
||||
.setUseNativeGLFW(vs.isUseNativeGLFW())
|
||||
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
|
||||
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
|
||||
.setJavaAgents(javaAgents);
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxy(ProxyManager.getProxy());
|
||||
if (config().hasProxyAuth()) {
|
||||
@@ -320,16 +440,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
try {
|
||||
String jsonText = FileUtils.readText(json);
|
||||
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
|
||||
if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(modpackConfiguration.getType())) {
|
||||
ModpackConfiguration<McbbsModpackManifest> config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
|
||||
}.getType());
|
||||
config.getManifest().injectLaunchOptions(builder);
|
||||
}
|
||||
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
|
||||
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
|
||||
builder.setMaxMemory(null);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@@ -364,7 +484,56 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
private static final HashSet<String> FORBIDDEN = new HashSet<>(Arrays.asList("modpack", "minecraftinstance", "manifest"));
|
||||
|
||||
private static final String PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}";
|
||||
|
||||
|
||||
// These version ids are forbidden because they may conflict with modpack configuration filenames
|
||||
private static final Set<String> FORBIDDEN_VERSION_IDS = new HashSet<>(Arrays.asList(
|
||||
"modpack", "minecraftinstance", "manifest"));
|
||||
|
||||
public static boolean isValidVersionId(String id) {
|
||||
if (FORBIDDEN_VERSION_IDS.contains(id))
|
||||
return false;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS &&
|
||||
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
|
||||
return false;
|
||||
|
||||
return OperatingSystem.isNameValid(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given version id conflicts with an existing version.
|
||||
*/
|
||||
public boolean versionIdConflicts(String id) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
// on Windows, filenames are case-insensitive
|
||||
for (String existingId : versions.keySet()) {
|
||||
if (existingId.equalsIgnoreCase(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return versions.containsKey(id);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
|
||||
if (auto) {
|
||||
available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
|
||||
if (available <= 0) {
|
||||
return minimum;
|
||||
}
|
||||
|
||||
final long threshold = 8L * 1024 * 1024 * 1024;
|
||||
final long suggested = Math.min(available <= threshold
|
||||
? (long) (available * 0.8)
|
||||
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
|
||||
16384L * 1024 * 1024);
|
||||
return Math.max(minimum, suggested);
|
||||
} else {
|
||||
return minimum;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
|
||||
@@ -33,10 +32,9 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final File zipFile;
|
||||
@@ -44,8 +42,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependency;
|
||||
private final Modpack modpack;
|
||||
private final List<Task<?>> dependencies = new LinkedList<>();
|
||||
private final List<Task<?>> dependents = new LinkedList<>();
|
||||
private final List<Task<?>> dependencies = new ArrayList<>(1);
|
||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||
|
||||
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
|
||||
dependency = profile.getDependency();
|
||||
@@ -68,16 +66,15 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
ModpackConfiguration<Modpack> config = null;
|
||||
try {
|
||||
if (json.exists()) {
|
||||
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
|
||||
}.getType());
|
||||
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
|
||||
|
||||
if (!MODPACK_TYPE.equals(config.getType()))
|
||||
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
|
||||
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
|
||||
}
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/minecraft", it -> !"pack.json".equals(it), config));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,7 +91,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
public void execute() throws Exception {
|
||||
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
|
||||
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion, null);
|
||||
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
|
||||
// reinstall libraries
|
||||
// libraries of Forge and OptiFine should be obtained by installation.
|
||||
@@ -106,14 +103,4 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
|
||||
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getStages() {
|
||||
return Stream.concat(
|
||||
dependents.stream().flatMap(task -> task.getStages().stream()),
|
||||
Stream.of("hmcl.modpack")
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "HMCL";
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class HMCLModpackManager {
|
||||
|
||||
/**
|
||||
* Read the manifest in a HMCL modpack.
|
||||
*
|
||||
* @param file a HMCL modpack file.
|
||||
* @param encoding encoding of modpack zip file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the manifest.json is missing or malformed.
|
||||
* @return the manifest of HMCL modpack.
|
||||
*/
|
||||
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, Modpack.class).setEncoding(encoding);
|
||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
|
||||
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
|
||||
if (game.getJar() == null)
|
||||
if (StringUtils.isBlank(manifest.getVersion()))
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
|
||||
else
|
||||
return manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||
else
|
||||
return manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,16 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
public final class HMCLModpackManifest {
|
||||
import org.jackhuang.hmcl.mod.ModpackManifest;
|
||||
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||
|
||||
public final class HMCLModpackManifest implements ModpackManifest {
|
||||
public static final HMCLModpackManifest INSTANCE = new HMCLModpackManifest();
|
||||
|
||||
private HMCLModpackManifest() {}
|
||||
|
||||
@Override
|
||||
public ModpackProvider getProvider() {
|
||||
return HMCLModpackProvider.INSTANCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class HMCLModpackProvider implements ModpackProvider {
|
||||
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "HMCL";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository)) {
|
||||
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
|
||||
}
|
||||
|
||||
HMCLGameRepository repository = (HMCLGameRepository) dependencyManager.getGameRepository();
|
||||
Profile profile = repository.getProfile();
|
||||
|
||||
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modpack readManifest(ZipFile file, Path path, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
|
||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
|
||||
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
|
||||
if (game.getJar() == null)
|
||||
if (StringUtils.isBlank(manifest.getVersion()))
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
|
||||
else
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||
else
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private static class HMCLModpack extends Modpack {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.ui.versions.ModTranslations;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public abstract class LocalizedRemoteModRepository implements RemoteModRepository {
|
||||
private static final int CONTAIN_CHINESE_WEIGHT = 10;
|
||||
|
||||
private static final int INITIAL_CAPACITY = 16;
|
||||
|
||||
protected abstract RemoteModRepository getBackedRemoteModRepository();
|
||||
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
if (!StringUtils.containsChinese(searchFilter)) {
|
||||
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
}
|
||||
|
||||
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
|
||||
|
||||
int count = 0;
|
||||
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByRepositoryType(getType()).searchMod(searchFilter)) {
|
||||
for (String englishWord : StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName())) {
|
||||
if (englishSearchFiltersSet.contains(englishWord)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
englishSearchFiltersSet.add(englishWord);
|
||||
}
|
||||
|
||||
count++;
|
||||
if (count >= 3) break;
|
||||
}
|
||||
|
||||
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
|
||||
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
|
||||
{
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
|
||||
if (chineseIndex > englishIndex) {
|
||||
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
|
||||
continue;
|
||||
}
|
||||
|
||||
RemoteMod remoteMod = iterator.next();
|
||||
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
|
||||
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
|
||||
searchResultArray[chineseIndex++] = remoteMod;
|
||||
} else {
|
||||
searchResultArray[englishIndex--] = remoteMod;
|
||||
}
|
||||
}
|
||||
totalPages = searchResult.getTotalPages();
|
||||
}
|
||||
|
||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
|
||||
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
|
||||
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) {
|
||||
return Pair.pair(remoteMod, Integer.MAX_VALUE);
|
||||
}
|
||||
String chineseRemoteModName = chineseRemoteMod.getName();
|
||||
if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) {
|
||||
return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length()));
|
||||
}
|
||||
int l = levCalculator.calc(searchFilter, chineseRemoteModName);
|
||||
for (int i = 0; i < searchFilter.length(); i++) {
|
||||
if (chineseRemoteModName.indexOf(searchFilter.charAt(i)) >= 0) {
|
||||
l -= CONTAIN_CHINESE_WEIGHT;
|
||||
}
|
||||
}
|
||||
return Pair.pair(remoteMod, l);
|
||||
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), Arrays.stream(searchResultArray, englishIndex + 1, searchResultArray.length)), totalPages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Category> getCategories() throws IOException {
|
||||
return getBackedRemoteModRepository().getCategories();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
|
||||
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod getModById(String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getModById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
|
||||
return getBackedRemoteModRepository().getModFile(modId, fileId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getRemoteVersionsById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
public final class Log {
|
||||
public static final int DEFAULT_LOG_LINES = 2000;
|
||||
|
||||
public static int getLogLines() {
|
||||
Integer lines = config().getLogLines();
|
||||
return lines != null && lines > 0 ? lines : DEFAULT_LOG_LINES;
|
||||
}
|
||||
|
||||
private final String log;
|
||||
private Log4jLevel level;
|
||||
private boolean selected = false;
|
||||
|
||||
public Log(String log) {
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public Log(String log, Log4jLevel level) {
|
||||
this.log = log;
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public String getLog() {
|
||||
return log;
|
||||
}
|
||||
|
||||
public Log4jLevel getLevel() {
|
||||
Log4jLevel level = this.level;
|
||||
if (level == null) {
|
||||
level = Log4jLevel.guessLevel(log);
|
||||
if (level == null)
|
||||
level = Log4jLevel.INFO;
|
||||
this.level = level;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return log;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class LogExporter {
|
||||
private LogExporter() {
|
||||
}
|
||||
|
||||
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
|
||||
Path runDirectory = gameRepository.getRunDirectory(versionId).toPath();
|
||||
Path baseDirectory = gameRepository.getBaseDirectory().toPath();
|
||||
List<String> versions = new ArrayList<>();
|
||||
|
||||
String currentVersionId = versionId;
|
||||
HashSet<String> resolvedSoFar = new HashSet<>();
|
||||
while (true) {
|
||||
if (resolvedSoFar.contains(currentVersionId)) break;
|
||||
resolvedSoFar.add(currentVersionId);
|
||||
Version currentVersion = gameRepository.getVersion(currentVersionId);
|
||||
versions.add(currentVersionId);
|
||||
|
||||
if (StringUtils.isNotBlank(currentVersion.getInheritsFrom())) {
|
||||
currentVersionId = currentVersion.getInheritsFrom();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Zipper zipper = new Zipper(zipFile)) {
|
||||
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper);
|
||||
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper);
|
||||
processLogs(runDirectory, "*.log", "runDirectory", zipper);
|
||||
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper);
|
||||
|
||||
zipper.putTextFile(LOG.getLogs(), "hmcl.log");
|
||||
zipper.putTextFile(logs, "minecraft.log");
|
||||
zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
|
||||
|
||||
for (String id : versions) {
|
||||
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
|
||||
if (Files.exists(versionJson)) {
|
||||
zipper.putFile(versionJson, id + ".json");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, fileExtension)) {
|
||||
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
|
||||
|
||||
for (Path file : stream) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
|
||||
if (time.toMillis() >= processStartTime) {
|
||||
try {
|
||||
String crashLog = Logger.filterForbiddenToken(FileUtils.readText(file, OperatingSystem.NATIVE_CHARSET));
|
||||
zipper.putTextFile(crashLog, file.getFileName().toString());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read log file: " + file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to find any log on " + logDirectory, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,21 +15,18 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class ReferenceHolder implements InvalidationListener {
|
||||
@SuppressWarnings("unused")
|
||||
private Object ref;
|
||||
public class ManuallyCreatedModpackException extends Exception {
|
||||
private final Path path;
|
||||
|
||||
public ReferenceHolder(Object ref) {
|
||||
this.ref = ref;
|
||||
public ManuallyCreatedModpackException(Path path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
// no-op
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.Unzipper;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||
|
||||
private final Profile profile;
|
||||
private final Path zipFile;
|
||||
private final Charset charset;
|
||||
private final String name;
|
||||
|
||||
public ManuallyCreatedModpackInstallTask(Profile profile, Path zipFile, Charset charset, String name) {
|
||||
this.profile = profile;
|
||||
this.zipFile = zipFile;
|
||||
this.charset = charset;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path subdirectory;
|
||||
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(charset).build()) {
|
||||
subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), fs);
|
||||
}
|
||||
|
||||
Path dest = Paths.get("externalgames").resolve(name);
|
||||
|
||||
setResult(dest);
|
||||
|
||||
new Unzipper(zipFile, dest)
|
||||
.setSubDirectory(subdirectory.toString())
|
||||
.setTerminateIfSubDirectoryNotExists()
|
||||
.setEncoding(charset)
|
||||
.unzip();
|
||||
}
|
||||
}
|
||||
@@ -18,19 +18,19 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseManifest;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackRemoteInstallTask;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -38,78 +38,115 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.toIterable;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public final class ModpackHelper {
|
||||
private ModpackHelper() {}
|
||||
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception ignored) {
|
||||
ignored.printStackTrace();
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
private static final Map<String, ModpackProvider> providers = mapOf(
|
||||
pair(CurseModpackProvider.INSTANCE.getName(), CurseModpackProvider.INSTANCE),
|
||||
pair(McbbsModpackProvider.INSTANCE.getName(), McbbsModpackProvider.INSTANCE),
|
||||
pair(ModrinthModpackProvider.INSTANCE.getName(), ModrinthModpackProvider.INSTANCE),
|
||||
pair(MultiMCModpackProvider.INSTANCE.getName(), MultiMCModpackProvider.INSTANCE),
|
||||
pair(ServerModpackProvider.INSTANCE.getName(), ServerModpackProvider.INSTANCE),
|
||||
pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE)
|
||||
);
|
||||
|
||||
@Nullable
|
||||
public static ModpackProvider getProviderByType(String type) {
|
||||
return providers.get(type);
|
||||
}
|
||||
|
||||
public static boolean isFileModpackByExtension(File file) {
|
||||
String ext = FileUtils.getExtension(file);
|
||||
return "zip".equals(ext) || "mrpack".equals(ext);
|
||||
}
|
||||
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
||||
try (ZipFile zipFile = CompressingUtils.openZipFile(file, charset)) {
|
||||
// Order for trying detecting manifest is necessary here.
|
||||
// Do not change to iterating providers.
|
||||
for (ModpackProvider provider : new ModpackProvider[]{
|
||||
McbbsModpackProvider.INSTANCE,
|
||||
CurseModpackProvider.INSTANCE,
|
||||
ModrinthModpackProvider.INSTANCE,
|
||||
HMCLModpackProvider.INSTANCE,
|
||||
MultiMCModpackProvider.INSTANCE,
|
||||
ServerModpackProvider.INSTANCE}) {
|
||||
try {
|
||||
return provider.readManifest(zipFile, file, charset);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid CurseForge modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return HMCLModpackManager.readHMCLModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid HMCL modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid MultiMC modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return ServerModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid Server modpack.
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), fs);
|
||||
throw new ManuallyCreatedModpackException(file);
|
||||
} catch (IOException e) {
|
||||
// ignore it
|
||||
}
|
||||
|
||||
throw new UnsupportedModpackException(file.toString());
|
||||
}
|
||||
|
||||
public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException {
|
||||
Path root = fs.getPath("/");
|
||||
if (isMinecraftDirectory(root)) return root;
|
||||
try (Stream<Path> firstLayer = Files.list(root)) {
|
||||
for (Path dir : toIterable(firstLayer)) {
|
||||
if (isMinecraftDirectory(dir)) return dir;
|
||||
|
||||
try (Stream<Path> secondLayer = Files.list(dir)) {
|
||||
for (Path subdir : toIterable(secondLayer)) {
|
||||
if (isMinecraftDirectory(subdir)) return subdir;
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
throw new UnsupportedModpackException(modpackName);
|
||||
}
|
||||
|
||||
private static boolean isMinecraftDirectory(Path path) {
|
||||
return Files.isDirectory(path.resolve("versions")) &&
|
||||
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
|
||||
}
|
||||
|
||||
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
|
||||
if (!file.exists())
|
||||
throw new FileNotFoundException(file.getPath());
|
||||
else
|
||||
try {
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
|
||||
}.getType());
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed modpack configuration");
|
||||
}
|
||||
}
|
||||
|
||||
private static String getManifestType(Object manifest) throws UnsupportedModpackException {
|
||||
if (manifest instanceof HMCLModpackManifest)
|
||||
return HMCLModpackInstallTask.MODPACK_TYPE;
|
||||
else if (manifest instanceof MultiMCInstanceConfiguration)
|
||||
return MultiMCModpackInstallTask.MODPACK_TYPE;
|
||||
else if (manifest instanceof CurseManifest)
|
||||
return CurseInstallTask.MODPACK_TYPE;
|
||||
else
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
|
||||
public static Task<Void> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
|
||||
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
@@ -122,17 +159,36 @@ public final class ModpackHelper {
|
||||
};
|
||||
|
||||
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
||||
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
|
||||
if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
|
||||
success.run();
|
||||
// This is tolerable and we will not delete the game
|
||||
}
|
||||
};
|
||||
|
||||
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
public static Task<Void> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
|
||||
public static boolean isExternalGameNameConflicts(String name) {
|
||||
return Files.exists(Paths.get("externalgames").resolve(name));
|
||||
}
|
||||
|
||||
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
|
||||
if (isExternalGameNameConflicts(name)) {
|
||||
throw new IllegalArgumentException("name existing");
|
||||
}
|
||||
|
||||
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
|
||||
.thenAcceptAsync(Schedulers.javafx(), location -> {
|
||||
Profile newProfile = new Profile(name, location.toFile());
|
||||
newProfile.setUseRelativePath(true);
|
||||
Profiles.getProfiles().add(newProfile);
|
||||
Profiles.setSelectedProfile(newProfile);
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
@@ -145,73 +201,42 @@ public final class ModpackHelper {
|
||||
};
|
||||
|
||||
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
||||
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
|
||||
if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
|
||||
success.run();
|
||||
// This is tolerable and we will not delete the game
|
||||
}
|
||||
};
|
||||
|
||||
if (modpack.getManifest() instanceof CurseManifest)
|
||||
return new CurseInstallTask(profile.getDependency(), zipFile, modpack, ((CurseManifest) modpack.getManifest()), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof HMCLModpackManifest)
|
||||
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
else if (modpack.getManifest() instanceof ServerModpackManifest)
|
||||
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
|
||||
else
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.javafx(), success, failure);
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
switch (configuration.getType()) {
|
||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name));
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
|
||||
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
|
||||
public static Task<?> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
|
||||
|
||||
switch (configuration.getType()) {
|
||||
case CurseInstallTask.MODPACK_TYPE:
|
||||
if (!(modpack.getManifest() instanceof CurseManifest))
|
||||
throw new MismatchedModpackTypeException(CurseInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new CurseInstallTask(profile.getDependency(), zipFile, modpack, (CurseManifest) modpack.getManifest(), name));
|
||||
case MultiMCModpackInstallTask.MODPACK_TYPE:
|
||||
if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration))
|
||||
throw new MismatchedModpackTypeException(MultiMCModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
case HMCLModpackInstallTask.MODPACK_TYPE:
|
||||
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
||||
throw new MismatchedModpackTypeException(HMCLModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
|
||||
case McbbsModpackLocalInstallTask.MODPACK_TYPE:
|
||||
if (!(modpack.getManifest() instanceof McbbsModpackManifest))
|
||||
throw new MismatchedModpackTypeException(McbbsModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
case ServerModpackLocalInstallTask.MODPACK_TYPE:
|
||||
if (!(modpack.getManifest() instanceof ServerModpackManifest))
|
||||
throw new MismatchedModpackTypeException(ServerModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name));
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
ModpackProvider provider = getProviderByType(configuration.getType());
|
||||
if (provider == null) {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
|
||||
}
|
||||
|
||||
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
private final int port;
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
|
||||
public static String lastlyOpenedURL;
|
||||
|
||||
private String idToken;
|
||||
|
||||
private OAuthServer(int port) {
|
||||
super(port);
|
||||
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRedirectURI() {
|
||||
return String.format("http://localhost:%d/auth-response", port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String waitFor() throws InterruptedException, ExecutionException {
|
||||
return future.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdToken() {
|
||||
return idToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
if (!"/auth-response".equals(session.getUri())) {
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
|
||||
}
|
||||
|
||||
if (session.getMethod() == Method.POST) {
|
||||
Map<String, String> files = new HashMap<>();
|
||||
try {
|
||||
session.parseBody(files);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read post data", e);
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||
} catch (ResponseException re) {
|
||||
return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
|
||||
}
|
||||
} else if (session.getMethod() == Method.GET) {
|
||||
// do nothing
|
||||
} else {
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
|
||||
}
|
||||
String parameters = session.getQueryParameterString();
|
||||
|
||||
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
|
||||
if (query.containsKey("code")) {
|
||||
idToken = query.get("id_token");
|
||||
future.complete(query.get("code"));
|
||||
} else {
|
||||
LOG.warning("Error: " + parameters);
|
||||
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
|
||||
}
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load html", e);
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||
}
|
||||
thread(() -> {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
stop();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("Failed to sleep for 1 second");
|
||||
}
|
||||
});
|
||||
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
|
||||
}
|
||||
|
||||
public static class Factory implements OAuth.Callback {
|
||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
|
||||
|
||||
@Override
|
||||
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
||||
if (StringUtils.isBlank(getClientId())) {
|
||||
throw new MicrosoftAuthenticationNotSupportedException();
|
||||
}
|
||||
|
||||
IOException exception = null;
|
||||
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
|
||||
try {
|
||||
OAuthServer server = new OAuthServer(port);
|
||||
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
|
||||
return server;
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
}
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void grantDeviceCode(String userCode, String verificationURI) {
|
||||
onGrantDeviceCode.fireEvent(new GrantDeviceCodeEvent(this, userCode, verificationURI));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openBrowser(String url) throws IOException {
|
||||
lastlyOpenedURL = url;
|
||||
FXUtils.openLink(url);
|
||||
|
||||
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientId() {
|
||||
return System.getProperty("hmcl.microsoft.auth.id",
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPublicClient() {
|
||||
return true; // We have turned on the device auth flow.
|
||||
}
|
||||
}
|
||||
|
||||
public static class GrantDeviceCodeEvent extends Event {
|
||||
private final String userCode;
|
||||
private final String verificationUri;
|
||||
|
||||
public GrantDeviceCodeEvent(Object source, String userCode, String verificationUri) {
|
||||
super(source);
|
||||
this.userCode = userCode;
|
||||
this.verificationUri = verificationUri;
|
||||
}
|
||||
|
||||
public String getUserCode() {
|
||||
return userCode;
|
||||
}
|
||||
|
||||
public String getVerificationUri() {
|
||||
return verificationUri;
|
||||
}
|
||||
}
|
||||
|
||||
public static class OpenBrowserEvent extends Event {
|
||||
private final String url;
|
||||
|
||||
public OpenBrowserEvent(Object source, String url) {
|
||||
super(source);
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MicrosoftAuthenticationNotSupportedException extends AuthenticationException {
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,31 +17,34 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -49,13 +52,12 @@ import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
@@ -67,15 +69,15 @@ public final class TexturesLoader {
|
||||
|
||||
// ==== Texture Loading ====
|
||||
public static class LoadedTexture {
|
||||
private final BufferedImage image;
|
||||
private final Image image;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
|
||||
public LoadedTexture(Image image, Map<String, String> metadata) {
|
||||
this.image = requireNonNull(image);
|
||||
this.metadata = requireNonNull(metadata);
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
public Image getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -99,7 +101,7 @@ public final class TexturesLoader {
|
||||
return TEXTURES_DIR.resolve(prefix).resolve(hash);
|
||||
}
|
||||
|
||||
public static LoadedTexture loadTexture(Texture texture) throws IOException {
|
||||
public static LoadedTexture loadTexture(Texture texture) throws Throwable {
|
||||
if (StringUtils.isBlank(texture.getUrl())) {
|
||||
throw new IOException("Texture url is empty");
|
||||
}
|
||||
@@ -113,19 +115,20 @@ public final class TexturesLoader {
|
||||
} catch (Exception e) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
// concurrency conflict?
|
||||
LOG.log(Level.WARNING, "Failed to download texture " + texture.getUrl() + ", but the file is available", e);
|
||||
LOG.warning("Failed to download texture " + texture.getUrl() + ", but the file is available", e);
|
||||
} else {
|
||||
throw new IOException("Failed to download texture " + texture.getUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BufferedImage img;
|
||||
Image img;
|
||||
try (InputStream in = Files.newInputStream(file)) {
|
||||
img = ImageIO.read(in);
|
||||
img = new Image(in);
|
||||
}
|
||||
if (img == null)
|
||||
throw new IOException("Texture is malformed");
|
||||
|
||||
if (img.isError())
|
||||
throw img.getException();
|
||||
|
||||
Map<String, String> metadata = texture.getMetadata();
|
||||
if (metadata == null) {
|
||||
@@ -136,34 +139,42 @@ public final class TexturesLoader {
|
||||
// ====
|
||||
|
||||
// ==== Skins ====
|
||||
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
|
||||
private static final String[] DEFAULT_SKINS = {"alex", "ari", "efe", "kai", "makena", "noor", "steve", "sunny", "zuri"};
|
||||
|
||||
static {
|
||||
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
|
||||
public static Image getDefaultSkinImage() {
|
||||
return FXUtils.newBuiltinImage("/assets/img/skin/wide/steve.png");
|
||||
}
|
||||
|
||||
private static void loadDefaultSkin(String path, TextureModel model) {
|
||||
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
|
||||
} catch (Throwable e) {
|
||||
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
|
||||
public static LoadedTexture getDefaultSkin(UUID uuid) {
|
||||
int idx = Math.floorMod(uuid.hashCode(), DEFAULT_SKINS.length * 2);
|
||||
TextureModel model;
|
||||
Image skin;
|
||||
if (idx < DEFAULT_SKINS.length) {
|
||||
model = TextureModel.SLIM;
|
||||
skin = FXUtils.newBuiltinImage("/assets/img/skin/slim/" + DEFAULT_SKINS[idx] + ".png");
|
||||
} else {
|
||||
model = TextureModel.WIDE;
|
||||
skin = FXUtils.newBuiltinImage("/assets/img/skin/wide/" + DEFAULT_SKINS[idx - DEFAULT_SKINS.length] + ".png");
|
||||
}
|
||||
|
||||
return new LoadedTexture(skin, singletonMap("model", model.modelName));
|
||||
}
|
||||
|
||||
public static LoadedTexture getDefaultSkin(TextureModel model) {
|
||||
return DEFAULT_SKINS.get(model);
|
||||
public static TextureModel getDefaultModel(UUID uuid) {
|
||||
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).getMetadata().get("model"))
|
||||
? TextureModel.WIDE
|
||||
: TextureModel.SLIM;
|
||||
}
|
||||
|
||||
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
|
||||
LoadedTexture uuidFallback = getDefaultSkin(uuid);
|
||||
return BindingMapping.of(service.getProfileRepository().binding(uuid))
|
||||
.map(profile -> profile
|
||||
.flatMap(it -> {
|
||||
try {
|
||||
return YggdrasilService.getTextures(it);
|
||||
} catch (ServerResponseMalformedException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse texture payload", e);
|
||||
LOG.warning("Failed to parse texture payload", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
})
|
||||
@@ -175,8 +186,8 @@ public final class TexturesLoader {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
}, POOL);
|
||||
@@ -186,81 +197,178 @@ public final class TexturesLoader {
|
||||
}, uuidFallback);
|
||||
}
|
||||
|
||||
public static ObjectBinding<LoadedTexture> skinBinding(Account account) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
|
||||
return BindingMapping.of(account.getTextures())
|
||||
.map(textures -> textures
|
||||
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
|
||||
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
|
||||
.asyncMap(it -> {
|
||||
if (it.isPresent()) {
|
||||
Texture texture = it.get();
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
public static ObservableValue<LoadedTexture> skinBinding(Account account) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(account.getUUID());
|
||||
if (account instanceof OfflineAccount) {
|
||||
OfflineAccount offlineAccount = (OfflineAccount) account;
|
||||
|
||||
SimpleObjectProperty<LoadedTexture> binding = new SimpleObjectProperty<>();
|
||||
InvalidationListener listener = o -> {
|
||||
Skin skin = offlineAccount.getSkin();
|
||||
String username = offlineAccount.getUsername();
|
||||
|
||||
binding.set(uuidFallback);
|
||||
if (skin != null) {
|
||||
skin.load(username).setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to load texture", exception);
|
||||
} else if (result != null && result.getSkin() != null && result.getSkin().getImage() != null) {
|
||||
Map<String, String> metadata;
|
||||
if (result.getModel() != null) {
|
||||
metadata = singletonMap("model", result.getModel().modelName);
|
||||
} else {
|
||||
metadata = emptyMap();
|
||||
}
|
||||
}, POOL);
|
||||
} else {
|
||||
|
||||
binding.set(new LoadedTexture(result.getSkin().getImage(), metadata));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
};
|
||||
|
||||
listener.invalidated(offlineAccount);
|
||||
|
||||
binding.addListener(new Holder<>(listener));
|
||||
offlineAccount.addListener(new WeakInvalidationListener(listener));
|
||||
|
||||
return binding;
|
||||
} else {
|
||||
return BindingMapping.of(account.getTextures())
|
||||
.asyncMap(textures -> {
|
||||
if (textures.isPresent()) {
|
||||
Texture texture = textures.get().get(TextureType.SKIN);
|
||||
if (texture != null && StringUtils.isNotBlank(texture.getUrl())) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
}, POOL);
|
||||
}
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(uuidFallback);
|
||||
}
|
||||
}, uuidFallback);
|
||||
}, uuidFallback);
|
||||
}
|
||||
}
|
||||
|
||||
// ====
|
||||
|
||||
// ==== Avatar ====
|
||||
public static BufferedImage toAvatar(BufferedImage skin, int size) {
|
||||
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = avatar.createGraphics();
|
||||
public static void drawAvatar(Canvas canvas, Image skin) {
|
||||
GraphicsContext g = canvas.getGraphicsContext2D();
|
||||
g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
|
||||
int scale = skin.getWidth() / 64;
|
||||
int size = (int) canvas.getWidth();
|
||||
int scale = (int) skin.getWidth() / 64;
|
||||
int faceOffset = (int) Math.round(size / 18.0);
|
||||
g.drawImage(skin,
|
||||
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||
8 * scale, 8 * scale, 16 * scale, 16 * scale,
|
||||
null);
|
||||
g.drawImage(skin,
|
||||
0, 0, size, size,
|
||||
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
||||
|
||||
g.dispose();
|
||||
return avatar;
|
||||
try {
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatar(g, skin, size, scale, faceOffset);
|
||||
} catch (NoSuchMethodError ignored) {
|
||||
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing
|
||||
// In order to prevent the blurring caused by bilinear interpolation,
|
||||
// we use the self-implemented nearest neighbor interpolation to scale
|
||||
drawAvatarSlow(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
|
||||
return BindingMapping.of(skinBinding(service, uuid))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(TexturesLoader::toFXImage);
|
||||
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
g.drawImage(skin,
|
||||
8 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
|
||||
g.drawImage(skin,
|
||||
40 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
|
||||
return BindingMapping.of(skinBinding(account))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(TexturesLoader::toFXImage);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
|
||||
private static void drawAvatarSlow(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
PixelReader reader = skin.getPixelReader();
|
||||
PixelWriter writer = g.getPixelWriter();
|
||||
drawImage(writer, reader,
|
||||
8 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
|
||||
drawImage(writer, reader,
|
||||
40 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
/*
|
||||
* Scale and draw image using nearest-neighbor interpolation
|
||||
*/
|
||||
private static void drawImage(PixelWriter writer, PixelReader reader,
|
||||
int sx, int sy, int sw, int sh,
|
||||
int dx, int dy, int dw, int dh) {
|
||||
double xScale = ((double) sw) / dw;
|
||||
double yScale = ((double) sh) / dh;
|
||||
|
||||
for (int xOffset = 0; xOffset < dw; xOffset++) {
|
||||
for (int yOffset = 0; yOffset < dh; yOffset++) {
|
||||
int color = reader.getArgb(sx + (int) (xOffset * xScale), sy + (int) (yOffset * yScale));
|
||||
|
||||
// Draw only non-transparent pixels
|
||||
if ((color >>> 24) != 0)
|
||||
writer.setArgb(dx + xOffset, dy + yOffset, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
|
||||
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
|
||||
|
||||
final WeakReference<Canvas> canvasRef;
|
||||
final ObservableValue<LoadedTexture> binding;
|
||||
|
||||
SkinBindingChangeListener(Canvas canvas, ObservableValue<LoadedTexture> binding) {
|
||||
this.canvasRef = new WeakReference<>(canvas);
|
||||
this.binding = binding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends LoadedTexture> observable,
|
||||
LoadedTexture oldValue, LoadedTexture loadedTexture) {
|
||||
Canvas canvas = canvasRef.get();
|
||||
if (canvas != null)
|
||||
drawAvatar(canvas, loadedTexture.image);
|
||||
}
|
||||
}
|
||||
|
||||
public static void fxAvatarBinding(Canvas canvas, ObservableValue<LoadedTexture> skinBinding) {
|
||||
synchronized (SkinBindingChangeListener.hole) {
|
||||
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||
if (oldListener != null)
|
||||
oldListener.binding.removeListener(oldListener);
|
||||
|
||||
SkinBindingChangeListener listener = new SkinBindingChangeListener(canvas, skinBinding);
|
||||
listener.changed(skinBinding, null, skinBinding.getValue());
|
||||
skinBinding.addListener(listener);
|
||||
|
||||
SkinBindingChangeListener.hole.put(canvas, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindAvatar(Canvas canvas, YggdrasilService service, UUID uuid) {
|
||||
fxAvatarBinding(canvas, skinBinding(service, uuid));
|
||||
}
|
||||
|
||||
public static void bindAvatar(Canvas canvas, Account account) {
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount || account instanceof OfflineAccount)
|
||||
fxAvatarBinding(canvas, skinBinding(account));
|
||||
else {
|
||||
unbindAvatar(canvas);
|
||||
drawAvatar(canvas, getDefaultSkin(account.getUUID()).image);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unbindAvatar(Canvas canvas) {
|
||||
synchronized (SkinBindingChangeListener.hole) {
|
||||
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||
if (oldListener != null)
|
||||
oldListener.binding.removeListener(oldListener);
|
||||
}
|
||||
}
|
||||
// ====
|
||||
|
||||
// Based on https://stackoverflow.com/a/57552025
|
||||
// Fix #874: Use it instead of SwingFXUtils.toFXImage
|
||||
private static WritableImage toFXImage(BufferedImage image) {
|
||||
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
|
||||
PixelWriter pw = wr.getPixelWriter();
|
||||
|
||||
final int iw = image.getWidth();
|
||||
final int ih = image.getHeight();
|
||||
for (int x = 0; x < iw; x++) {
|
||||
for (int y = 0; y < ih; y++) {
|
||||
pw.setArgb(x, y, image.getRGB(x, y));
|
||||
}
|
||||
}
|
||||
return wr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
|
||||
import org.jackhuang.hmcl.download.java.mojang.MojangJavaRemoteFiles;
|
||||
import org.jackhuang.hmcl.game.DownloadInfo;
|
||||
import org.jackhuang.hmcl.game.GameJavaVersion;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class HMCLJavaRepository implements JavaRepository {
|
||||
public static final String MOJANG_JAVA_PREFIX = "mojang-";
|
||||
|
||||
private final Path root;
|
||||
|
||||
public HMCLJavaRepository(Path root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public Path getPlatformRoot(Platform platform) {
|
||||
return root.resolve(platform.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getJavaDir(Platform platform, String name) {
|
||||
return getPlatformRoot(platform).resolve(name);
|
||||
}
|
||||
|
||||
public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getManifestFile(Platform platform, String name) {
|
||||
return getPlatformRoot(platform).resolve(name + ".json");
|
||||
}
|
||||
|
||||
public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
}
|
||||
|
||||
public boolean isInstalled(Platform platform, String name) {
|
||||
return Files.exists(getManifestFile(platform, name));
|
||||
}
|
||||
|
||||
public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
}
|
||||
|
||||
public @Nullable Path getJavaExecutable(Platform platform, String name) {
|
||||
Path javaDir = getJavaDir(platform, name);
|
||||
try {
|
||||
return JavaManager.getExecutable(javaDir).toRealPath();
|
||||
} catch (IOException ignored) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.OSX) {
|
||||
try {
|
||||
return JavaManager.getMacExecutable(javaDir).toRealPath();
|
||||
} catch (IOException ignored1) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<JavaRuntime> getAllJava(Platform platform) {
|
||||
Path root = getPlatformRoot(platform);
|
||||
if (!Files.isDirectory(root))
|
||||
return Collections.emptyList();
|
||||
|
||||
ArrayList<JavaRuntime> list = new ArrayList<>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(root)) {
|
||||
for (Path file : stream) {
|
||||
try {
|
||||
String name = file.getFileName().toString();
|
||||
if (name.endsWith(".json") && Files.isRegularFile(file)) {
|
||||
Path javaDir = file.resolveSibling(name.substring(0, name.length() - ".json".length()));
|
||||
Path executable;
|
||||
try {
|
||||
executable = JavaManager.getExecutable(javaDir).toRealPath();
|
||||
} catch (IOException e) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.OSX)
|
||||
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (Files.isDirectory(javaDir)) {
|
||||
JavaManifest manifest;
|
||||
try (InputStream input = Files.newInputStream(file)) {
|
||||
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
|
||||
}
|
||||
|
||||
list.add(JavaRuntime.of(executable, manifest.getInfo(), true));
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to parse " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
Path javaDir = getJavaDir(platform, gameJavaVersion);
|
||||
|
||||
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
|
||||
Path executable;
|
||||
try {
|
||||
executable = JavaManager.getExecutable(javaDir).toRealPath();
|
||||
} catch (IOException e) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.OSX)
|
||||
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
|
||||
JavaInfo info;
|
||||
if (JavaManager.isCompatible(platform))
|
||||
info = JavaInfo.fromExecutable(executable, false);
|
||||
else
|
||||
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
|
||||
|
||||
Map<String, Object> update = new LinkedHashMap<>();
|
||||
update.put("provider", "mojang");
|
||||
update.put("component", gameJavaVersion.getComponent());
|
||||
|
||||
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
|
||||
result.remoteFiles.getFiles().forEach((path, file) -> {
|
||||
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
|
||||
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
|
||||
if (downloadInfo != null) {
|
||||
files.put(path, new JavaLocalFiles.LocalFile(downloadInfo.getSha1(), downloadInfo.getSize()));
|
||||
}
|
||||
} else if (file instanceof MojangJavaRemoteFiles.RemoteDirectory) {
|
||||
files.put(path, new JavaLocalFiles.LocalDirectory());
|
||||
} else if (file instanceof MojangJavaRemoteFiles.RemoteLink) {
|
||||
files.put(path, new JavaLocalFiles.LocalLink(((MojangJavaRemoteFiles.RemoteLink) file).getTarget()));
|
||||
}
|
||||
});
|
||||
|
||||
JavaManifest manifest = new JavaManifest(info, update, files);
|
||||
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
|
||||
return JavaRuntime.of(executable, info, true);
|
||||
});
|
||||
}
|
||||
|
||||
public Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
|
||||
Path javaDir = getJavaDir(platform, name);
|
||||
return new JavaInstallTask(javaDir, update, archiveFile).thenApplyAsync(result -> {
|
||||
if (!result.getInfo().getPlatform().equals(platform))
|
||||
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
|
||||
|
||||
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
|
||||
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
|
||||
return JavaRuntime.of(executable, result.getInfo(), true);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<Void> getUninstallJavaTask(Platform platform, String name) {
|
||||
return Task.runAsync(() -> {
|
||||
Files.deleteIfExists(getManifestFile(platform, name));
|
||||
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<Void> getUninstallJavaTask(JavaRuntime java) {
|
||||
return Task.runAsync(() -> {
|
||||
Path root = getPlatformRoot(java.getPlatform());
|
||||
Path relativized = root.relativize(java.getBinary());
|
||||
|
||||
if (relativized.getNameCount() > 1) {
|
||||
String name = relativized.getName(0).toString();
|
||||
Files.deleteIfExists(getManifestFile(java.getPlatform(), name));
|
||||
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Hex;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class JavaInstallTask extends Task<JavaManifest> {
|
||||
|
||||
private final Path targetDir;
|
||||
private final Map<String, Object> update;
|
||||
private final Path archiveFile;
|
||||
|
||||
private final Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
|
||||
private final ArrayList<String> nameStack = new ArrayList<>();
|
||||
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
private final MessageDigest messageDigest = DigestUtils.getDigest("SHA-1");
|
||||
|
||||
public JavaInstallTask(Path targetDir, Map<String, Object> update, Path archiveFile) {
|
||||
this.targetDir = targetDir;
|
||||
this.update = update;
|
||||
this.archiveFile = archiveFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
JavaInfo info;
|
||||
|
||||
try (ArchiveFileTree<?, ?> tree = ArchiveFileTree.open(archiveFile)) {
|
||||
info = JavaInfo.fromArchive(tree);
|
||||
copyDirContent(tree, targetDir);
|
||||
}
|
||||
|
||||
setResult(new JavaManifest(info, update, files));
|
||||
}
|
||||
|
||||
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, Path targetDir) throws IOException {
|
||||
copyDirContent(tree, tree.getRoot().getSubDirs().values().iterator().next(), targetDir);
|
||||
}
|
||||
|
||||
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, ArchiveFileTree.Dir<E> dir, Path targetDir) throws IOException {
|
||||
Files.createDirectories(targetDir);
|
||||
|
||||
for (Map.Entry<String, E> pair : dir.getFiles().entrySet()) {
|
||||
Path path = targetDir.resolve(pair.getKey());
|
||||
E entry = pair.getValue();
|
||||
|
||||
nameStack.add(pair.getKey());
|
||||
if (tree.isLink(entry)) {
|
||||
String linkTarget = tree.getLink(entry);
|
||||
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalLink(linkTarget));
|
||||
Files.createSymbolicLink(path, Paths.get(linkTarget));
|
||||
} else {
|
||||
long size = 0L;
|
||||
|
||||
try (InputStream input = tree.getInputStream(entry);
|
||||
OutputStream output = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
messageDigest.reset();
|
||||
|
||||
int c;
|
||||
while ((c = input.read(buffer)) > 0) {
|
||||
size += c;
|
||||
output.write(buffer, 0, c);
|
||||
messageDigest.update(buffer, 0, c);
|
||||
}
|
||||
}
|
||||
|
||||
if (tree.isExecutable(entry))
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
path.toFile().setExecutable(true);
|
||||
|
||||
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
|
||||
}
|
||||
nameStack.remove(nameStack.size() - 1);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, ArchiveFileTree.Dir<E>> pair : dir.getSubDirs().entrySet()) {
|
||||
nameStack.add(pair.getKey());
|
||||
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalDirectory());
|
||||
copyDirContent(tree, pair.getValue(), targetDir.resolve(pair.getKey()));
|
||||
nameStack.remove(nameStack.size() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class JavaLocalFiles {
|
||||
@JsonAdapter(Serializer.class)
|
||||
public abstract static class Local {
|
||||
private final String type;
|
||||
|
||||
Local(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class LocalFile extends Local {
|
||||
private final String sha1;
|
||||
private final long size;
|
||||
|
||||
public LocalFile(String sha1, long size) {
|
||||
super("file");
|
||||
this.sha1 = sha1;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getSha1() {
|
||||
return sha1;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class LocalDirectory extends Local {
|
||||
public LocalDirectory() {
|
||||
super("directory");
|
||||
}
|
||||
}
|
||||
|
||||
public static final class LocalLink extends Local {
|
||||
private final String target;
|
||||
|
||||
public LocalLink(String target) {
|
||||
super("link");
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<Local>, JsonDeserializer<Local> {
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Local src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject obj = new JsonObject();
|
||||
obj.addProperty("type", src.getType());
|
||||
if (src instanceof LocalFile) {
|
||||
obj.addProperty("sha1", ((LocalFile) src).getSha1());
|
||||
obj.addProperty("size", ((LocalFile) src).getSize());
|
||||
} else if (src instanceof LocalLink) {
|
||||
obj.addProperty("target", ((LocalLink) src).getTarget());
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Local deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!json.isJsonObject())
|
||||
throw new JsonParseException(json.toString());
|
||||
|
||||
JsonObject obj = json.getAsJsonObject();
|
||||
if (!obj.has("type"))
|
||||
throw new JsonParseException(json.toString());
|
||||
|
||||
String type = obj.getAsJsonPrimitive("type").getAsString();
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case "file": {
|
||||
String sha1 = obj.getAsJsonPrimitive("sha1").getAsString();
|
||||
long size = obj.getAsJsonPrimitive("size").getAsLong();
|
||||
return new LocalFile(sha1, size);
|
||||
}
|
||||
case "directory": {
|
||||
return new LocalDirectory();
|
||||
}
|
||||
case "link": {
|
||||
String target = obj.getAsJsonPrimitive("target").getAsString();
|
||||
return new LocalLink(target);
|
||||
}
|
||||
default:
|
||||
throw new AssertionError("unknown type: " + type);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new JsonParseException(json.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,698 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameJavaVersion;
|
||||
import org.jackhuang.hmcl.game.JavaVersionConstraint;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class JavaManager {
|
||||
|
||||
private JavaManager() {
|
||||
}
|
||||
|
||||
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_DIRECTORY.resolve("java"));
|
||||
|
||||
public static String getMojangJavaPlatform(Platform platform) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
|
||||
return "windows-x86";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "windows-x64";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
return "windows-arm64";
|
||||
}
|
||||
} else if (platform.getOperatingSystem() == OperatingSystem.LINUX) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
|
||||
return "linux-i386";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "linux";
|
||||
}
|
||||
} else if (platform.getOperatingSystem() == OperatingSystem.OSX) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "mac-os";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
return "mac-os-arm64";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Path getExecutable(Path javaHome) {
|
||||
return javaHome.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable());
|
||||
}
|
||||
|
||||
public static Path getMacExecutable(Path javaHome) {
|
||||
return javaHome.resolve("jre.bundle/Contents/Home/bin/java");
|
||||
}
|
||||
|
||||
public static boolean isCompatible(Platform platform) {
|
||||
if (platform.getOperatingSystem() != OperatingSystem.CURRENT_OS)
|
||||
return false;
|
||||
|
||||
Architecture architecture = platform.getArchitecture();
|
||||
if (architecture == Architecture.SYSTEM_ARCH || architecture == Architecture.CURRENT_ARCH)
|
||||
return true;
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
return architecture == Architecture.X86;
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
|
||||
break;
|
||||
case LINUX:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
return architecture == Architecture.X86;
|
||||
break;
|
||||
case OSX:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
return architecture == Architecture.X86_64;
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static volatile Map<Path, JavaRuntime> allJava;
|
||||
private static final CountDownLatch LATCH = new CountDownLatch(1);
|
||||
|
||||
private static final ObjectProperty<Collection<JavaRuntime>> allJavaProperty = new SimpleObjectProperty<>();
|
||||
|
||||
private static Map<Path, JavaRuntime> getAllJavaMap() throws InterruptedException {
|
||||
Map<Path, JavaRuntime> map = allJava;
|
||||
if (map == null) {
|
||||
LATCH.await();
|
||||
map = allJava;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void updateAllJavaProperty(Map<Path, JavaRuntime> javaRuntimes) {
|
||||
JavaRuntime[] array = javaRuntimes.values().toArray(new JavaRuntime[0]);
|
||||
Arrays.sort(array);
|
||||
allJavaProperty.set(Arrays.asList(array));
|
||||
}
|
||||
|
||||
public static boolean isInitialized() {
|
||||
return allJava != null;
|
||||
}
|
||||
|
||||
public static Collection<JavaRuntime> getAllJava() throws InterruptedException {
|
||||
return getAllJavaMap().values();
|
||||
}
|
||||
|
||||
public static ObjectProperty<Collection<JavaRuntime>> getAllJavaProperty() {
|
||||
return allJavaProperty;
|
||||
}
|
||||
|
||||
public static JavaRuntime getJava(Path executable) throws IOException, InterruptedException {
|
||||
executable = executable.toRealPath();
|
||||
|
||||
JavaRuntime javaRuntime = getAllJavaMap().get(executable);
|
||||
if (javaRuntime != null) {
|
||||
return javaRuntime;
|
||||
}
|
||||
|
||||
JavaInfo info = JavaInfo.fromExecutable(executable);
|
||||
return JavaRuntime.of(executable, info, false);
|
||||
}
|
||||
|
||||
public static void refresh() {
|
||||
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (result != null) {
|
||||
LATCH.await();
|
||||
allJava = result;
|
||||
updateAllJavaProperty(result);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public static Task<JavaRuntime> getAddJavaTask(Path binary) {
|
||||
return Task.supplyAsync("Get Java", () -> JavaManager.getJava(binary))
|
||||
.thenApplyAsync(Schedulers.javafx(), javaRuntime -> {
|
||||
if (!JavaManager.isCompatible(javaRuntime.getPlatform())) {
|
||||
throw new UnsupportedPlatformException("Incompatible platform: " + javaRuntime.getPlatform());
|
||||
}
|
||||
|
||||
String pathString = javaRuntime.getBinary().toString();
|
||||
|
||||
ConfigHolder.globalConfig().getDisabledJava().remove(pathString);
|
||||
if (ConfigHolder.globalConfig().getUserJava().add(pathString)) {
|
||||
addJava(javaRuntime);
|
||||
}
|
||||
return javaRuntime;
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
return REPOSITORY.getDownloadJavaTask(downloadProvider, platform, gameJavaVersion)
|
||||
.thenApplyAsync(Schedulers.javafx(), java -> {
|
||||
addJava(java);
|
||||
return java;
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
|
||||
return REPOSITORY.getInstallJavaTask(platform, name, update, archiveFile)
|
||||
.thenApplyAsync(Schedulers.javafx(), java -> {
|
||||
addJava(java);
|
||||
return java;
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<Void> getUninstallJavaTask(JavaRuntime java) {
|
||||
assert java.isManaged();
|
||||
Path root = REPOSITORY.getPlatformRoot(java.getPlatform());
|
||||
Path relativized = root.relativize(java.getBinary());
|
||||
|
||||
if (relativized.getNameCount() > 1) {
|
||||
FXUtils.runInFX(() -> {
|
||||
try {
|
||||
removeJava(java);
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError("Unreachable code", e);
|
||||
}
|
||||
});
|
||||
|
||||
String name = relativized.getName(0).toString();
|
||||
return REPOSITORY.getUninstallJavaTask(java.getPlatform(), name);
|
||||
} else {
|
||||
return Task.completed(null);
|
||||
}
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static void addJava(JavaRuntime java) throws InterruptedException {
|
||||
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
|
||||
if (!oldMap.containsKey(java.getBinary())) {
|
||||
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
|
||||
newMap.put(java.getBinary(), java);
|
||||
allJava = newMap;
|
||||
updateAllJavaProperty(newMap);
|
||||
}
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static void removeJava(JavaRuntime java) throws InterruptedException {
|
||||
removeJava(java.getBinary());
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static void removeJava(Path realPath) throws InterruptedException {
|
||||
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
|
||||
if (oldMap.containsKey(realPath)) {
|
||||
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
|
||||
newMap.remove(realPath);
|
||||
allJava = newMap;
|
||||
updateAllJavaProperty(newMap);
|
||||
}
|
||||
}
|
||||
|
||||
private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) {
|
||||
if (suggestedJavaVersion != null) {
|
||||
boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
|
||||
boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
|
||||
|
||||
if (b1 != b2)
|
||||
return b1 ? 1 : -1;
|
||||
}
|
||||
|
||||
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
|
||||
return findSuitableJava(getAllJava(), gameVersion, version);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static JavaRuntime findSuitableJava(Collection<JavaRuntime> javaRuntimes, GameVersionNumber gameVersion, Version version) {
|
||||
LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null;
|
||||
|
||||
boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
|
||||
|
||||
GameJavaVersion suggestedJavaVersion =
|
||||
(version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null;
|
||||
|
||||
JavaRuntime mandatory = null;
|
||||
JavaRuntime suggested = null;
|
||||
for (JavaRuntime java : javaRuntimes) {
|
||||
if (forceX86) {
|
||||
if (!java.getArchitecture().isX86())
|
||||
continue;
|
||||
} else {
|
||||
if (java.getArchitecture() != Architecture.SYSTEM_ARCH)
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean violationMandatory = false;
|
||||
boolean violationSuggested = false;
|
||||
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java)) {
|
||||
if (constraint.isMandatory()) {
|
||||
violationMandatory = true;
|
||||
} else {
|
||||
violationSuggested = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!violationMandatory) {
|
||||
if (mandatory == null) mandatory = java;
|
||||
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
|
||||
mandatory = java;
|
||||
|
||||
if (!violationSuggested) {
|
||||
if (suggested == null) suggested = java;
|
||||
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
|
||||
suggested = java;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggested != null ? suggested : mandatory;
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
|
||||
JavaManager.allJava = allJava;
|
||||
LATCH.countDown();
|
||||
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
|
||||
}
|
||||
|
||||
// search java
|
||||
|
||||
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
|
||||
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64);
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
|
||||
}
|
||||
break;
|
||||
case OSX:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.OSX_X86_64);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\");
|
||||
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\");
|
||||
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\");
|
||||
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\");
|
||||
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(ARM)", "C:\\Program Files (ARM)");
|
||||
}
|
||||
break;
|
||||
case LINUX:
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/java")); // Oracle RPMs
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib32/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
|
||||
break;
|
||||
case OSX:
|
||||
tryAddJavaHome(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines/Contents/Home"));
|
||||
tryAddJavaHome(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines/Contents/Home"));
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
|
||||
// Homebrew
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get("/opt/homebrew/opt/java/bin/java"));
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/opt/homebrew/Cellar/openjdk"));
|
||||
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Paths.get("/opt/homebrew/Cellar"), "openjdk@*")) {
|
||||
for (Path dir : dirs) {
|
||||
searchAllJavaInDirectory(javaRuntimes, dir);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Search Minecraft bundled runtimes
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH.isX86()) {
|
||||
FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime")
|
||||
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
|
||||
|
||||
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
|
||||
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), ".minecraft/runtime"), false);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
|
||||
}
|
||||
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
|
||||
|
||||
// Search in PATH.
|
||||
if (System.getenv("PATH") != null) {
|
||||
String[] paths = System.getenv("PATH").split(OperatingSystem.PATH_SEPARATOR);
|
||||
for (String path : paths) {
|
||||
try {
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
} catch (InvalidPathException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (System.getenv("HMCL_JRES") != null) {
|
||||
String[] paths = System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR);
|
||||
for (String path : paths) {
|
||||
try {
|
||||
tryAddJavaHome(javaRuntimes, Paths.get(path));
|
||||
} catch (InvalidPathException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
|
||||
|
||||
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
|
||||
try {
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Invalid Java path: " + javaPath);
|
||||
}
|
||||
}
|
||||
|
||||
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
|
||||
if (currentJava != null
|
||||
&& !javaRuntimes.containsKey(currentJava.getBinary())
|
||||
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
|
||||
javaRuntimes.put(currentJava.getBinary(), currentJava);
|
||||
}
|
||||
|
||||
LOG.trace(javaRuntimes.values().stream().sorted()
|
||||
.map(it -> String.format(" - %s %s (%s, %s): %s",
|
||||
it.isJDK() ? "JDK" : "JRE",
|
||||
it.getVersion(),
|
||||
it.getPlatform().getArchitecture().getDisplayName(),
|
||||
Lang.requireNonNullElse(it.getVendor(), "Unknown"),
|
||||
it.getBinary()))
|
||||
.collect(Collectors.joining("\n", "Finished Java lookup, found " + javaRuntimes.size() + "\n", "")));
|
||||
|
||||
return javaRuntimes;
|
||||
}
|
||||
|
||||
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
|
||||
Path executable = getExecutable(javaHome);
|
||||
if (!Files.isRegularFile(executable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
executable = executable.toRealPath();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to resolve path " + executable, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
JavaInfo info = null;
|
||||
|
||||
Path releaseFile = javaHome.resolve("release");
|
||||
if (Files.exists(releaseFile)) {
|
||||
try {
|
||||
info = JavaInfo.fromReleaseFile(releaseFile);
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
info = JavaInfo.fromExecutable(executable, false);
|
||||
} catch (IOException e2) {
|
||||
e2.addSuppressed(e);
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info != null && isCompatible(info.getPlatform()))
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
|
||||
}
|
||||
|
||||
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
|
||||
try {
|
||||
executable = executable.toRealPath();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
JavaInfo info = null;
|
||||
try {
|
||||
info = JavaInfo.fromExecutable(executable);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e);
|
||||
}
|
||||
|
||||
if (info != null && isCompatible(info.getPlatform())) {
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
|
||||
}
|
||||
}
|
||||
|
||||
private static void tryAddJavaInComponentDir(Map<Path, JavaRuntime> javaRuntimes, String platform, Path component, boolean verify) {
|
||||
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
|
||||
if (!Files.isRegularFile(sha1File))
|
||||
return;
|
||||
|
||||
Path dir = component.resolve(platform).resolve(component.getFileName());
|
||||
|
||||
if (verify) {
|
||||
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isEmpty()) continue;
|
||||
|
||||
int idx = line.indexOf(" /#//");
|
||||
if (idx <= 0)
|
||||
throw new IOException("Illegal line: " + line);
|
||||
|
||||
Path file = dir.resolve(line.substring(0, idx));
|
||||
|
||||
// Should we check the sha1 of files? This will take a lot of time.
|
||||
if (Files.notExists(file))
|
||||
throw new NoSuchFileException(file.toAbsolutePath().toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to verify Java in " + component, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
Path macPath = dir.resolve("jre.bundle/Contents/Home");
|
||||
if (Files.exists(macPath)) {
|
||||
tryAddJavaHome(javaRuntimes, macPath);
|
||||
return;
|
||||
} else
|
||||
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
|
||||
}
|
||||
|
||||
tryAddJavaHome(javaRuntimes, dir);
|
||||
}
|
||||
|
||||
private static void searchAllJavaInRepository(Map<Path, JavaRuntime> javaRuntimes, Platform platform) {
|
||||
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
|
||||
if (!Files.isDirectory(directory))
|
||||
return;
|
||||
// Examples:
|
||||
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
|
||||
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
|
||||
|
||||
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
|
||||
if (javaPlatform != null) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify);
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
|
||||
}
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.CURRENT_ARCH == Architecture.ARM64) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.OSX_X86_64), verify);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, String platform, boolean verify) {
|
||||
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
|
||||
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
|
||||
for (Path component : dir) {
|
||||
tryAddJavaInComponentDir(javaRuntimes, platform, component, verify);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list java-runtime directory " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllJavaInDirectory(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||
for (Path subDir : stream) {
|
||||
tryAddJavaHome(javaRuntimes, subDir);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to find Java in " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchJavaInProgramFiles(Map<Path, JavaRuntime> javaRuntimes, String env, String defaultValue) {
|
||||
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
|
||||
Path path;
|
||||
try {
|
||||
path = Paths.get(programFiles);
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
|
||||
searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor));
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Windows Registry Support ====
|
||||
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, String location) {
|
||||
for (String java : querySubFolders(location)) {
|
||||
if (!querySubFolders(java).contains(java + "\\MSI"))
|
||||
continue;
|
||||
String home = queryRegisterValue(java, "JavaHome");
|
||||
if (home != null) {
|
||||
try {
|
||||
tryAddJavaHome(javaRuntimes, Paths.get(home));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Invalid Java path in system registry: " + home);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> querySubFolders(String location) {
|
||||
List<String> res = new ArrayList<>();
|
||||
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location});
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
|
||||
for (String line; (line = reader.readLine()) != null; ) {
|
||||
if (line.startsWith(location) && !line.equals(location)) {
|
||||
res.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to query sub folders of " + location, e);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private static String queryRegisterValue(String location, String name) {
|
||||
boolean last = false;
|
||||
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location, "/v", name});
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
|
||||
for (String line; (line = reader.readLine()) != null; ) {
|
||||
if (StringUtils.isNotBlank(line)) {
|
||||
if (last && line.trim().startsWith(name)) {
|
||||
int begins = line.indexOf(name);
|
||||
if (begins > 0) {
|
||||
String s2 = line.substring(begins + name.length());
|
||||
begins = s2.indexOf("REG_SZ");
|
||||
if (begins > 0) {
|
||||
return s2.substring(begins + "REG_SZ".length()).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (location.equals(line.trim())) {
|
||||
last = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to query register value of " + location, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
@JsonAdapter(JavaManifest.Serializer.class)
|
||||
public final class JavaManifest {
|
||||
|
||||
private final JavaInfo info;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, Object> update;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, JavaLocalFiles.Local> files;
|
||||
|
||||
public JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
|
||||
this.info = info;
|
||||
this.update = update;
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public JavaInfo getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public Map<String, Object> getUpdate() {
|
||||
return update;
|
||||
}
|
||||
|
||||
public Map<String, JavaLocalFiles.Local> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public static final class Serializer implements JsonSerializer<JavaManifest>, JsonDeserializer<JavaManifest> {
|
||||
|
||||
private static final Type LOCAL_FILES_TYPE = mapTypeOf(String.class, JavaLocalFiles.Local.class).getType();
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(JavaManifest src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject res = new JsonObject();
|
||||
res.addProperty("os.name", src.getInfo().getPlatform().getOperatingSystem().getCheckedName());
|
||||
res.addProperty("os.arch", src.getInfo().getPlatform().getArchitecture().getCheckedName());
|
||||
res.addProperty("java.version", src.getInfo().getVersion());
|
||||
res.addProperty("java.vendor", src.getInfo().getVendor());
|
||||
|
||||
if (src.getUpdate() != null)
|
||||
res.add("update", context.serialize(src.getUpdate()));
|
||||
|
||||
if (src.getFiles() != null)
|
||||
res.add("files", context.serialize(src.getFiles(), LOCAL_FILES_TYPE));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaManifest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!json.isJsonObject())
|
||||
throw new JsonParseException(json.toString());
|
||||
|
||||
try {
|
||||
JsonObject jsonObject = json.getAsJsonObject();
|
||||
OperatingSystem osName = OperatingSystem.parseOSName(jsonObject.getAsJsonPrimitive("os.name").getAsString());
|
||||
Architecture osArch = Architecture.parseArchName(jsonObject.getAsJsonPrimitive("os.arch").getAsString());
|
||||
String javaVersion = jsonObject.getAsJsonPrimitive("java.version").getAsString();
|
||||
String javaVendor = Optional.ofNullable(jsonObject.get("java.vendor")).map(JsonElement::getAsString).orElse(null);
|
||||
|
||||
Map<String, Object> update = jsonObject.has("update") ? context.deserialize(jsonObject.get("update"), Map.class) : null;
|
||||
Map<String, JavaLocalFiles.Local> files = jsonObject.has("files") ? context.deserialize(jsonObject.get("files"), LOCAL_FILES_TYPE) : null;
|
||||
|
||||
if (osName == null || osArch == null || javaVersion == null)
|
||||
throw new JsonParseException(json.toString());
|
||||
|
||||
return new JavaManifest(new JavaInfo(Platform.getPlatform(osName, osArch), javaVersion, javaVendor), update, files);
|
||||
} catch (JsonParseException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new JsonParseException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,40 +17,35 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyListProperty;
|
||||
import javafx.beans.property.ReadOnlyListWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.*;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static javafx.collections.FXCollections.observableArrayList;
|
||||
@@ -58,41 +53,46 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class Accounts {
|
||||
private Accounts() {}
|
||||
private Accounts() {
|
||||
}
|
||||
|
||||
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
|
||||
|
||||
private static void triggerAuthlibInjectorUpdateCheck() {
|
||||
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to check update for authlib-injector", e);
|
||||
LOG.warning("Failed to check update for authlib-injector", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MicrosoftAccountLoginStage.INSTANCE));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_AUTHLIB_INJECTOR, FACTORY_MICROSOFT);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
|
||||
|
||||
static {
|
||||
type2factory.put("offline", FACTORY_OFFLINE);
|
||||
type2factory.put("yggdrasil", FACTORY_MOJANG);
|
||||
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
|
||||
type2factory.put("microsoft", FACTORY_MICROSOFT);
|
||||
|
||||
@@ -100,14 +100,24 @@ public final class Accounts {
|
||||
}
|
||||
|
||||
public static String getLoginType(AccountFactory<?> factory) {
|
||||
return Optional.ofNullable(factory2type.get(factory))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory"));
|
||||
String type = factory2type.get(factory);
|
||||
if (type != null) return type;
|
||||
|
||||
if (factory instanceof BoundAuthlibInjectorAccountFactory) {
|
||||
return factory2type.get(FACTORY_AUTHLIB_INJECTOR);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unrecognized account factory");
|
||||
}
|
||||
|
||||
public static AccountFactory<?> getAccountFactory(String loginType) {
|
||||
return Optional.ofNullable(type2factory.get(loginType))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unrecognized login type"));
|
||||
}
|
||||
|
||||
public static BoundAuthlibInjectorAccountFactory getAccountFactoryByAuthlibInjectorServer(AuthlibInjectorServer server) {
|
||||
return new BoundAuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, server);
|
||||
}
|
||||
// ====
|
||||
|
||||
public static AccountFactory<?> getAccountFactory(Account account) {
|
||||
@@ -115,67 +125,26 @@ public final class Accounts {
|
||||
return FACTORY_OFFLINE;
|
||||
else if (account instanceof AuthlibInjectorAccount)
|
||||
return FACTORY_AUTHLIB_INJECTOR;
|
||||
else if (account instanceof YggdrasilAccount)
|
||||
return FACTORY_MOJANG;
|
||||
else if (account instanceof MicrosoftAccount)
|
||||
return FACTORY_MICROSOFT;
|
||||
else
|
||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||
}
|
||||
|
||||
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
|
||||
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
|
||||
private static final String GLOBAL_PREFIX = "$GLOBAL:";
|
||||
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
|
||||
|
||||
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
|
||||
{
|
||||
accounts.addListener(onInvalidating(this::invalidated));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
// this methods first checks whether the current selection is valid
|
||||
// if it's valid, the underlying storage will be updated
|
||||
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
|
||||
Account selected = get();
|
||||
if (accounts.isEmpty()) {
|
||||
if (selected == null) {
|
||||
// valid
|
||||
} else {
|
||||
// the previously selected account is gone, we can only set it to null here
|
||||
set(null);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (accounts.contains(selected)) {
|
||||
// valid
|
||||
} else {
|
||||
// the previously selected account is gone
|
||||
set(accounts.get(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// selection is valid, store it
|
||||
if (!initialized)
|
||||
return;
|
||||
updateAccountStorages();
|
||||
}
|
||||
};
|
||||
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[]{account});
|
||||
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
|
||||
|
||||
/**
|
||||
* True if {@link #init()} hasn't been called.
|
||||
*/
|
||||
private static boolean initialized = false;
|
||||
|
||||
static {
|
||||
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
||||
}
|
||||
|
||||
private static Map<Object, Object> getAccountStorage(Account account) {
|
||||
Map<Object, Object> storage = account.toStorage();
|
||||
storage.put("type", getLoginType(getAccountFactory(account)));
|
||||
if (account == selectedAccount.get()) {
|
||||
storage.put("selected", true);
|
||||
}
|
||||
return storage;
|
||||
}
|
||||
|
||||
@@ -185,7 +154,64 @@ public final class Accounts {
|
||||
if (!initialized)
|
||||
return;
|
||||
// update storage
|
||||
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
|
||||
|
||||
ArrayList<Map<Object, Object>> global = new ArrayList<>();
|
||||
ArrayList<Map<Object, Object>> portable = new ArrayList<>();
|
||||
|
||||
for (Account account : accounts) {
|
||||
Map<Object, Object> storage = getAccountStorage(account);
|
||||
if (account.isPortable())
|
||||
portable.add(storage);
|
||||
else
|
||||
global.add(storage);
|
||||
}
|
||||
|
||||
if (!global.equals(globalAccountStorages))
|
||||
globalAccountStorages.setAll(global);
|
||||
if (!portable.equals(config().getAccountStorages()))
|
||||
config().getAccountStorages().setAll(portable);
|
||||
}
|
||||
|
||||
private static void loadGlobalAccountStorages() {
|
||||
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
|
||||
if (Files.exists(globalAccountsFile)) {
|
||||
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
|
||||
globalAccountStorages.setAll(Config.CONFIG_GSON.fromJson(reader, listTypeOf(mapTypeOf(Object.class, Object.class))));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load global accounts", e);
|
||||
}
|
||||
}
|
||||
|
||||
InvocationDispatcher<String> dispatcher = InvocationDispatcher.runOn(Lang::thread, json -> {
|
||||
LOG.info("Saving global accounts");
|
||||
synchronized (globalAccountsFile) {
|
||||
try {
|
||||
synchronized (globalAccountsFile) {
|
||||
FileUtils.saveSafely(globalAccountsFile, json);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to save global accounts", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalAccountStorages.addListener(onInvalidating(() ->
|
||||
dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages))));
|
||||
}
|
||||
|
||||
private static Account parseAccount(Map<Object, Object> storage) {
|
||||
AccountFactory<?> factory = type2factory.get(storage.get("type"));
|
||||
if (factory == null) {
|
||||
LOG.warning("Unrecognized account type: " + storage);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return factory.fromStorage(storage);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load account: " + storage, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,45 +221,112 @@ public final class Accounts {
|
||||
if (initialized)
|
||||
throw new IllegalStateException("Already initialized");
|
||||
|
||||
// load accounts
|
||||
config().getAccountStorages().forEach(storage -> {
|
||||
AccountFactory<?> factory = type2factory.get(storage.get("type"));
|
||||
if (factory == null) {
|
||||
LOG.warning("Unrecognized account type: " + storage);
|
||||
return;
|
||||
}
|
||||
Account account;
|
||||
try {
|
||||
account = factory.fromStorage(storage);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
|
||||
return;
|
||||
}
|
||||
accounts.add(account);
|
||||
if (!config().isAddedLittleSkin()) {
|
||||
AuthlibInjectorServer littleSkin = new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/");
|
||||
|
||||
if (Boolean.TRUE.equals(storage.get("selected"))) {
|
||||
selectedAccount.set(account);
|
||||
if (config().getAuthlibInjectorServers().stream().noneMatch(it -> littleSkin.getUrl().equals(it.getUrl()))) {
|
||||
config().getAuthlibInjectorServers().add(0, littleSkin);
|
||||
}
|
||||
});
|
||||
|
||||
config().setAddedLittleSkin(true);
|
||||
}
|
||||
|
||||
loadGlobalAccountStorages();
|
||||
|
||||
// load accounts
|
||||
Account selected = null;
|
||||
for (Map<Object, Object> storage : config().getAccountStorages()) {
|
||||
Account account = parseAccount(storage);
|
||||
if (account != null) {
|
||||
account.setPortable(true);
|
||||
accounts.add(account);
|
||||
if (Boolean.TRUE.equals(storage.get("selected"))) {
|
||||
selected = account;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Map<Object, Object> storage : globalAccountStorages) {
|
||||
Account account = parseAccount(storage);
|
||||
if (account != null) {
|
||||
accounts.add(account);
|
||||
}
|
||||
}
|
||||
|
||||
String selectedAccountIdentifier = config().getSelectedAccount();
|
||||
if (selected == null && selectedAccountIdentifier != null) {
|
||||
boolean portable = true;
|
||||
if (selectedAccountIdentifier.startsWith(GLOBAL_PREFIX)) {
|
||||
portable = false;
|
||||
selectedAccountIdentifier = selectedAccountIdentifier.substring(GLOBAL_PREFIX.length());
|
||||
}
|
||||
|
||||
for (Account account : accounts) {
|
||||
if (selectedAccountIdentifier.equals(account.getIdentifier())) {
|
||||
if (portable == account.isPortable()) {
|
||||
selected = account;
|
||||
break;
|
||||
} else if (selected == null) {
|
||||
selected = account;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selected == null && !accounts.isEmpty()) {
|
||||
selected = accounts.get(0);
|
||||
}
|
||||
|
||||
selectedAccount.set(selected);
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
// this method first checks whether the current selection is valid
|
||||
// if it's valid, the underlying storage will be updated
|
||||
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
|
||||
Account account = selectedAccount.get();
|
||||
if (accounts.isEmpty()) {
|
||||
if (account == null) {
|
||||
// valid
|
||||
} else {
|
||||
// the previously selected account is gone, we can only set it to null here
|
||||
selectedAccount.set(null);
|
||||
}
|
||||
} else {
|
||||
if (accounts.contains(account)) {
|
||||
// valid
|
||||
} else {
|
||||
// the previously selected account is gone
|
||||
selectedAccount.set(accounts.get(0));
|
||||
}
|
||||
}
|
||||
};
|
||||
selectedAccount.addListener(listener);
|
||||
selectedAccount.addListener(onInvalidating(() -> {
|
||||
Account account = selectedAccount.get();
|
||||
if (account != null)
|
||||
config().setSelectedAccount(account.isPortable() ? account.getIdentifier() : GLOBAL_PREFIX + account.getIdentifier());
|
||||
else
|
||||
config().setSelectedAccount(null);
|
||||
}));
|
||||
accounts.addListener(listener);
|
||||
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
||||
|
||||
initialized = true;
|
||||
|
||||
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
|
||||
|
||||
Account selected = selectedAccount.get();
|
||||
if (selected != null) {
|
||||
Account finalSelected = selected;
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
selected.logIn();
|
||||
} catch (AuthenticationException e) {
|
||||
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
|
||||
finalSelected.logIn();
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to log " + finalSelected + " in", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!config().getAuthlibInjectorServers().isEmpty()) {
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
}
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
@@ -242,7 +335,7 @@ public final class Accounts {
|
||||
try {
|
||||
server.fetchMetadataResponse();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + server, e);
|
||||
LOG.warning("Failed to fetch authlib-injector server metdata: " + server, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -252,10 +345,6 @@ public final class Accounts {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
public static ReadOnlyListProperty<Account> accountsProperty() {
|
||||
return accountsWrapper.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public static Account getSelectedAccount() {
|
||||
return selectedAccount.get();
|
||||
}
|
||||
@@ -317,9 +406,8 @@ public final class Accounts {
|
||||
// ====
|
||||
|
||||
// ==== Login type name i18n ===
|
||||
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
|
||||
private static final Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
|
||||
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
|
||||
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
|
||||
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"),
|
||||
pair(Accounts.FACTORY_MICROSOFT, "account.methods.microsoft"));
|
||||
|
||||
@@ -328,4 +416,70 @@ public final class Accounts {
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory")));
|
||||
}
|
||||
// ====
|
||||
|
||||
public static String localizeErrorMessage(Exception exception) {
|
||||
if (exception instanceof NoCharacterException) {
|
||||
return i18n("account.failed.no_character");
|
||||
} else if (exception instanceof ServerDisconnectException) {
|
||||
if (exception.getCause() instanceof SSLException) {
|
||||
return i18n("account.failed.ssl");
|
||||
} else {
|
||||
return i18n("account.failed.connect_authentication_server");
|
||||
}
|
||||
} else if (exception instanceof ServerResponseMalformedException) {
|
||||
return i18n("account.failed.server_response_malformed");
|
||||
} else if (exception instanceof RemoteAuthenticationException) {
|
||||
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
|
||||
String remoteMessage = remoteException.getRemoteMessage();
|
||||
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
|
||||
if (remoteMessage.contains("Invalid credentials")) {
|
||||
return i18n("account.failed.invalid_credentials");
|
||||
} else if (remoteMessage.contains("Invalid token")) {
|
||||
return i18n("account.failed.invalid_token");
|
||||
} else if (remoteMessage.contains("Invalid username or password")) {
|
||||
return i18n("account.failed.invalid_password");
|
||||
} else {
|
||||
return remoteMessage;
|
||||
}
|
||||
} else if ("ResourceException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
|
||||
if (remoteMessage.contains("The requested resource is no longer available")) {
|
||||
return i18n("account.failed.migration");
|
||||
} else {
|
||||
return remoteMessage;
|
||||
}
|
||||
}
|
||||
return exception.getMessage();
|
||||
} else if (exception instanceof AuthlibInjectorDownloadException) {
|
||||
return i18n("account.failed.injector_download_failure");
|
||||
} else if (exception instanceof CharacterDeletedException) {
|
||||
return i18n("account.failed.character_deleted");
|
||||
} else if (exception instanceof InvalidSkinException) {
|
||||
return i18n("account.skin.invalid_skin");
|
||||
} else if (exception instanceof MicrosoftService.XboxAuthorizationException) {
|
||||
long errorCode = ((MicrosoftService.XboxAuthorizationException) exception).getErrorCode();
|
||||
if (errorCode == MicrosoftService.XboxAuthorizationException.ADD_FAMILY) {
|
||||
return i18n("account.methods.microsoft.error.add_family");
|
||||
} else if (errorCode == MicrosoftService.XboxAuthorizationException.COUNTRY_UNAVAILABLE) {
|
||||
return i18n("account.methods.microsoft.error.country_unavailable");
|
||||
} else if (errorCode == MicrosoftService.XboxAuthorizationException.MISSING_XBOX_ACCOUNT) {
|
||||
return i18n("account.methods.microsoft.error.missing_xbox_account");
|
||||
} else {
|
||||
return i18n("account.methods.microsoft.error.unknown", errorCode);
|
||||
}
|
||||
} else if (exception instanceof MicrosoftService.XBox400Exception) {
|
||||
return i18n("account.methods.microsoft.error.wrong_verify_method");
|
||||
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
|
||||
return i18n("account.methods.microsoft.error.no_character");
|
||||
} else if (exception instanceof MicrosoftService.NoXuiException) {
|
||||
return i18n("account.methods.microsoft.error.add_family_probably");
|
||||
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
|
||||
return i18n("account.methods.microsoft.snapshot");
|
||||
} else if (exception instanceof OAuthAccount.WrongAccountException) {
|
||||
return i18n("account.failed.wrong_account");
|
||||
} else if (exception.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,70 +22,75 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class AuthlibInjectorServers implements Validation {
|
||||
public final class AuthlibInjectorServers implements Validation {
|
||||
|
||||
public static final String CONFIG_FILENAME = "authlib-injectors.json";
|
||||
|
||||
private static final Set<AuthlibInjectorServer> servers = new CopyOnWriteArraySet<>();
|
||||
|
||||
public static Set<AuthlibInjectorServer> getServers() {
|
||||
return servers;
|
||||
}
|
||||
|
||||
private final List<String> urls;
|
||||
|
||||
public AuthlibInjectorServers(List<String> urls) {
|
||||
private AuthlibInjectorServers(List<String> urls) {
|
||||
this.urls = urls;
|
||||
}
|
||||
|
||||
public List<String> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (urls == null)
|
||||
throw new JsonParseException("authlib-injectors.json -> urls cannot be null");
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (this.urls == null) {
|
||||
throw new JsonParseException("authlib-injectors.json -> urls cannot be null.");
|
||||
}
|
||||
}
|
||||
|
||||
private static final Path configLocation = Paths.get(CONFIG_FILENAME);
|
||||
private static AuthlibInjectorServers configInstance;
|
||||
|
||||
public synchronized static void init() {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("AuthlibInjectorServers is already loaded");
|
||||
public static void init() {
|
||||
Path configLocation;
|
||||
Path jarPath = JarUtils.thisJarPath();
|
||||
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
|
||||
configLocation = jarPath.getParent().resolve(CONFIG_FILENAME);
|
||||
} else {
|
||||
configLocation = Paths.get(CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
configInstance = new AuthlibInjectorServers(Collections.emptyList());
|
||||
|
||||
if (Files.exists(configLocation)) {
|
||||
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
|
||||
AuthlibInjectorServers configInstance;
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Malformed authlib-injectors.json", e);
|
||||
LOG.warning("Malformed authlib-injectors.json", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!configInstance.urls.isEmpty()) {
|
||||
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
|
||||
for (String url : configInstance.urls) {
|
||||
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
|
||||
.thenAcceptAsync(Schedulers.javafx(), server -> {
|
||||
config().getAuthlibInjectorServers().add(server);
|
||||
servers.add(server);
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ConfigHolder.isNewlyCreated() && !AuthlibInjectorServers.getConfigInstance().getUrls().isEmpty()) {
|
||||
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
|
||||
for (String url : AuthlibInjectorServers.getConfigInstance().getUrls()) {
|
||||
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
|
||||
.thenAcceptAsync(Schedulers.javafx(), server -> config().getAuthlibInjectorServers().add(server))
|
||||
.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static AuthlibInjectorServers getConfigInstance() {
|
||||
return configInstance;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -34,7 +34,6 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
|
||||
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
|
||||
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
@@ -52,7 +51,7 @@ public final class Config implements Cloneable, Observable {
|
||||
|
||||
public static final int CURRENT_UI_VERSION = 0;
|
||||
|
||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
public static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
||||
@@ -83,6 +82,9 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("bgpath")
|
||||
private StringProperty backgroundImage = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("bgurl")
|
||||
private StringProperty backgroundImageUrl = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("commonDirType")
|
||||
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
|
||||
|
||||
@@ -110,6 +112,12 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("proxyPassword")
|
||||
private StringProperty proxyPass = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("x")
|
||||
private DoubleProperty x = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("y")
|
||||
private DoubleProperty y = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("width")
|
||||
private DoubleProperty width = new SimpleDoubleProperty();
|
||||
|
||||
@@ -117,34 +125,58 @@ public final class Config implements Cloneable, Observable {
|
||||
private DoubleProperty height = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("theme")
|
||||
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
|
||||
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>();
|
||||
|
||||
@SerializedName("localization")
|
||||
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
|
||||
|
||||
@SerializedName("autoDownloadThreads")
|
||||
private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(true);
|
||||
|
||||
@SerializedName("downloadThreads")
|
||||
private IntegerProperty downloadThreads = new SimpleIntegerProperty(64);
|
||||
|
||||
@SerializedName("downloadType")
|
||||
private StringProperty downloadType = new SimpleStringProperty("bmclapi");
|
||||
private StringProperty downloadType = new SimpleStringProperty(DownloadProviders.DEFAULT_RAW_PROVIDER_ID);
|
||||
|
||||
@SerializedName("autoChooseDownloadType")
|
||||
private BooleanProperty autoChooseDownloadType = new SimpleBooleanProperty(true);
|
||||
|
||||
@SerializedName("versionListSource")
|
||||
private StringProperty versionListSource = new SimpleStringProperty("balanced");
|
||||
|
||||
@SerializedName("configurations")
|
||||
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
|
||||
private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
|
||||
|
||||
@SerializedName("selectedAccount")
|
||||
private StringProperty selectedAccount = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("accounts")
|
||||
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
||||
|
||||
@SerializedName("fontFamily")
|
||||
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
|
||||
private StringProperty fontFamily = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("fontSize")
|
||||
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
|
||||
|
||||
@SerializedName("launcherFontFamily")
|
||||
private StringProperty launcherFontFamily = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("logLines")
|
||||
private IntegerProperty logLines = new SimpleIntegerProperty(100);
|
||||
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();
|
||||
|
||||
@SerializedName("titleTransparent")
|
||||
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
|
||||
|
||||
@SerializedName("authlibInjectorServers")
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
|
||||
|
||||
@SerializedName("updateChannel")
|
||||
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
|
||||
@SerializedName("addedLittleSkin")
|
||||
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
|
||||
|
||||
@SerializedName("promptedVersion")
|
||||
private StringProperty promptedVersion = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("_version")
|
||||
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
|
||||
@@ -165,6 +197,12 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("preferredLoginType")
|
||||
private StringProperty preferredLoginType = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("animationDisabled")
|
||||
private BooleanProperty animationDisabled = new SimpleBooleanProperty();
|
||||
|
||||
@SerializedName("shownTips")
|
||||
private ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
|
||||
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public Config() {
|
||||
@@ -227,6 +265,18 @@ public final class Config implements Cloneable, Observable {
|
||||
return backgroundImage;
|
||||
}
|
||||
|
||||
public String getBackgroundImageUrl() {
|
||||
return backgroundImageUrl.get();
|
||||
}
|
||||
|
||||
public StringProperty backgroundImageUrlProperty() {
|
||||
return backgroundImageUrl;
|
||||
}
|
||||
|
||||
public void setBackgroundImageUrl(String backgroundImageUrl) {
|
||||
this.backgroundImageUrl.set(backgroundImageUrl);
|
||||
}
|
||||
|
||||
public EnumCommonDirectory getCommonDirType() {
|
||||
return commonDirType.get();
|
||||
}
|
||||
@@ -335,6 +385,30 @@ public final class Config implements Cloneable, Observable {
|
||||
return proxyPass;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x.get();
|
||||
}
|
||||
|
||||
public DoubleProperty xProperty() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public void setX(double x) {
|
||||
this.x.set(x);
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y.get();
|
||||
}
|
||||
|
||||
public DoubleProperty yProperty() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setY(double y) {
|
||||
this.y.set(y);
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width.get();
|
||||
}
|
||||
@@ -383,6 +457,30 @@ public final class Config implements Cloneable, Observable {
|
||||
return localization;
|
||||
}
|
||||
|
||||
public boolean getAutoDownloadThreads() {
|
||||
return autoDownloadThreads.get();
|
||||
}
|
||||
|
||||
public BooleanProperty autoDownloadThreadsProperty() {
|
||||
return autoDownloadThreads;
|
||||
}
|
||||
|
||||
public void setAutoDownloadThreads(boolean autoDownloadThreads) {
|
||||
this.autoDownloadThreads.set(autoDownloadThreads);
|
||||
}
|
||||
|
||||
public int getDownloadThreads() {
|
||||
return downloadThreads.get();
|
||||
}
|
||||
|
||||
public IntegerProperty downloadThreadsProperty() {
|
||||
return downloadThreads;
|
||||
}
|
||||
|
||||
public void setDownloadThreads(int downloadThreads) {
|
||||
this.downloadThreads.set(downloadThreads);
|
||||
}
|
||||
|
||||
public String getDownloadType() {
|
||||
return downloadType.get();
|
||||
}
|
||||
@@ -395,10 +493,46 @@ public final class Config implements Cloneable, Observable {
|
||||
return downloadType;
|
||||
}
|
||||
|
||||
public ObservableMap<String, Profile> getConfigurations() {
|
||||
public boolean isAutoChooseDownloadType() {
|
||||
return autoChooseDownloadType.get();
|
||||
}
|
||||
|
||||
public BooleanProperty autoChooseDownloadTypeProperty() {
|
||||
return autoChooseDownloadType;
|
||||
}
|
||||
|
||||
public void setAutoChooseDownloadType(boolean autoChooseDownloadType) {
|
||||
this.autoChooseDownloadType.set(autoChooseDownloadType);
|
||||
}
|
||||
|
||||
public String getVersionListSource() {
|
||||
return versionListSource.get();
|
||||
}
|
||||
|
||||
public void setVersionListSource(String versionListSource) {
|
||||
this.versionListSource.set(versionListSource);
|
||||
}
|
||||
|
||||
public StringProperty versionListSourceProperty() {
|
||||
return versionListSource;
|
||||
}
|
||||
|
||||
public MapProperty<String, Profile> getConfigurations() {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
public String getSelectedAccount() {
|
||||
return selectedAccount.get();
|
||||
}
|
||||
|
||||
public void setSelectedAccount(String selectedAccount) {
|
||||
this.selectedAccount.set(selectedAccount);
|
||||
}
|
||||
|
||||
public StringProperty selectedAccountProperty() {
|
||||
return selectedAccount;
|
||||
}
|
||||
|
||||
public ObservableList<Map<Object, Object>> getAccountStorages() {
|
||||
return accountStorages;
|
||||
}
|
||||
@@ -427,15 +561,27 @@ public final class Config implements Cloneable, Observable {
|
||||
return fontSize;
|
||||
}
|
||||
|
||||
public int getLogLines() {
|
||||
public String getLauncherFontFamily() {
|
||||
return launcherFontFamily.get();
|
||||
}
|
||||
|
||||
public StringProperty launcherFontFamilyProperty() {
|
||||
return launcherFontFamily;
|
||||
}
|
||||
|
||||
public void setLauncherFontFamily(String launcherFontFamily) {
|
||||
this.launcherFontFamily.set(launcherFontFamily);
|
||||
}
|
||||
|
||||
public Integer getLogLines() {
|
||||
return logLines.get();
|
||||
}
|
||||
|
||||
public void setLogLines(int logLines) {
|
||||
public void setLogLines(Integer logLines) {
|
||||
this.logLines.set(logLines);
|
||||
}
|
||||
|
||||
public IntegerProperty logLinesProperty() {
|
||||
public ObjectProperty<Integer> logLinesProperty() {
|
||||
return logLines;
|
||||
}
|
||||
|
||||
@@ -443,16 +589,16 @@ public final class Config implements Cloneable, Observable {
|
||||
return authlibInjectorServers;
|
||||
}
|
||||
|
||||
public UpdateChannel getUpdateChannel() {
|
||||
return updateChannel.get();
|
||||
public boolean isAddedLittleSkin() {
|
||||
return addedLittleSkin.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<UpdateChannel> updateChannelProperty() {
|
||||
return updateChannel;
|
||||
public BooleanProperty addedLittleSkinProperty() {
|
||||
return addedLittleSkin;
|
||||
}
|
||||
|
||||
public void setUpdateChannel(UpdateChannel updateChannel) {
|
||||
this.updateChannel.set(updateChannel);
|
||||
public void setAddedLittleSkin(boolean addedLittleSkin) {
|
||||
this.addedLittleSkin.set(addedLittleSkin);
|
||||
}
|
||||
|
||||
public int getConfigVersion() {
|
||||
@@ -490,4 +636,44 @@ public final class Config implements Cloneable, Observable {
|
||||
public StringProperty preferredLoginTypeProperty() {
|
||||
return preferredLoginType;
|
||||
}
|
||||
|
||||
public boolean isAnimationDisabled() {
|
||||
return animationDisabled.get();
|
||||
}
|
||||
|
||||
public BooleanProperty animationDisabledProperty() {
|
||||
return animationDisabled;
|
||||
}
|
||||
|
||||
public void setAnimationDisabled(boolean animationDisabled) {
|
||||
this.animationDisabled.set(animationDisabled);
|
||||
}
|
||||
|
||||
public boolean isTitleTransparent() {
|
||||
return titleTransparent.get();
|
||||
}
|
||||
|
||||
public BooleanProperty titleTransparentProperty() {
|
||||
return titleTransparent;
|
||||
}
|
||||
|
||||
public void setTitleTransparent(boolean titleTransparent) {
|
||||
this.titleTransparent.set(titleTransparent);
|
||||
}
|
||||
|
||||
public String getPromptedVersion() {
|
||||
return promptedVersion.get();
|
||||
}
|
||||
|
||||
public StringProperty promptedVersionProperty() {
|
||||
return promptedVersion;
|
||||
}
|
||||
|
||||
public void setPromptedVersion(String promptedVersion) {
|
||||
this.promptedVersion.set(promptedVersion);
|
||||
}
|
||||
|
||||
public ObservableMap<String, Object> getShownTips() {
|
||||
return shownTips;
|
||||
}
|
||||
}
|
||||
@@ -17,33 +17,35 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.nio.file.*;
|
||||
import java.util.Locale;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ConfigHolder {
|
||||
|
||||
private ConfigHolder() {}
|
||||
private ConfigHolder() {
|
||||
}
|
||||
|
||||
public static final String CONFIG_FILENAME = "hmcl.json";
|
||||
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
|
||||
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_DIRECTORY.resolve("config.json");
|
||||
|
||||
private static Path configLocation;
|
||||
private static Config configInstance;
|
||||
private static GlobalConfig globalConfigInstance;
|
||||
private static boolean newlyCreated;
|
||||
private static boolean ownerChanged = false;
|
||||
|
||||
public static Config config() {
|
||||
if (configInstance == null) {
|
||||
@@ -52,22 +54,43 @@ public final class ConfigHolder {
|
||||
return configInstance;
|
||||
}
|
||||
|
||||
public static GlobalConfig globalConfig() {
|
||||
if (globalConfigInstance == null) {
|
||||
throw new IllegalStateException("Configuration hasn't been loaded");
|
||||
}
|
||||
return globalConfigInstance;
|
||||
}
|
||||
|
||||
public static Path configLocation() {
|
||||
return configLocation;
|
||||
}
|
||||
|
||||
public static boolean isNewlyCreated() {
|
||||
return newlyCreated;
|
||||
}
|
||||
|
||||
public synchronized static void init() throws IOException {
|
||||
public static boolean isOwnerChanged() {
|
||||
return ownerChanged;
|
||||
}
|
||||
|
||||
public static void init() throws IOException {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("Configuration is already loaded");
|
||||
}
|
||||
|
||||
configLocation = locateConfig();
|
||||
|
||||
LOG.log(Level.INFO, "Config location: " + configLocation);
|
||||
LOG.info("Config location: " + configLocation);
|
||||
|
||||
configInstance = loadConfig();
|
||||
configInstance.addListener(source -> markConfigDirty());
|
||||
|
||||
globalConfigInstance = loadGlobalConfig();
|
||||
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
|
||||
|
||||
Locale.setDefault(config().getLocalization().getLocale());
|
||||
I18n.setLocale(configInstance.getLocalization());
|
||||
LOG.setLogRetention(globalConfig().getLogRetention());
|
||||
Settings.init();
|
||||
|
||||
if (newlyCreated) {
|
||||
@@ -78,24 +101,30 @@ public final class ConfigHolder {
|
||||
try {
|
||||
Files.setAttribute(configLocation, "dos:hidden", true);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to set hidden attribute of " + configLocation, e);
|
||||
LOG.warning("Failed to set hidden attribute of " + configLocation, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Files.isWritable(configLocation)) {
|
||||
// the config cannot be saved
|
||||
// throw up the error now to prevent further data loss
|
||||
throw new IOException("Config at " + configLocation + " is not writable");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& configLocation.getFileSystem() == FileSystems.getDefault()
|
||||
&& configLocation.toFile().canWrite()) {
|
||||
// There are some serious problems with the implementation of Samba or OpenJDK
|
||||
throw new SambaException();
|
||||
} else {
|
||||
// the config cannot be saved
|
||||
// throw up the error now to prevent further data loss
|
||||
throw new IOException("Config at " + configLocation + " is not writable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Path locateConfig() {
|
||||
Path exePath = Paths.get("");
|
||||
Path exePath = Paths.get("").toAbsolutePath();
|
||||
try {
|
||||
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
|
||||
.toURI()).toAbsolutePath();
|
||||
if (Files.isRegularFile(jarPath)) {
|
||||
Path jarPath = JarUtils.thisJarPath();
|
||||
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
|
||||
jarPath = jarPath.getParent();
|
||||
exePath = jarPath;
|
||||
|
||||
@@ -125,18 +154,26 @@ public final class ConfigHolder {
|
||||
|
||||
private static Config loadConfig() throws IOException {
|
||||
if (Files.exists(configLocation)) {
|
||||
try {
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
||||
&& "root".equals(System.getProperty("user.name"))
|
||||
&& !"root".equals(Files.getOwner(configLocation).getName())) {
|
||||
ownerChanged = true;
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
LOG.warning("Failed to get owner");
|
||||
}
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
Config deserialized = Config.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
} else {
|
||||
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
|
||||
ConfigUpgrader.upgradeConfig(deserialized, raw);
|
||||
ConfigUpgrader.upgradeConfig(deserialized, content);
|
||||
return deserialized;
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Malformed config.", e);
|
||||
LOG.warning("Malformed config.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,18 +182,18 @@ public final class ConfigHolder {
|
||||
return new Config();
|
||||
}
|
||||
|
||||
private static InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
|
||||
private static final InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
|
||||
try {
|
||||
writeToConfig(content);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to save config", e);
|
||||
LOG.error("Failed to save config", e);
|
||||
}
|
||||
});
|
||||
|
||||
private static void writeToConfig(String content) throws IOException {
|
||||
LOG.info("Saving config");
|
||||
synchronized (configLocation) {
|
||||
Files.write(configLocation, content.getBytes(UTF_8));
|
||||
FileUtils.saveSafely(configLocation, content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,4 +204,48 @@ public final class ConfigHolder {
|
||||
private static void saveConfigSync() throws IOException {
|
||||
writeToConfig(configInstance.toJson());
|
||||
}
|
||||
|
||||
// Global Config
|
||||
|
||||
private static GlobalConfig loadGlobalConfig() throws IOException {
|
||||
if (Files.exists(GLOBAL_CONFIG_PATH)) {
|
||||
try {
|
||||
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
|
||||
GlobalConfig deserialized = GlobalConfig.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
} else {
|
||||
return deserialized;
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
LOG.warning("Malformed config.", e);
|
||||
}
|
||||
}
|
||||
|
||||
LOG.info("Creating an empty global config");
|
||||
return new GlobalConfig();
|
||||
}
|
||||
|
||||
private static final InvocationDispatcher<String> globalConfigWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
|
||||
try {
|
||||
writeToGlobalConfig(content);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to save config", e);
|
||||
}
|
||||
});
|
||||
|
||||
private static void writeToGlobalConfig(String content) throws IOException {
|
||||
LOG.info("Saving global config");
|
||||
synchronized (GLOBAL_CONFIG_PATH) {
|
||||
FileUtils.saveSafely(GLOBAL_CONFIG_PATH, content);
|
||||
}
|
||||
}
|
||||
|
||||
static void markGlobalConfigDirty() {
|
||||
globalConfigWriter.accept(globalConfigInstance.toJson());
|
||||
}
|
||||
|
||||
private static void saveGlobalConfigSync() throws IOException {
|
||||
writeToConfig(globalConfigInstance.toJson());
|
||||
}
|
||||
}
|
||||
@@ -17,113 +17,89 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
final class ConfigUpgrader {
|
||||
private static final int VERSION = 0;
|
||||
|
||||
private ConfigUpgrader() {
|
||||
}
|
||||
|
||||
private static final int CURRENT_VERSION = 2;
|
||||
|
||||
/**
|
||||
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
|
||||
* This method is for the compatibility with old HMCL versions.
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
* @return true if config version is upgraded
|
||||
* @param rawContent raw json content of the config settings without modification
|
||||
*/
|
||||
static boolean upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
|
||||
boolean upgraded;
|
||||
if (deserialized.getConfigVersion() < VERSION) {
|
||||
deserialized.setConfigVersion(VERSION);
|
||||
// TODO: Add upgrade code here.
|
||||
upgraded = true;
|
||||
} else {
|
||||
upgraded = false;
|
||||
static void upgradeConfig(Config deserialized, String rawContent) {
|
||||
int configVersion = deserialized.getConfigVersion();
|
||||
|
||||
if (configVersion == CURRENT_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
upgradeV2(deserialized, rawJson);
|
||||
upgradeV3(deserialized, rawJson);
|
||||
if (configVersion > CURRENT_VERSION) {
|
||||
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
|
||||
deserialized.setConfigVersion(CURRENT_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
return upgraded;
|
||||
}
|
||||
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
|
||||
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
|
||||
|
||||
/**
|
||||
* Upgrade configuration of HMCL 2.x
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
*/
|
||||
private static void upgradeV2(Config deserialized, Map<?, ?> rawJson) {
|
||||
// Convert OfflineAccounts whose stored uuid is important.
|
||||
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
|
||||
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
|
||||
String selected = rawJson.containsKey("selectedAccount") ? null
|
||||
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
|
||||
if (configVersion < 1) {
|
||||
// Upgrade configuration of HMCL 2.x: Convert OfflineAccounts whose stored uuid is important.
|
||||
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
|
||||
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
|
||||
String selected = rawJson.containsKey("selectedAccount") ? null
|
||||
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
|
||||
|
||||
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
|
||||
((Map<?, ?>) uuidMap).forEach((key, value) -> {
|
||||
Map<Object, Object> storage = new HashMap<>();
|
||||
storage.put("type", "offline");
|
||||
storage.put("username", key);
|
||||
storage.put("uuid", value);
|
||||
if (key.equals(selected)) {
|
||||
storage.put("selected", true);
|
||||
}
|
||||
deserialized.getAccountStorages().add(storage);
|
||||
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
|
||||
((Map<?, ?>) uuidMap).forEach((key, value) -> {
|
||||
Map<Object, Object> storage = new HashMap<>();
|
||||
storage.put("type", "offline");
|
||||
storage.put("username", key);
|
||||
storage.put("uuid", value);
|
||||
if (key.equals(selected)) {
|
||||
storage.put("selected", true);
|
||||
}
|
||||
deserialized.getAccountStorages().add(storage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade configuration of HMCL earlier than 3.1.70
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
*/
|
||||
private static void upgradeV3(Config deserialized, Map<?, ?> rawJson) {
|
||||
if (!rawJson.containsKey("commonDirType"))
|
||||
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
|
||||
if (!rawJson.containsKey("backgroundType"))
|
||||
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
|
||||
if (!rawJson.containsKey("hasProxy"))
|
||||
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
|
||||
if (!rawJson.containsKey("hasProxyAuth"))
|
||||
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
|
||||
// Upgrade configuration of HMCL earlier than 3.1.70
|
||||
if (!rawJson.containsKey("commonDirType"))
|
||||
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
|
||||
if (!rawJson.containsKey("backgroundType"))
|
||||
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
|
||||
if (!rawJson.containsKey("hasProxy"))
|
||||
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
|
||||
if (!rawJson.containsKey("hasProxyAuth"))
|
||||
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
|
||||
|
||||
if (!rawJson.containsKey("downloadType")) {
|
||||
tryCast(rawJson.get("downloadtype"), Number.class)
|
||||
.map(Number::intValue)
|
||||
.ifPresent(id -> {
|
||||
if (id == 0) {
|
||||
deserialized.setDownloadType("mojang");
|
||||
} else if (id == 1) {
|
||||
deserialized.setDownloadType("bmclapi");
|
||||
}
|
||||
});
|
||||
if (!rawJson.containsKey("downloadType")) {
|
||||
tryCast(rawJson.get("downloadtype"), Number.class)
|
||||
.map(Number::intValue)
|
||||
.ifPresent(id -> {
|
||||
if (id == 0) {
|
||||
deserialized.setDownloadType("mojang");
|
||||
} else if (id == 1) {
|
||||
deserialized.setDownloadType("bmclapi");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tryCast(rawJson.get("selectedAccount"), String.class)
|
||||
.ifPresent(selected -> {
|
||||
deserialized.getAccountStorages().stream()
|
||||
.filter(storage -> {
|
||||
Object type = storage.get("type");
|
||||
if ("offline".equals(type)) {
|
||||
return selected.equals(storage.get("username") + ":" + storage.get("username"));
|
||||
} else if ("yggdrasil".equals(type) || "authlibInjector".equals(type)) {
|
||||
return selected.equals(storage.get("username") + ":" + storage.get("displayName"));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.ifPresent(storage -> storage.put("selected", true));
|
||||
});
|
||||
deserialized.setConfigVersion(CURRENT_VERSION);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,49 +17,99 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import org.jackhuang.hmcl.download.AdaptedDownloadProvider;
|
||||
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.MojangDownloadProvider;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class DownloadProviders {
|
||||
private DownloadProviders() {}
|
||||
|
||||
private static final AdaptedDownloadProvider DOWNLOAD_PROVIDER = new AdaptedDownloadProvider();
|
||||
private static DownloadProvider currentDownloadProvider;
|
||||
|
||||
public static final Map<String, DownloadProvider> providersById;
|
||||
public static final Map<String, DownloadProvider> rawProviders;
|
||||
private static final AdaptedDownloadProvider fileDownloadProvider = new AdaptedDownloadProvider();
|
||||
|
||||
public static final String DEFAULT_PROVIDER_ID = "mcbbs";
|
||||
private static final MojangDownloadProvider MOJANG;
|
||||
private static final BMCLAPIDownloadProvider BMCLAPI;
|
||||
|
||||
public static final String DEFAULT_PROVIDER_ID = "balanced";
|
||||
public static final String DEFAULT_RAW_PROVIDER_ID = "bmclapi";
|
||||
|
||||
private static final InvalidationListener observer;
|
||||
|
||||
static {
|
||||
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
|
||||
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
|
||||
if (bmclapiRootOverride != null) bmclapiRoot = bmclapiRootOverride;
|
||||
|
||||
MOJANG = new MojangDownloadProvider();
|
||||
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
|
||||
rawProviders = mapOf(
|
||||
pair("mojang", MOJANG),
|
||||
pair("bmclapi", BMCLAPI)
|
||||
);
|
||||
|
||||
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
|
||||
fileProvider.setDownloadProviderCandidates(Arrays.asList(BMCLAPI, MOJANG));
|
||||
BalancedDownloadProvider balanced = new BalancedDownloadProvider(MOJANG, BMCLAPI);
|
||||
|
||||
providersById = mapOf(
|
||||
pair("mojang", new MojangDownloadProvider()),
|
||||
pair("bmclapi", new BMCLAPIDownloadProvider(bmclapiRoot)),
|
||||
pair("mcbbs", new BMCLAPIDownloadProvider("https://download.mcbbs.net")));
|
||||
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
|
||||
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
|
||||
pair("mirror", new AutoDownloadProvider(BMCLAPI, fileProvider)));
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
FetchTask.setDownloadExecutorConcurrency(
|
||||
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
|
||||
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
|
||||
}
|
||||
|
||||
static void init() {
|
||||
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
|
||||
DownloadProvider primary = Optional.ofNullable(providersById.get(config().getDownloadType()))
|
||||
FXUtils.onChangeAndOperate(config().versionListSourceProperty(), versionListSource -> {
|
||||
if (!providersById.containsKey(versionListSource)) {
|
||||
config().setVersionListSource(DEFAULT_PROVIDER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
currentDownloadProvider = Optional.ofNullable(providersById.get(versionListSource))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
DOWNLOAD_PROVIDER.setDownloadProviderCandidates(
|
||||
});
|
||||
|
||||
if (!rawProviders.containsKey(config().getDownloadType())) {
|
||||
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
|
||||
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
|
||||
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
|
||||
fileDownloadProvider.setDownloadProviderCandidates(
|
||||
Stream.concat(
|
||||
Stream.of(primary),
|
||||
providersById.values().stream().filter(x -> x != primary)
|
||||
rawProviders.values().stream().filter(x -> x != primary)
|
||||
).collect(Collectors.toList())
|
||||
);
|
||||
});
|
||||
@@ -73,23 +123,46 @@ public final class DownloadProviders {
|
||||
return DEFAULT_PROVIDER_ID;
|
||||
}
|
||||
|
||||
public static AdaptedDownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
|
||||
AdaptedDownloadProvider adaptedDownloadProvider = new AdaptedDownloadProvider();
|
||||
DownloadProvider primary = Optional.ofNullable(providersById.get(primaryId))
|
||||
public static DownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
|
||||
return Optional.ofNullable(providersById.get(primaryId))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
adaptedDownloadProvider.setDownloadProviderCandidates(
|
||||
Stream.concat(
|
||||
Stream.of(primary),
|
||||
providersById.values().stream().filter(x -> x != primary)
|
||||
).collect(Collectors.toList())
|
||||
);
|
||||
return adaptedDownloadProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current primary preferred download provider
|
||||
*/
|
||||
public static AdaptedDownloadProvider getDownloadProvider() {
|
||||
return DOWNLOAD_PROVIDER;
|
||||
public static DownloadProvider getDownloadProvider() {
|
||||
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
|
||||
}
|
||||
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
return i18n("install.failed.downloading.timeout", url);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
return i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception.getCause() instanceof FileNotFoundException) {
|
||||
return i18n("download.code.404", url);
|
||||
} else if (exception.getCause() instanceof AccessDeniedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
} else if (exception.getCause() instanceof ArtifactMalformedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception instanceof ArtifactMalformedException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
} else if (exception instanceof CancellationException) {
|
||||
return i18n("message.cancelled");
|
||||
}
|
||||
return StringUtils.getStackTrace(exception);
|
||||
}
|
||||
}
|
||||
@@ -19,5 +19,8 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
public enum EnumBackgroundImage {
|
||||
DEFAULT,
|
||||
CUSTOM
|
||||
CUSTOM,
|
||||
CLASSIC,
|
||||
NETWORK,
|
||||
TRANSLUCENT
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(GlobalConfig.Serializer.class)
|
||||
public final class GlobalConfig implements Cloneable, Observable {
|
||||
|
||||
@Nullable
|
||||
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
||||
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
||||
if (loaded == null) {
|
||||
return null;
|
||||
}
|
||||
GlobalConfig instance = new GlobalConfig();
|
||||
PropertyUtils.copyProperties(loaded, instance);
|
||||
instance.unknownFields.putAll(loaded.unknownFields);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty logRetention = new SimpleIntegerProperty();
|
||||
|
||||
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
private final Map<String, Object> unknownFields = new HashMap<>();
|
||||
|
||||
private final transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public GlobalConfig() {
|
||||
PropertyUtils.attachListener(this, helper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
helper.removeListener(listener);
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
return Config.CONFIG_GSON.toJson(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GlobalConfig clone() {
|
||||
return fromJson(this.toJson());
|
||||
}
|
||||
|
||||
public int getAgreementVersion() {
|
||||
return agreementVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty agreementVersionProperty() {
|
||||
return agreementVersion;
|
||||
}
|
||||
|
||||
public void setAgreementVersion(int agreementVersion) {
|
||||
this.agreementVersion.set(agreementVersion);
|
||||
}
|
||||
|
||||
public int getPlatformPromptVersion() {
|
||||
return platformPromptVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty platformPromptVersionProperty() {
|
||||
return platformPromptVersion;
|
||||
}
|
||||
|
||||
public void setPlatformPromptVersion(int platformPromptVersion) {
|
||||
this.platformPromptVersion.set(platformPromptVersion);
|
||||
}
|
||||
|
||||
public int getLogRetention() {
|
||||
return logRetention.get();
|
||||
}
|
||||
|
||||
public IntegerProperty logRetentionProperty() {
|
||||
return logRetention;
|
||||
}
|
||||
|
||||
public void setLogRetention(int logRetention) {
|
||||
this.logRetention.set(logRetention);
|
||||
}
|
||||
|
||||
public ObservableSet<String> getUserJava() {
|
||||
return userJava;
|
||||
}
|
||||
|
||||
public ObservableSet<String> getDisabledJava() {
|
||||
return disabledJava;
|
||||
}
|
||||
|
||||
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||
"agreementVersion",
|
||||
"platformPromptVersion",
|
||||
"logRetention",
|
||||
"userJava",
|
||||
"disabledJava"
|
||||
));
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(GlobalConfig src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null) {
|
||||
return JsonNull.INSTANCE;
|
||||
}
|
||||
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
|
||||
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
|
||||
if (!src.getUserJava().isEmpty())
|
||||
jsonObject.add("userJava", context.serialize(src.getUserJava()));
|
||||
|
||||
if (!src.getDisabledJava().isEmpty())
|
||||
jsonObject.add("disabledJava", context.serialize(src.getDisabledJava()));
|
||||
|
||||
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
|
||||
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GlobalConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!(json instanceof JsonObject)) return null;
|
||||
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
GlobalConfig config = new GlobalConfig();
|
||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
|
||||
|
||||
JsonElement userJava = obj.get("userJava");
|
||||
if (userJava != null && userJava.isJsonArray()) {
|
||||
for (JsonElement element : userJava.getAsJsonArray()) {
|
||||
config.userJava.add(element.getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
JsonElement disabledJava = obj.get("disabledJava");
|
||||
if (disabledJava != null && disabledJava.isJsonArray()) {
|
||||
for (JsonElement element : disabledJava.getAsJsonArray()) {
|
||||
config.disabledJava.add(element.getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
|
||||
if (!knownFields.contains(entry.getKey())) {
|
||||
config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public enum JavaVersionType {
|
||||
DEFAULT, AUTO, VERSION, DETECTED, CUSTOM
|
||||
}
|
||||
@@ -43,5 +43,9 @@ public enum LauncherVisibility {
|
||||
/**
|
||||
* Hide the launcher and reopen it when game closes.
|
||||
*/
|
||||
HIDE_AND_REOPEN
|
||||
HIDE_AND_REOPEN;
|
||||
|
||||
public boolean isDaemon() {
|
||||
return this != CLOSE;
|
||||
}
|
||||
}
|
||||
@@ -208,6 +208,24 @@ public final class Profile implements Observable {
|
||||
Platform.runLater(observableHelper::invalidate);
|
||||
}
|
||||
|
||||
public static class ProfileVersion {
|
||||
private final Profile profile;
|
||||
private final String version;
|
||||
|
||||
public ProfileVersion(Profile profile, String version) {
|
||||
this.profile = profile;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public Profile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
|
||||
@Override
|
||||
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
|
||||
@@ -20,17 +20,18 @@ package org.jackhuang.hmcl.setting;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static javafx.collections.FXCollections.observableArrayList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
@@ -130,8 +131,11 @@ public final class Profiles {
|
||||
if (!initialized)
|
||||
return;
|
||||
// update storage
|
||||
config().getConfigurations().clear();
|
||||
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
|
||||
TreeMap<String, Profile> newConfigurations = new TreeMap<>();
|
||||
for (Profile profile : profiles) {
|
||||
newConfigurations.put(profile.getName(), profile);
|
||||
}
|
||||
config().getConfigurations().setValue(FXCollections.observableMap(newConfigurations));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,7 +208,7 @@ public final class Profiles {
|
||||
return selectedVersion.get();
|
||||
}
|
||||
|
||||
private static final List<Consumer<Profile>> versionsListeners = new LinkedList<>();
|
||||
private static final List<Consumer<Profile>> versionsListeners = new ArrayList<>(4);
|
||||
|
||||
public static void registerVersionsListener(Consumer<Profile> listener) {
|
||||
Profile profile = getSelectedProfile();
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.net.Proxy;
|
||||
import java.net.Proxy.Type;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ProxyManager {
|
||||
private ProxyManager() {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
public final class SambaException extends RuntimeException {
|
||||
public SambaException() {
|
||||
}
|
||||
|
||||
public SambaException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SambaException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public SambaException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,13 @@ package org.jackhuang.hmcl.setting;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class Settings {
|
||||
public final class Settings {
|
||||
|
||||
private static Settings instance;
|
||||
|
||||
@@ -46,14 +45,12 @@ public class Settings {
|
||||
}
|
||||
|
||||
private Settings() {
|
||||
config().localizationProperty().addListener(unused -> updateSystemLocale());
|
||||
updateSystemLocale();
|
||||
|
||||
DownloadProviders.init();
|
||||
ProxyManager.init();
|
||||
Accounts.init();
|
||||
Profiles.init();
|
||||
AuthlibInjectorServers.init();
|
||||
AnimationUtils.init();
|
||||
|
||||
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
|
||||
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
|
||||
@@ -79,8 +76,4 @@ public class Settings {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateSystemLocale() {
|
||||
Locale.setDefault(config().getLocalization().getLocale());
|
||||
}
|
||||
}
|
||||
@@ -23,25 +23,32 @@ import com.google.gson.stream.JsonWriter;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@JsonAdapter(Theme.TypeAdapter.class)
|
||||
public class Theme {
|
||||
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
|
||||
public static final Color BLACK = Color.web("#292929");
|
||||
public static final Color[] SUGGESTED_COLORS = new Color[]{
|
||||
Color.web("#5C6BC0"), // blue
|
||||
Color.web("#3D6DA3"), // blue
|
||||
Color.web("#283593"), // dark blue
|
||||
Color.web("#43A047"), // green
|
||||
Color.web("#E67E22"), // orange
|
||||
@@ -49,13 +56,42 @@ public class Theme {
|
||||
Color.web("#B71C1C") // red
|
||||
};
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private static Optional<Font> font;
|
||||
|
||||
private static Optional<Font> tryLoadFont() {
|
||||
//noinspection OptionalAssignedToNull
|
||||
if (font != null) {
|
||||
return font;
|
||||
}
|
||||
|
||||
Path path = Paths.get("font.ttf");
|
||||
if (!Files.isRegularFile(path)) {
|
||||
path = Paths.get("font.otf");
|
||||
}
|
||||
|
||||
if (Files.isRegularFile(path)) {
|
||||
try {
|
||||
return font = Optional.ofNullable(Font.loadFont(path.toAbsolutePath().toUri().toURL().toExternalForm(), 0));
|
||||
} catch (MalformedURLException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return font = Optional.empty();
|
||||
}
|
||||
|
||||
public static Theme getTheme() {
|
||||
Theme theme = config().getTheme();
|
||||
return theme == null ? BLUE : theme;
|
||||
}
|
||||
|
||||
private final Color paint;
|
||||
private final String color;
|
||||
private final String name;
|
||||
|
||||
Theme(String name, String color) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.color = Objects.requireNonNull(color);
|
||||
this.paint = Color.web(color);
|
||||
}
|
||||
|
||||
@@ -72,35 +108,80 @@ public class Theme {
|
||||
}
|
||||
|
||||
public boolean isLight() {
|
||||
return Color.web(color).grayscale().getRed() >= 0.5;
|
||||
return paint.grayscale().getRed() >= 0.5;
|
||||
}
|
||||
|
||||
public Color getForegroundColor() {
|
||||
return isLight() ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
|
||||
public String[] getStylesheets() {
|
||||
Color textFill = getForegroundColor();
|
||||
private static String rgba(Color color, double opacity) {
|
||||
return String.format("rgba(%d, %d, %d, %.1f)",
|
||||
(int) Math.ceil(color.getRed() * 256),
|
||||
(int) Math.ceil(color.getGreen() * 256),
|
||||
(int) Math.ceil(color.getBlue() * 256),
|
||||
opacity);
|
||||
}
|
||||
|
||||
String css;
|
||||
try {
|
||||
File temp = File.createTempFile("hmcl", ".css");
|
||||
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
|
||||
.replace("%base-color%", color)
|
||||
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int) Math.ceil(paint.getRed() * 256), (int) Math.ceil(paint.getGreen() * 256), (int) Math.ceil(paint.getBlue() * 256)))
|
||||
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
|
||||
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
|
||||
.replace("%font%", Optional.ofNullable(System.getProperty("hmcl.font.override")).map(fontFamily -> "-fx-font-family: " + fontFamily + ";").orElse("")));
|
||||
css = temp.toURI().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
|
||||
css = "/assets/css/blue.css";
|
||||
public String[] getStylesheets(String overrideFontFamily) {
|
||||
String css = "/assets/css/blue.css";
|
||||
|
||||
String fontFamily = overrideFontFamily == null
|
||||
? System.getProperty("hmcl.font.override", System.getenv("HMCL_FONT"))
|
||||
: overrideFontFamily;
|
||||
|
||||
String fontStyle = null;
|
||||
if (fontFamily == null) {
|
||||
Optional<Font> font = tryLoadFont();
|
||||
if (font.isPresent()) {
|
||||
fontFamily = font.get().getFamily();
|
||||
fontStyle = font.get().getStyle();
|
||||
}
|
||||
}
|
||||
|
||||
return new String[]{
|
||||
css,
|
||||
"/assets/css/root.css"
|
||||
};
|
||||
if (fontFamily != null || !this.color.equalsIgnoreCase(BLUE.color)) {
|
||||
Color textFill = getForegroundColor();
|
||||
|
||||
StringBuilder themeBuilder = new StringBuilder(512);
|
||||
themeBuilder.append(".root {")
|
||||
.append("-fx-base-color:").append(color).append(';')
|
||||
.append("-fx-base-darker-color: derive(-fx-base-color, -10%);")
|
||||
.append("-fx-base-check-color: derive(-fx-base-color, 30%);")
|
||||
.append("-fx-rippler-color:").append(rgba(paint, 0.3)).append(';')
|
||||
.append("-fx-base-rippler-color: derive(").append(rgba(paint, 0.3)).append(", 100%);")
|
||||
.append("-fx-base-disabled-text-fill:").append(rgba(textFill, 0.7)).append(";")
|
||||
.append("-fx-base-text-fill:").append(getColorDisplayName(getForegroundColor())).append(";")
|
||||
.append("-theme-thumb:").append(rgba(paint, 0.7)).append(";");
|
||||
|
||||
if (fontFamily == null)
|
||||
// https://github.com/HMCL-dev/HMCL/pull/3423
|
||||
themeBuilder.append("-fx-font-family: -fx-base-font-family;");
|
||||
else
|
||||
themeBuilder.append("-fx-font-family:\"").append(fontFamily).append("\";");
|
||||
|
||||
if (fontStyle != null && !fontStyle.isEmpty())
|
||||
themeBuilder.append("-fx-font-style:\"").append(fontStyle).append("\";");
|
||||
|
||||
themeBuilder.append('}');
|
||||
|
||||
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
|
||||
// JavaFX 17+ support loading stylesheets from data URIs
|
||||
// https://bugs.openjdk.org/browse/JDK-8267554
|
||||
css = "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(themeBuilder.toString().getBytes(StandardCharsets.UTF_8));
|
||||
else
|
||||
try {
|
||||
File temp = File.createTempFile("hmcl", ".css");
|
||||
// For JavaFX 17 or earlier, CssParser uses the default charset
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
FileUtils.writeText(temp, themeBuilder.toString(), Charset.defaultCharset());
|
||||
temp.deleteOnExit();
|
||||
css = temp.toURI().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
LOG.error("Unable to create theme stylesheet. Fallback to blue theme.", e);
|
||||
}
|
||||
}
|
||||
|
||||
return new String[]{css, "/assets/css/root.css"};
|
||||
}
|
||||
|
||||
public static Theme custom(String color) {
|
||||
@@ -112,50 +193,67 @@ public class Theme {
|
||||
public static Optional<Theme> getTheme(String name) {
|
||||
if (name == null)
|
||||
return Optional.empty();
|
||||
else if (name.equalsIgnoreCase("blue"))
|
||||
return Optional.of(custom("#5C6BC0"));
|
||||
else if (name.equalsIgnoreCase("darker_blue"))
|
||||
return Optional.of(custom("#283593"));
|
||||
else if (name.equalsIgnoreCase("green"))
|
||||
return Optional.of(custom("#43A047"));
|
||||
else if (name.equalsIgnoreCase("orange"))
|
||||
return Optional.of(custom("#E67E22"));
|
||||
else if (name.equalsIgnoreCase("purple"))
|
||||
return Optional.of(custom("#9C27B0"));
|
||||
else if (name.equalsIgnoreCase("red"))
|
||||
return Optional.of(custom("#F44336"));
|
||||
|
||||
if (name.startsWith("#"))
|
||||
else if (name.startsWith("#"))
|
||||
try {
|
||||
Color.web(name);
|
||||
return Optional.of(custom(name));
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
else {
|
||||
String color = null;
|
||||
switch (name.toLowerCase(Locale.ROOT)) {
|
||||
case "blue":
|
||||
return Optional.of(BLUE);
|
||||
case "darker_blue":
|
||||
color = "#283593";
|
||||
break;
|
||||
case "green":
|
||||
color = "#43A047";
|
||||
break;
|
||||
case "orange":
|
||||
color = "#E67E22";
|
||||
break;
|
||||
case "purple":
|
||||
color = "#9C27B0";
|
||||
break;
|
||||
case "red":
|
||||
color = "#F44336";
|
||||
}
|
||||
if (color != null)
|
||||
return Optional.of(new Theme(name, color));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static String getColorDisplayName(Color c) {
|
||||
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase() : null;
|
||||
return c != null ? String.format("#%02X%02X%02X", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)) : null;
|
||||
}
|
||||
|
||||
private static ObjectBinding<Color> FOREGROUND_FILL;
|
||||
|
||||
public static ObjectBinding<Color> foregroundFillBinding() {
|
||||
return BindingMapping.of(config().themeProperty())
|
||||
.map(Theme::getForegroundColor);
|
||||
if (FOREGROUND_FILL == null)
|
||||
FOREGROUND_FILL = Bindings.createObjectBinding(
|
||||
() -> Theme.getTheme().getForegroundColor(),
|
||||
config().themeProperty()
|
||||
);
|
||||
|
||||
return FOREGROUND_FILL;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> blackFillBinding() {
|
||||
return Bindings.createObjectBinding(() -> BLACK);
|
||||
public static Color blackFill() {
|
||||
return BLACK;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> whiteFillBinding() {
|
||||
return Bindings.createObjectBinding(() -> Color.WHITE);
|
||||
public static Color whiteFill() {
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
|
||||
@Override
|
||||
public void write(JsonWriter out, Theme value) throws IOException {
|
||||
out.value(value.getName().toLowerCase());
|
||||
out.value(value.getName().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public enum VersionIconType {
|
||||
DEFAULT("/assets/img/grass.png"),
|
||||
|
||||
GRASS("/assets/img/grass.png"),
|
||||
CHEST("/assets/img/chest.png"),
|
||||
CHICKEN("/assets/img/chicken.png"),
|
||||
COMMAND("/assets/img/command.png"),
|
||||
OPTIFINE("/assets/img/optifine.png"),
|
||||
CRAFT_TABLE("/assets/img/craft_table.png"),
|
||||
FABRIC("/assets/img/fabric.png"),
|
||||
FORGE("/assets/img/forge.png"),
|
||||
NEO_FORGE("/assets/img/neoforge.png"),
|
||||
FURNACE("/assets/img/furnace.png"),
|
||||
QUILT("/assets/img/quilt.png");
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
private final String resourceUrl;
|
||||
|
||||
VersionIconType(String resourceUrl) {
|
||||
this.resourceUrl = resourceUrl;
|
||||
}
|
||||
|
||||
public Image getIcon() {
|
||||
return FXUtils.newBuiltinImage(resourceUrl);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,30 +21,29 @@ import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@JsonAdapter(VersionSetting.Serializer.class)
|
||||
public final class VersionSetting implements Cloneable {
|
||||
|
||||
public transient String id;
|
||||
|
||||
private boolean global = false;
|
||||
|
||||
public boolean isGlobal() {
|
||||
@@ -55,7 +54,7 @@ public final class VersionSetting implements Cloneable {
|
||||
this.global = global;
|
||||
}
|
||||
|
||||
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", false);
|
||||
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", true);
|
||||
|
||||
public BooleanProperty usesGlobalProperty() {
|
||||
return usesGlobalProperty;
|
||||
@@ -66,7 +65,7 @@ public final class VersionSetting implements Cloneable {
|
||||
* 1. Global settings.
|
||||
* 2. Version settings.
|
||||
* If a version claims that it uses global settings, its version setting will be disabled.
|
||||
*
|
||||
* <p>
|
||||
* Defaults false because if one version uses global first, custom version file will not be generated.
|
||||
*/
|
||||
public boolean isUsesGlobal() {
|
||||
@@ -79,29 +78,43 @@ public final class VersionSetting implements Cloneable {
|
||||
|
||||
// java
|
||||
|
||||
private final StringProperty javaProperty = new SimpleStringProperty(this, "java", "");
|
||||
private final ObjectProperty<JavaVersionType> javaVersionTypeProperty = new SimpleObjectProperty<>(this, "javaVersionType", JavaVersionType.AUTO);
|
||||
|
||||
public StringProperty javaProperty() {
|
||||
return javaProperty;
|
||||
public ObjectProperty<JavaVersionType> javaVersionTypeProperty() {
|
||||
return javaVersionTypeProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
|
||||
*/
|
||||
public String getJava() {
|
||||
return javaProperty.get();
|
||||
public JavaVersionType getJavaVersionType() {
|
||||
return javaVersionTypeProperty.get();
|
||||
}
|
||||
|
||||
public void setJava(String java) {
|
||||
javaProperty.set(java);
|
||||
public void setJavaVersionType(JavaVersionType javaVersionType) {
|
||||
javaVersionTypeProperty.set(javaVersionType);
|
||||
}
|
||||
|
||||
public boolean isUsesCustomJavaDir() {
|
||||
return "Custom".equals(getJava());
|
||||
private final StringProperty javaVersionProperty = new SimpleStringProperty(this, "javaVersion", "");
|
||||
|
||||
public StringProperty javaVersionProperty() {
|
||||
return javaVersionProperty;
|
||||
}
|
||||
|
||||
public String getJavaVersion() {
|
||||
return javaVersionProperty.get();
|
||||
}
|
||||
|
||||
public void setJavaVersion(String java) {
|
||||
javaVersionProperty.set(java);
|
||||
}
|
||||
|
||||
public void setUsesCustomJavaDir() {
|
||||
setJava("Custom");
|
||||
setJavaVersionType(JavaVersionType.CUSTOM);
|
||||
setJavaVersion("");
|
||||
setDefaultJavaPath(null);
|
||||
}
|
||||
|
||||
public void setJavaAutoSelected() {
|
||||
setJavaVersionType(JavaVersionType.AUTO);
|
||||
setJavaVersion("");
|
||||
setDefaultJavaPath(null);
|
||||
}
|
||||
|
||||
@@ -115,10 +128,47 @@ public final class VersionSetting implements Cloneable {
|
||||
return defaultJavaPathProperty.get();
|
||||
}
|
||||
|
||||
public StringProperty defaultJavaPathPropertyProperty() {
|
||||
return defaultJavaPathProperty;
|
||||
}
|
||||
|
||||
public void setDefaultJavaPath(String defaultJavaPath) {
|
||||
defaultJavaPathProperty.set(defaultJavaPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 0 - .minecraft/versions/<version>/natives/<br/>
|
||||
*/
|
||||
private final ObjectProperty<NativesDirectoryType> nativesDirTypeProperty = new SimpleObjectProperty<>(this, "nativesDirType", NativesDirectoryType.VERSION_FOLDER);
|
||||
|
||||
public ObjectProperty<NativesDirectoryType> nativesDirTypeProperty() {
|
||||
return nativesDirTypeProperty;
|
||||
}
|
||||
|
||||
public NativesDirectoryType getNativesDirType() {
|
||||
return nativesDirTypeProperty.get();
|
||||
}
|
||||
|
||||
public void setNativesDirType(NativesDirectoryType nativesDirType) {
|
||||
nativesDirTypeProperty.set(nativesDirType);
|
||||
}
|
||||
|
||||
// Path to lwjgl natives directory
|
||||
|
||||
private final StringProperty nativesDirProperty = new SimpleStringProperty(this, "nativesDirProperty", "");
|
||||
|
||||
public StringProperty nativesDirProperty() {
|
||||
return nativesDirProperty;
|
||||
}
|
||||
|
||||
public String getNativesDir() {
|
||||
return nativesDirProperty.get();
|
||||
}
|
||||
|
||||
public void setNativesDir(String nativesDir) {
|
||||
nativesDirProperty.set(nativesDir);
|
||||
}
|
||||
|
||||
private final StringProperty javaDirProperty = new SimpleStringProperty(this, "javaDir", "");
|
||||
|
||||
public StringProperty javaDirProperty() {
|
||||
@@ -204,6 +254,20 @@ public final class VersionSetting implements Cloneable {
|
||||
minMemoryProperty.set(minMemory);
|
||||
}
|
||||
|
||||
private final BooleanProperty autoMemory = new SimpleBooleanProperty(this, "autoMemory", true);
|
||||
|
||||
public boolean isAutoMemory() {
|
||||
return autoMemory.get();
|
||||
}
|
||||
|
||||
public BooleanProperty autoMemoryProperty() {
|
||||
return autoMemory;
|
||||
}
|
||||
|
||||
public void setAutoMemory(boolean autoMemory) {
|
||||
this.autoMemory.set(autoMemory);
|
||||
}
|
||||
|
||||
private final StringProperty preLaunchCommandProperty = new SimpleStringProperty(this, "precalledCommand", "");
|
||||
|
||||
public StringProperty preLaunchCommandProperty() {
|
||||
@@ -222,6 +286,24 @@ public final class VersionSetting implements Cloneable {
|
||||
preLaunchCommandProperty.set(preLaunchCommand);
|
||||
}
|
||||
|
||||
private final StringProperty postExitCommand = new SimpleStringProperty(this, "postExitCommand", "");
|
||||
|
||||
public StringProperty postExitCommandProperty() {
|
||||
return postExitCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command that will be executed after game exits.
|
||||
* Operating system relevant.
|
||||
*/
|
||||
public String getPostExitCommand() {
|
||||
return postExitCommand.get();
|
||||
}
|
||||
|
||||
public void setPostExitCommand(String postExitCommand) {
|
||||
this.postExitCommand.set(postExitCommand);
|
||||
}
|
||||
|
||||
// options
|
||||
|
||||
private final StringProperty javaArgsProperty = new SimpleStringProperty(this, "javaArgs", "");
|
||||
@@ -258,6 +340,20 @@ public final class VersionSetting implements Cloneable {
|
||||
minecraftArgsProperty.set(minecraftArgs);
|
||||
}
|
||||
|
||||
private final StringProperty environmentVariablesProperty = new SimpleStringProperty(this, "environmentVariables", "");
|
||||
|
||||
public StringProperty environmentVariablesProperty() {
|
||||
return environmentVariablesProperty;
|
||||
}
|
||||
|
||||
public String getEnvironmentVariables() {
|
||||
return environmentVariablesProperty.get();
|
||||
}
|
||||
|
||||
public void setEnvironmentVariables(String env) {
|
||||
environmentVariablesProperty.set(env);
|
||||
}
|
||||
|
||||
private final BooleanProperty noJVMArgsProperty = new SimpleBooleanProperty(this, "noJVMArgs", false);
|
||||
|
||||
public BooleanProperty noJVMArgsProperty() {
|
||||
@@ -309,6 +405,20 @@ public final class VersionSetting implements Cloneable {
|
||||
notCheckGameProperty.set(notCheckGame);
|
||||
}
|
||||
|
||||
private final BooleanProperty notPatchNativesProperty = new SimpleBooleanProperty(this, "notPatchNatives", false);
|
||||
|
||||
public BooleanProperty notPatchNativesProperty() {
|
||||
return notPatchNativesProperty;
|
||||
}
|
||||
|
||||
public boolean isNotPatchNatives() {
|
||||
return notPatchNativesProperty.get();
|
||||
}
|
||||
|
||||
public void setNotPatchNatives(boolean notPatchNatives) {
|
||||
notPatchNativesProperty.set(notPatchNatives);
|
||||
}
|
||||
|
||||
private final BooleanProperty showLogsProperty = new SimpleBooleanProperty(this, "showLogs", false);
|
||||
|
||||
public BooleanProperty showLogsProperty() {
|
||||
@@ -336,7 +446,7 @@ public final class VersionSetting implements Cloneable {
|
||||
|
||||
/**
|
||||
* The server ip that will be entered after Minecraft successfully loaded ly.
|
||||
*
|
||||
* <p>
|
||||
* Format: ip:port or without port.
|
||||
*/
|
||||
public String getServerIp() {
|
||||
@@ -373,7 +483,7 @@ public final class VersionSetting implements Cloneable {
|
||||
|
||||
/**
|
||||
* The width of Minecraft window, defaults 800.
|
||||
*
|
||||
* <p>
|
||||
* The field saves int value.
|
||||
* String type prevents unexpected value from JsonParseException.
|
||||
* We can only reset this field instead of recreating the whole setting file.
|
||||
@@ -386,7 +496,6 @@ public final class VersionSetting implements Cloneable {
|
||||
widthProperty.set(width);
|
||||
}
|
||||
|
||||
|
||||
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
|
||||
|
||||
public IntegerProperty heightProperty() {
|
||||
@@ -395,7 +504,7 @@ public final class VersionSetting implements Cloneable {
|
||||
|
||||
/**
|
||||
* The height of Minecraft window, defaults 480.
|
||||
*
|
||||
* <p>
|
||||
* The field saves int value.
|
||||
* String type prevents unexpected value from JsonParseException.
|
||||
* We can only reset this field instead of recreating the whole setting file.
|
||||
@@ -443,6 +552,76 @@ public final class VersionSetting implements Cloneable {
|
||||
gameDirProperty.set(gameDir);
|
||||
}
|
||||
|
||||
private final ObjectProperty<ProcessPriority> processPriorityProperty = new SimpleObjectProperty<>(this, "processPriority", ProcessPriority.NORMAL);
|
||||
|
||||
public ObjectProperty<ProcessPriority> processPriorityProperty() {
|
||||
return processPriorityProperty;
|
||||
}
|
||||
|
||||
public ProcessPriority getProcessPriority() {
|
||||
return processPriorityProperty.get();
|
||||
}
|
||||
|
||||
public void setProcessPriority(ProcessPriority processPriority) {
|
||||
processPriorityProperty.set(processPriority);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
|
||||
|
||||
public Renderer getRenderer() {
|
||||
return rendererProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Renderer> rendererProperty() {
|
||||
return rendererProperty;
|
||||
}
|
||||
|
||||
public void setRenderer(Renderer renderer) {
|
||||
this.rendererProperty.set(renderer);
|
||||
}
|
||||
|
||||
private final BooleanProperty useNativeGLFW = new SimpleBooleanProperty(this, "nativeGLFW", false);
|
||||
|
||||
public boolean isUseNativeGLFW() {
|
||||
return useNativeGLFW.get();
|
||||
}
|
||||
|
||||
public BooleanProperty useNativeGLFWProperty() {
|
||||
return useNativeGLFW;
|
||||
}
|
||||
|
||||
public void setUseNativeGLFW(boolean useNativeGLFW) {
|
||||
this.useNativeGLFW.set(useNativeGLFW);
|
||||
}
|
||||
|
||||
private final BooleanProperty useNativeOpenAL = new SimpleBooleanProperty(this, "nativeOpenAL", false);
|
||||
|
||||
public boolean isUseNativeOpenAL() {
|
||||
return useNativeOpenAL.get();
|
||||
}
|
||||
|
||||
public BooleanProperty useNativeOpenALProperty() {
|
||||
return useNativeOpenAL;
|
||||
}
|
||||
|
||||
public void setUseNativeOpenAL(boolean useNativeOpenAL) {
|
||||
this.useNativeOpenAL.set(useNativeOpenAL);
|
||||
}
|
||||
|
||||
private final ObjectProperty<VersionIconType> versionIcon = new SimpleObjectProperty<>(this, "versionIcon", VersionIconType.DEFAULT);
|
||||
|
||||
public VersionIconType getVersionIcon() {
|
||||
return versionIcon.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<VersionIconType> versionIconProperty() {
|
||||
return versionIcon;
|
||||
}
|
||||
|
||||
public void setVersionIcon(VersionIconType versionIcon) {
|
||||
this.versionIcon.set(versionIcon);
|
||||
}
|
||||
|
||||
// launcher settings
|
||||
|
||||
/**
|
||||
@@ -464,59 +643,89 @@ public final class VersionSetting implements Cloneable {
|
||||
launcherVisibilityProperty.set(launcherVisibility);
|
||||
}
|
||||
|
||||
public JavaVersion getJavaVersion() throws InterruptedException {
|
||||
return getJavaVersion(true);
|
||||
}
|
||||
public JavaRuntime getJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
|
||||
switch (getJavaVersionType()) {
|
||||
case DEFAULT:
|
||||
return JavaRuntime.getDefault();
|
||||
case AUTO:
|
||||
return JavaManager.findSuitableJava(gameVersion, version);
|
||||
case CUSTOM:
|
||||
try {
|
||||
return JavaManager.getJava(Paths.get(getJavaDir()));
|
||||
} catch (IOException | InvalidPathException e) {
|
||||
return null; // Custom Java not found
|
||||
}
|
||||
case VERSION: {
|
||||
String javaVersion = getJavaVersion();
|
||||
if (StringUtils.isBlank(javaVersion)) {
|
||||
return JavaManager.findSuitableJava(gameVersion, version);
|
||||
}
|
||||
|
||||
public JavaVersion getJavaVersion(boolean checkJava) throws InterruptedException {
|
||||
// TODO: lazy initialization may result in UI suspension.
|
||||
if (StringUtils.isBlank(getJava()))
|
||||
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
|
||||
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
|
||||
else if (isUsesCustomJavaDir()) {
|
||||
try {
|
||||
if (checkJava)
|
||||
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
||||
else
|
||||
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.PLATFORM);
|
||||
} catch (IOException | InvalidPathException e) {
|
||||
return null; // Custom Java Directory not found,
|
||||
}
|
||||
} else if (StringUtils.isNotBlank(getJava())) {
|
||||
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
|
||||
.filter(java -> java.getVersion().equals(getJava()))
|
||||
.collect(Collectors.toList());
|
||||
if (matchedJava.isEmpty()) {
|
||||
setJava("Default");
|
||||
return JavaVersion.fromCurrentEnvironment();
|
||||
} else {
|
||||
return matchedJava.stream()
|
||||
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
|
||||
.findFirst()
|
||||
.orElse(matchedJava.get(0));
|
||||
}
|
||||
} else throw new Error();
|
||||
}
|
||||
int majorVersion = -1;
|
||||
try {
|
||||
majorVersion = Integer.parseInt(javaVersion);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
public void setJavaVersion(JavaVersion java) {
|
||||
setJava(java.getVersion());
|
||||
setDefaultJavaPath(java.getBinary().toString());
|
||||
if (majorVersion < 0) {
|
||||
LOG.warning("Invalid Java version: " + javaVersion);
|
||||
return null;
|
||||
}
|
||||
|
||||
final int finalMajorVersion = majorVersion;
|
||||
Collection<JavaRuntime> allJava = JavaManager.getAllJava().stream()
|
||||
.filter(it -> it.getParsedVersion() == finalMajorVersion)
|
||||
.collect(Collectors.toList());
|
||||
return JavaManager.findSuitableJava(allJava, gameVersion, version);
|
||||
}
|
||||
case DETECTED: {
|
||||
String javaVersion = getJavaVersion();
|
||||
if (StringUtils.isBlank(javaVersion)) {
|
||||
return JavaManager.findSuitableJava(gameVersion, version);
|
||||
}
|
||||
|
||||
try {
|
||||
String defaultJavaPath = getDefaultJavaPath();
|
||||
if (StringUtils.isNotBlank(defaultJavaPath)) {
|
||||
JavaRuntime java = JavaManager.getJava(Paths.get(defaultJavaPath).toRealPath());
|
||||
if (java != null && java.getVersion().equals(javaVersion)) {
|
||||
return java;
|
||||
}
|
||||
}
|
||||
} catch (IOException | InvalidPathException ignored) {
|
||||
}
|
||||
|
||||
for (JavaRuntime java : JavaManager.getAllJava()) {
|
||||
if (java.getVersion().equals(javaVersion)) {
|
||||
return java;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
throw new AssertionError("JavaVersionType: " + getJavaVersionType());
|
||||
}
|
||||
}
|
||||
|
||||
public void addPropertyChangedListener(InvalidationListener listener) {
|
||||
usesGlobalProperty.addListener(listener);
|
||||
javaProperty.addListener(listener);
|
||||
javaVersionProperty.addListener(listener);
|
||||
javaDirProperty.addListener(listener);
|
||||
wrapperProperty.addListener(listener);
|
||||
permSizeProperty.addListener(listener);
|
||||
maxMemoryProperty.addListener(listener);
|
||||
minMemoryProperty.addListener(listener);
|
||||
autoMemory.addListener(listener);
|
||||
preLaunchCommandProperty.addListener(listener);
|
||||
postExitCommand.addListener(listener);
|
||||
javaArgsProperty.addListener(listener);
|
||||
minecraftArgsProperty.addListener(listener);
|
||||
environmentVariablesProperty.addListener(listener);
|
||||
noJVMArgsProperty.addListener(listener);
|
||||
notCheckGameProperty.addListener(listener);
|
||||
notCheckJVMProperty.addListener(listener);
|
||||
notPatchNativesProperty.addListener(listener);
|
||||
showLogsProperty.addListener(listener);
|
||||
serverIpProperty.addListener(listener);
|
||||
fullscreenProperty.addListener(listener);
|
||||
@@ -524,27 +733,39 @@ public final class VersionSetting implements Cloneable {
|
||||
heightProperty.addListener(listener);
|
||||
gameDirTypeProperty.addListener(listener);
|
||||
gameDirProperty.addListener(listener);
|
||||
processPriorityProperty.addListener(listener);
|
||||
rendererProperty.addListener(listener);
|
||||
useNativeGLFW.addListener(listener);
|
||||
useNativeOpenAL.addListener(listener);
|
||||
launcherVisibilityProperty.addListener(listener);
|
||||
defaultJavaPathProperty.addListener(listener);
|
||||
nativesDirProperty.addListener(listener);
|
||||
nativesDirTypeProperty.addListener(listener);
|
||||
versionIcon.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionSetting clone() {
|
||||
VersionSetting versionSetting = new VersionSetting();
|
||||
versionSetting.setUsesGlobal(isUsesGlobal());
|
||||
versionSetting.setJava(getJava());
|
||||
versionSetting.setJavaVersionType(getJavaVersionType());
|
||||
versionSetting.setJavaVersion(getJavaVersion());
|
||||
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
|
||||
versionSetting.setJavaDir(getJavaDir());
|
||||
versionSetting.setWrapper(getWrapper());
|
||||
versionSetting.setPermSize(getPermSize());
|
||||
versionSetting.setMaxMemory(getMaxMemory());
|
||||
versionSetting.setMinMemory(getMinMemory());
|
||||
versionSetting.setAutoMemory(isAutoMemory());
|
||||
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
|
||||
versionSetting.setPostExitCommand(getPostExitCommand());
|
||||
versionSetting.setJavaArgs(getJavaArgs());
|
||||
versionSetting.setMinecraftArgs(getMinecraftArgs());
|
||||
versionSetting.setEnvironmentVariables(getEnvironmentVariables());
|
||||
versionSetting.setNoJVMArgs(isNoJVMArgs());
|
||||
versionSetting.setNotCheckGame(isNotCheckGame());
|
||||
versionSetting.setNotCheckJVM(isNotCheckJVM());
|
||||
versionSetting.setNotPatchNatives(isNotPatchNatives());
|
||||
versionSetting.setShowLogs(isShowLogs());
|
||||
versionSetting.setServerIp(getServerIp());
|
||||
versionSetting.setFullscreen(isFullscreen());
|
||||
@@ -552,7 +773,13 @@ public final class VersionSetting implements Cloneable {
|
||||
versionSetting.setHeight(getHeight());
|
||||
versionSetting.setGameDirType(getGameDirType());
|
||||
versionSetting.setGameDir(getGameDir());
|
||||
versionSetting.setProcessPriority(getProcessPriority());
|
||||
versionSetting.setRenderer(getRenderer());
|
||||
versionSetting.setUseNativeGLFW(isUseNativeGLFW());
|
||||
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
|
||||
versionSetting.setLauncherVisibility(getLauncherVisibility());
|
||||
versionSetting.setNativesDir(getNativesDir());
|
||||
versionSetting.setVersionIcon(getVersionIcon());
|
||||
return versionSetting;
|
||||
}
|
||||
|
||||
@@ -565,32 +792,71 @@ public final class VersionSetting implements Cloneable {
|
||||
obj.addProperty("usesGlobal", src.isUsesGlobal());
|
||||
obj.addProperty("javaArgs", src.getJavaArgs());
|
||||
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
|
||||
obj.addProperty("environmentVariables", src.getEnvironmentVariables());
|
||||
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? OperatingSystem.SUGGESTED_MEMORY : src.getMaxMemory());
|
||||
obj.addProperty("minMemory", src.getMinMemory());
|
||||
obj.addProperty("autoMemory", src.isAutoMemory());
|
||||
obj.addProperty("permSize", src.getPermSize());
|
||||
obj.addProperty("width", src.getWidth());
|
||||
obj.addProperty("height", src.getHeight());
|
||||
obj.addProperty("javaDir", src.getJavaDir());
|
||||
obj.addProperty("precalledCommand", src.getPreLaunchCommand());
|
||||
obj.addProperty("postExitCommand", src.getPostExitCommand());
|
||||
obj.addProperty("serverIp", src.getServerIp());
|
||||
obj.addProperty("java", src.getJava());
|
||||
obj.addProperty("wrapper", src.getWrapper());
|
||||
obj.addProperty("fullscreen", src.isFullscreen());
|
||||
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
|
||||
obj.addProperty("notCheckGame", src.isNotCheckGame());
|
||||
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
|
||||
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
|
||||
obj.addProperty("showLogs", src.isShowLogs());
|
||||
obj.addProperty("gameDir", src.getGameDir());
|
||||
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
|
||||
obj.addProperty("processPriority", src.getProcessPriority().ordinal());
|
||||
obj.addProperty("useNativeGLFW", src.isUseNativeGLFW());
|
||||
obj.addProperty("useNativeOpenAL", src.isUseNativeOpenAL());
|
||||
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
|
||||
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
|
||||
obj.addProperty("nativesDir", src.getNativesDir());
|
||||
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
|
||||
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
|
||||
|
||||
obj.addProperty("javaVersionType", src.getJavaVersionType().name());
|
||||
String java;
|
||||
switch (src.getJavaVersionType()) {
|
||||
case DEFAULT:
|
||||
java = "Default";
|
||||
break;
|
||||
case AUTO:
|
||||
java = "Auto";
|
||||
break;
|
||||
case CUSTOM:
|
||||
java = "Custom";
|
||||
break;
|
||||
default:
|
||||
java = src.getJavaVersion();
|
||||
break;
|
||||
}
|
||||
obj.addProperty("java", java);
|
||||
|
||||
obj.addProperty("renderer", src.getRenderer().name());
|
||||
if (src.getRenderer() == Renderer.LLVMPIPE)
|
||||
obj.addProperty("useSoftwareRenderer", true);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
|
||||
if (index == null)
|
||||
return defaultValue;
|
||||
|
||||
int idx = index.getAsInt();
|
||||
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject))
|
||||
if (!(json instanceof JsonObject))
|
||||
return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
@@ -602,25 +868,67 @@ public final class VersionSetting implements Cloneable {
|
||||
vs.setUsesGlobal(Optional.ofNullable(obj.get("usesGlobal")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setJavaArgs(Optional.ofNullable(obj.get("javaArgs")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setEnvironmentVariables(Optional.ofNullable(obj.get("environmentVariables")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setMaxMemory(maxMemoryN);
|
||||
vs.setMinMemory(Optional.ofNullable(obj.get("minMemory")).map(JsonElement::getAsInt).orElse(null));
|
||||
vs.setAutoMemory(Optional.ofNullable(obj.get("autoMemory")).map(JsonElement::getAsBoolean).orElse(true));
|
||||
vs.setPermSize(Optional.ofNullable(obj.get("permSize")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setWidth(Optional.ofNullable(obj.get("width")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
|
||||
vs.setHeight(Optional.ofNullable(obj.get("height")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
|
||||
vs.setJavaDir(Optional.ofNullable(obj.get("javaDir")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setPreLaunchCommand(Optional.ofNullable(obj.get("precalledCommand")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setPostExitCommand(Optional.ofNullable(obj.get("postExitCommand")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
|
||||
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
|
||||
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
|
||||
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
|
||||
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
|
||||
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
|
||||
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
|
||||
|
||||
if (obj.get("javaVersionType") != null) {
|
||||
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
|
||||
vs.setJavaVersionType(javaVersionType);
|
||||
vs.setJavaVersion(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(null));
|
||||
} else {
|
||||
String java = Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse("");
|
||||
switch (java) {
|
||||
case "Default":
|
||||
vs.setJavaVersionType(JavaVersionType.DEFAULT);
|
||||
break;
|
||||
case "Auto":
|
||||
vs.setJavaVersionType(JavaVersionType.AUTO);
|
||||
break;
|
||||
case "Custom":
|
||||
vs.setJavaVersionType(JavaVersionType.CUSTOM);
|
||||
break;
|
||||
default:
|
||||
vs.setJavaVersion(java);
|
||||
}
|
||||
}
|
||||
|
||||
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
|
||||
.flatMap(name -> {
|
||||
try {
|
||||
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}).orElseGet(() -> {
|
||||
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
|
||||
}));
|
||||
|
||||
return vs;
|
||||
}
|
||||
@@ -637,5 +945,25 @@ public final class VersionSetting implements Cloneable {
|
||||
else
|
||||
return Lang.parseInt(primitive.getAsString(), defaultValue);
|
||||
}
|
||||
|
||||
private <E extends Enum<E>> E parseJsonPrimitive(JsonPrimitive primitive, Class<E> clazz, E defaultValue) {
|
||||
if (primitive == null)
|
||||
return defaultValue;
|
||||
else {
|
||||
E[] enumConstants = clazz.getEnumConstants();
|
||||
if (primitive.isNumber()) {
|
||||
int index = primitive.getAsInt();
|
||||
return index >= 0 && index < enumConstants.length ? enumConstants[index] : defaultValue;
|
||||
} else {
|
||||
String name = primitive.getAsString();
|
||||
for (E enumConstant : enumConstants) {
|
||||
if (enumConstant.name().equalsIgnoreCase(name)) {
|
||||
return enumConstant;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,14 @@ package org.jackhuang.hmcl.ui;
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class AwtUtils {
|
||||
public final class AwtUtils {
|
||||
private AwtUtils() {
|
||||
}
|
||||
|
||||
public static void setAppleIcon(Image image) {
|
||||
try {
|
||||
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskBar");
|
||||
Class<?> taskbarClass = Class.forName("java.awt.Taskbar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskbar");
|
||||
Object taskBar = getTaskBarMethod.invoke(null);
|
||||
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
|
||||
setIconImageMethod.invoke(taskBar, image);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,54 +17,90 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.java.JavaRepository;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPage;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
|
||||
import org.jackhuang.hmcl.ui.download.DownloadPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Controllers {
|
||||
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
|
||||
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
|
||||
public static final Screen SCREEN = Screen.getPrimary();
|
||||
private static InvalidationListener stageSizeChangeListener;
|
||||
private static DoubleProperty stageX = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageY = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
|
||||
|
||||
private static Scene scene;
|
||||
private static Stage stage;
|
||||
private static VersionPage versionPage = null;
|
||||
private static AuthlibInjectorServersPage serversPage = null;
|
||||
private static RootPage rootPage;
|
||||
private static Lazy<VersionPage> versionPage = new Lazy<>(VersionPage::new);
|
||||
private static Lazy<GameListPage> gameListPage = new Lazy<>(() -> {
|
||||
GameListPage gameListPage = new GameListPage();
|
||||
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
|
||||
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
|
||||
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
|
||||
File modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
|
||||
});
|
||||
return gameListPage;
|
||||
});
|
||||
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
|
||||
private static DecoratorController decorator;
|
||||
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
|
||||
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
||||
AccountListPage accountListPage = new AccountListPage();
|
||||
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
|
||||
accountListPage.accountsProperty().bindContent(Accounts.getAccounts());
|
||||
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
|
||||
return accountListPage;
|
||||
});
|
||||
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
|
||||
|
||||
private Controllers() {
|
||||
}
|
||||
|
||||
public static Scene getScene() {
|
||||
return scene;
|
||||
@@ -76,23 +112,32 @@ public final class Controllers {
|
||||
|
||||
// FXThread
|
||||
public static VersionPage getVersionPage() {
|
||||
if (versionPage == null)
|
||||
versionPage = new VersionPage();
|
||||
return versionPage;
|
||||
return versionPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static GameListPage getGameListPage() {
|
||||
return gameListPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static RootPage getRootPage() {
|
||||
if (rootPage == null)
|
||||
rootPage = new RootPage();
|
||||
return rootPage;
|
||||
return rootPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static AuthlibInjectorServersPage getServersPage() {
|
||||
if (serversPage == null)
|
||||
serversPage = new AuthlibInjectorServersPage();
|
||||
return serversPage;
|
||||
public static LauncherSettingsPage getSettingsPage() {
|
||||
return settingsPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static AccountListPage getAccountListPage() {
|
||||
return accountListPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static DownloadPage getDownloadPage() {
|
||||
return downloadPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
@@ -101,20 +146,101 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void onApplicationStop() {
|
||||
config().setHeight(stageHeight.get());
|
||||
config().setWidth(stageWidth.get());
|
||||
stageSizeChangeListener = null;
|
||||
if (stageX != null) {
|
||||
config().setX(stageX.get() / SCREEN.getBounds().getWidth());
|
||||
stageX = null;
|
||||
}
|
||||
if (stageY != null) {
|
||||
config().setY(stageY.get() / SCREEN.getBounds().getHeight());
|
||||
stageY = null;
|
||||
}
|
||||
if (stageHeight != null) {
|
||||
config().setHeight(stageHeight.get());
|
||||
stageHeight = null;
|
||||
}
|
||||
if (stageWidth != null) {
|
||||
config().setWidth(stageWidth.get());
|
||||
stageWidth = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize(Stage stage) {
|
||||
Logging.LOG.info("Start initializing application");
|
||||
LOG.info("Start initializing application");
|
||||
|
||||
Controllers.stage = stage;
|
||||
MicrosoftAccountLoginStage.INSTANCE.initOwner(stage);
|
||||
|
||||
stage.setHeight(config().getHeight());
|
||||
stageHeight.bind(stage.heightProperty());
|
||||
stage.setWidth(config().getWidth());
|
||||
stageWidth.bind(stage.widthProperty());
|
||||
stageSizeChangeListener = o -> {
|
||||
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
|
||||
DoubleProperty targetProperty;
|
||||
switch (sourceProperty.getName()) {
|
||||
case "x": {
|
||||
targetProperty = stageX;
|
||||
break;
|
||||
}
|
||||
case "y": {
|
||||
targetProperty = stageY;
|
||||
break;
|
||||
}
|
||||
case "width": {
|
||||
targetProperty = stageWidth;
|
||||
break;
|
||||
}
|
||||
case "height": {
|
||||
targetProperty = stageHeight;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
targetProperty = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetProperty != null
|
||||
&& Controllers.stage != null
|
||||
&& !Controllers.stage.isIconified()) {
|
||||
targetProperty.set(sourceProperty.get());
|
||||
}
|
||||
};
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
|
||||
|
||||
double initWidth = Math.max(MIN_WIDTH, config().getWidth());
|
||||
double initHeight = Math.max(MIN_HEIGHT, config().getHeight());
|
||||
|
||||
{
|
||||
double initX = config().getX() * SCREEN.getBounds().getWidth();
|
||||
double initY = config().getY() * SCREEN.getBounds().getHeight();
|
||||
|
||||
boolean invalid = true;
|
||||
double border = 20D;
|
||||
for (Screen screen : Screen.getScreens()) {
|
||||
Rectangle2D bound = screen.getBounds();
|
||||
|
||||
if (bound.getMinX() + border <= initX + initWidth && initX <= bound.getMaxX() - border && bound.getMinY() + border <= initY && initY <= bound.getMaxY() - border) {
|
||||
invalid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalid) {
|
||||
initX = (0.5D - initWidth / SCREEN.getBounds().getWidth() / 2) * SCREEN.getBounds().getWidth();
|
||||
initY = (0.5D - initHeight / SCREEN.getBounds().getHeight() / 2) * SCREEN.getBounds().getHeight();
|
||||
}
|
||||
|
||||
stage.setX(initX);
|
||||
stage.setY(initY);
|
||||
stageX.set(initX);
|
||||
stageY.set(initY);
|
||||
}
|
||||
|
||||
stage.setHeight(initHeight);
|
||||
stage.setWidth(initWidth);
|
||||
stageHeight.set(initHeight);
|
||||
stageWidth.set(initWidth);
|
||||
stage.xProperty().addListener(weakListener);
|
||||
stage.yProperty().addListener(weakListener);
|
||||
stage.heightProperty().addListener(weakListener);
|
||||
stage.widthProperty().addListener(weakListener);
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
@@ -126,20 +252,57 @@ public final class Controllers {
|
||||
dialog(i18n("launcher.cache_directory.invalid"));
|
||||
}
|
||||
|
||||
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
|
||||
Lang.thread(JavaManager::initialize, "Search Java", true);
|
||||
|
||||
scene = new Scene(decorator.getDecorator());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
stage.setMinHeight(482 + 32);
|
||||
stage.setMinWidth(802 + 32);
|
||||
stage.setMinWidth(MIN_WIDTH);
|
||||
stage.setMinHeight(MIN_HEIGHT);
|
||||
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
|
||||
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
||||
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
|
||||
scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
|
||||
stage.getIcons().add(newImage("/assets/img/icon.png"));
|
||||
stage.setTitle(Metadata.TITLE);
|
||||
FXUtils.setIcon(stage);
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
|
||||
if (!Architecture.SYSTEM_ARCH.isX86() && globalConfig().getPlatformPromptVersion() < 1) {
|
||||
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.osx_arm64"), null, MessageType.INFO, continueAction);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.windows_arm64"), null, MessageType.INFO, continueAction);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX &&
|
||||
(Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64
|
||||
|| Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW
|
||||
|| Architecture.SYSTEM_ARCH == Architecture.MIPS64EL)) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.loongarch"), null, MessageType.INFO, continueAction);
|
||||
} else {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform"), null, MessageType.WARNING, continueAction);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (globalConfig().getAgreementVersion() < 1) {
|
||||
JFXDialogLayout agreementPane = new JFXDialogLayout();
|
||||
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
|
||||
agreementPane.setBody(new Label(i18n("launcher.agreement.hint")));
|
||||
JFXHyperlink agreementLink = new JFXHyperlink(i18n("launcher.agreement"));
|
||||
agreementLink.setExternalLink(Metadata.EULA_URL);
|
||||
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
|
||||
yesButton.getStyleClass().add("dialog-accept");
|
||||
yesButton.setOnAction(e -> {
|
||||
globalConfig().setAgreementVersion(1);
|
||||
agreementPane.fireEvent(new DialogCloseEvent());
|
||||
});
|
||||
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
|
||||
noButton.getStyleClass().add("dialog-cancel");
|
||||
noButton.setOnAction(e -> javafx.application.Platform.exit());
|
||||
agreementPane.setActions(agreementLink, yesButton, noButton);
|
||||
Controllers.dialog(agreementPane);
|
||||
}
|
||||
}
|
||||
|
||||
public static void dialog(Region content) {
|
||||
@@ -152,19 +315,31 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void dialog(String text, String title) {
|
||||
dialog(text, title, MessageType.INFORMATION);
|
||||
dialog(text, title, MessageType.INFO);
|
||||
}
|
||||
|
||||
public static void dialog(String text, String title, MessageType type) {
|
||||
dialog(text, title, type, null);
|
||||
}
|
||||
|
||||
public static void dialog(String text, String title, MessageType type, Runnable onAccept) {
|
||||
dialog(new MessageDialogPane(text, title, type, onAccept));
|
||||
public static void dialog(String text, String title, MessageType type, Runnable ok) {
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).ok(ok).build());
|
||||
}
|
||||
|
||||
public static void confirm(String text, String title, Runnable onAccept, Runnable onCancel) {
|
||||
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
|
||||
public static void confirm(String text, String title, Runnable yes, Runnable no) {
|
||||
confirm(text, title, MessageType.QUESTION, yes, no);
|
||||
}
|
||||
|
||||
public static void confirm(String text, String title, MessageType type, Runnable yes, Runnable no) {
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).yesOrNo(yes, no).build());
|
||||
}
|
||||
|
||||
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton) {
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, null).build());
|
||||
}
|
||||
|
||||
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton, Runnable cancel) {
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, cancel).build());
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
|
||||
@@ -183,11 +358,7 @@ public final class Controllers {
|
||||
return pane.getCompletableFuture();
|
||||
}
|
||||
|
||||
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title) {
|
||||
return taskDialog(executor, title, null);
|
||||
}
|
||||
|
||||
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, Consumer<Region> onCancel) {
|
||||
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel) {
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
|
||||
pane.setTitle(title);
|
||||
pane.setExecutor(executor);
|
||||
@@ -195,8 +366,35 @@ public final class Controllers {
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, TaskCancellationAction onCancel) {
|
||||
TaskExecutor executor = task.executor();
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
|
||||
pane.setTitle(title);
|
||||
pane.setExecutor(executor);
|
||||
dialog(pane);
|
||||
executor.start();
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static void navigate(Node node) {
|
||||
decorator.getNavigator().navigate(node, ContainerAnimations.FADE.getAnimationProducer());
|
||||
decorator.navigate(node);
|
||||
}
|
||||
|
||||
public static void showToast(String content) {
|
||||
decorator.showToast(content);
|
||||
}
|
||||
|
||||
public static void onHyperlinkAction(String href) {
|
||||
if (href.startsWith("hmcl://")) {
|
||||
switch (href) {
|
||||
case "hmcl://settings/feedback":
|
||||
Controllers.getSettingsPage().showFeedback();
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
FXUtils.openLink(href);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isStopped() {
|
||||
@@ -206,9 +404,15 @@ public final class Controllers {
|
||||
public static void shutdown() {
|
||||
rootPage = null;
|
||||
versionPage = null;
|
||||
serversPage = null;
|
||||
gameListPage = null;
|
||||
downloadPage = null;
|
||||
accountListPage = null;
|
||||
settingsPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
onApplicationStop();
|
||||
|
||||
FXUtils.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,9 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.countly.CrashReport;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
@@ -37,21 +37,23 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
*/
|
||||
public class CrashWindow extends Stage {
|
||||
|
||||
public CrashWindow(String text) {
|
||||
public CrashWindow(CrashReport report) {
|
||||
Label lblCrash = new Label();
|
||||
if (UpdateChecker.isOutdated())
|
||||
lblCrash.setText(i18n("launcher.crash_out_dated"));
|
||||
if (report.getThrowable() instanceof InternalError)
|
||||
lblCrash.setText(i18n("launcher.crash.java_internal_error"));
|
||||
else if (UpdateChecker.isOutdated())
|
||||
lblCrash.setText(i18n("launcher.crash.hmcl_out_dated"));
|
||||
else
|
||||
lblCrash.setText(i18n("launcher.crash"));
|
||||
lblCrash.setWrapText(true);
|
||||
|
||||
TextArea textArea = new TextArea();
|
||||
textArea.setText(text);
|
||||
textArea.setText(report.getDisplayText());
|
||||
textArea.setEditable(false);
|
||||
|
||||
Button btnContact = new Button();
|
||||
btnContact.setText(i18n("launcher.contact"));
|
||||
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
btnContact.setOnAction(event -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
HBox box = new HBox();
|
||||
box.setStyle("-fx-padding: 8px;");
|
||||
box.getChildren().add(btnContact);
|
||||
@@ -67,10 +69,10 @@ public class CrashWindow extends Stage {
|
||||
|
||||
Scene scene = new Scene(pane, 800, 480);
|
||||
setScene(scene);
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
FXUtils.setIcon(this);
|
||||
setTitle(i18n("message.error"));
|
||||
|
||||
setOnCloseRequest(e -> System.exit(1));
|
||||
setOnCloseRequest(e -> javafx.application.Platform.exit());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,11 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.ui.account.AccountLoginPane;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
|
||||
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -31,15 +29,29 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
|
||||
public final class DialogController {
|
||||
private DialogController() {
|
||||
}
|
||||
|
||||
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
if (account instanceof ClassicAccount) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
||||
runInFX(() -> {
|
||||
AccountLoginPane pane = new AccountLoginPane(account, it -> {
|
||||
res.set(it);
|
||||
latch.countDown();
|
||||
ClassicAccountLoginDialog pane = new ClassicAccountLoginDialog((ClassicAccount) account, it -> {
|
||||
res.set(it);
|
||||
latch.countDown();
|
||||
}, latch::countDown);
|
||||
Controllers.dialog(pane);
|
||||
});
|
||||
latch.await();
|
||||
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
|
||||
} else if (account instanceof OAuthAccount) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
||||
runInFX(() -> {
|
||||
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
|
||||
res.set(it);
|
||||
latch.countDown();
|
||||
}, latch::countDown);
|
||||
Controllers.dialog(pane);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -18,61 +18,117 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.beans.value.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.glavo.png.PNGType;
|
||||
import org.glavo.png.PNGWriter;
|
||||
import org.glavo.png.javafx.PNGJavaFXUtils;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.*;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class FXUtils {
|
||||
private FXUtils() {
|
||||
}
|
||||
|
||||
public static final int JAVAFX_MAJOR_VERSION;
|
||||
|
||||
static {
|
||||
String jfxVersion = System.getProperty("javafx.version");
|
||||
int majorVersion = -1;
|
||||
if (jfxVersion != null) {
|
||||
Matcher matcher = Pattern.compile("^(?<version>[0-9]+)").matcher(jfxVersion);
|
||||
if (matcher.find()) {
|
||||
majorVersion = Lang.parseInt(matcher.group(), -1);
|
||||
}
|
||||
}
|
||||
JAVAFX_MAJOR_VERSION = majorVersion;
|
||||
}
|
||||
|
||||
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
|
||||
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
|
||||
|
||||
public static void shutdown() {
|
||||
for (Map.Entry<String, Path> entry : remoteImageCache.entrySet()) {
|
||||
try {
|
||||
Files.deleteIfExists(entry.getValue());
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Failed to delete cache file %s.", entry.getValue()), e);
|
||||
}
|
||||
remoteImageCache.remove(entry.getKey());
|
||||
}
|
||||
|
||||
builtinImageCache.clear();
|
||||
}
|
||||
|
||||
public static void runInFX(Runnable runnable) {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
runnable.run();
|
||||
@@ -96,9 +152,9 @@ public final class FXUtils {
|
||||
value.addListener((a, b, c) -> consumer.accept(c));
|
||||
}
|
||||
|
||||
public static <T> WeakChangeListener<T> onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
WeakChangeListener<T> listener = new WeakChangeListener<>((a, b, c) -> consumer.accept(c));
|
||||
value.addListener(listener);
|
||||
public static <T> ChangeListener<T> onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
ChangeListener<T> listener = (a, b, c) -> consumer.accept(c);
|
||||
value.addListener(new WeakChangeListener<>(listener));
|
||||
return listener;
|
||||
}
|
||||
|
||||
@@ -107,11 +163,21 @@ public final class FXUtils {
|
||||
onChange(value, consumer);
|
||||
}
|
||||
|
||||
public static <T> WeakChangeListener<T> onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
public static <T> ChangeListener<T> onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
consumer.accept(value.getValue());
|
||||
return onWeakChange(value, consumer);
|
||||
}
|
||||
|
||||
public static InvalidationListener observeWeak(Runnable runnable, Observable... observables) {
|
||||
InvalidationListener originalListener = observable -> runnable.run();
|
||||
WeakInvalidationListener listener = new WeakInvalidationListener(originalListener);
|
||||
for (Observable observable : observables) {
|
||||
observable.addListener(listener);
|
||||
}
|
||||
runnable.run();
|
||||
return originalListener;
|
||||
}
|
||||
|
||||
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
|
||||
if (condition.getAsBoolean()) Platform.runLater(() -> runLaterIf(condition, runnable));
|
||||
else runnable.run();
|
||||
@@ -243,22 +309,16 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static void smoothScrolling(ScrollPane scrollPane) {
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
if (AnimationUtils.isAnimationEnabled())
|
||||
ScrollUtils.addSmoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
public static void loadFXML(Node node, String absolutePath) {
|
||||
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
|
||||
loader.setRoot(node);
|
||||
loader.setController(node);
|
||||
try {
|
||||
loader.load();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(50);
|
||||
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
|
||||
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
|
||||
|
||||
public static void installFastTooltip(Node node, Tooltip tooltip) {
|
||||
installTooltip(node, 50, 5000, 0, tooltip);
|
||||
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
}
|
||||
|
||||
public static void installFastTooltip(Node node, String tooltip) {
|
||||
@@ -266,128 +326,273 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, Tooltip tooltip) {
|
||||
installTooltip(node, 500, 5000, 0, tooltip);
|
||||
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, String tooltip) {
|
||||
installSlowTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
|
||||
runInFX(() -> {
|
||||
try {
|
||||
// Java 8
|
||||
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
|
||||
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
|
||||
behaviorConstructor.setAccessible(true);
|
||||
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
|
||||
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
|
||||
installMethod.setAccessible(true);
|
||||
installMethod.invoke(behavior, node, tooltip);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
try {
|
||||
// Java 9
|
||||
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
|
||||
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
|
||||
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
|
||||
} catch (ReflectiveOperationException e2) {
|
||||
e.addSuppressed(e2);
|
||||
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
|
||||
}
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void playAnimation(Node node, String animationKey, Timeline timeline) {
|
||||
animationKey = "FXUTILS.ANIMATION." + animationKey;
|
||||
Object oldTimeline = node.getProperties().get(animationKey);
|
||||
if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
|
||||
// if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
|
||||
if (timeline != null) timeline.play();
|
||||
node.getProperties().put(animationKey, timeline);
|
||||
}
|
||||
|
||||
public static <T> void playAnimation(Node node, String animationKey, Duration duration, WritableValue<T> property, T from, T to, Interpolator interpolator) {
|
||||
public static <T> Animation playAnimation(Node node, String animationKey, Duration duration, WritableValue<T> property, T from, T to, Interpolator interpolator) {
|
||||
if (from == null) from = property.getValue();
|
||||
if (duration == null || Objects.equals(duration, Duration.ZERO) || Objects.equals(from, to)) {
|
||||
playAnimation(node, animationKey, null);
|
||||
property.setValue(to);
|
||||
return null;
|
||||
} else {
|
||||
playAnimation(node, animationKey, new Timeline(
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(property, from, interpolator)),
|
||||
new KeyFrame(duration, new KeyValue(property, to, interpolator))
|
||||
));
|
||||
);
|
||||
playAnimation(node, animationKey, timeline);
|
||||
return timeline;
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFolder(File file) {
|
||||
if (!FileUtils.makeDirectory(file)) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
|
||||
LOG.error("Unable to make directory " + file);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = file.getAbsolutePath();
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case OSX:
|
||||
String openCommand;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommand = "explorer.exe";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
openCommand = "/usr/bin/open";
|
||||
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && new File("/usr/bin/xdg-open").exists())
|
||||
openCommand = "/usr/bin/xdg-open";
|
||||
else
|
||||
openCommand = null;
|
||||
|
||||
thread(() -> {
|
||||
if (openCommand != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e);
|
||||
int exitCode = SystemUtils.callExternalProcess(openCommand, path);
|
||||
|
||||
// explorer.exe always return 1
|
||||
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
|
||||
return;
|
||||
else
|
||||
LOG.warning("Open " + path + " failed with code " + exitCode);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Unable to open " + path + " by executing " + openCommand, e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
thread(() -> {
|
||||
if (java.awt.Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback to java.awt.Desktop::open
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
LOG.error("Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String which(String command) {
|
||||
String path = System.getenv("PATH");
|
||||
if (path == null)
|
||||
return null;
|
||||
|
||||
for (String item : path.split(OperatingSystem.PATH_SEPARATOR)) {
|
||||
try {
|
||||
Path program = Paths.get(item, command);
|
||||
if (Files.isExecutable(program))
|
||||
return program.toRealPath().toString();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void showFileInExplorer(Path file) {
|
||||
String path = file.toAbsolutePath().toString();
|
||||
|
||||
String[] openCommands;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommands = new String[]{"explorer.exe", "/select,", path};
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
openCommands = new String[]{"/usr/bin/open", "-R", path};
|
||||
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && which("dbus-send") != null)
|
||||
openCommands = new String[]{
|
||||
"dbus-send",
|
||||
"--print-reply",
|
||||
"--dest=org.freedesktop.FileManager1",
|
||||
"/org/freedesktop/FileManager1",
|
||||
"org.freedesktop.FileManager1.ShowItems",
|
||||
"array:string:" + file.toAbsolutePath().toUri(),
|
||||
"string:"
|
||||
};
|
||||
else
|
||||
openCommands = null;
|
||||
|
||||
if (openCommands != null) {
|
||||
thread(() -> {
|
||||
try {
|
||||
int exitCode = SystemUtils.callExternalProcess(openCommands);
|
||||
|
||||
// explorer.exe always return 1
|
||||
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
|
||||
return;
|
||||
else
|
||||
LOG.warning("Show " + path + " in explorer failed with code " + exitCode);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Unable to show " + path + " in explorer", e);
|
||||
}
|
||||
|
||||
// Fallback to open folder
|
||||
openFolder(file.getParent().toFile());
|
||||
});
|
||||
} else {
|
||||
// We do not have a universal method to show file in file manager.
|
||||
openFolder(file.getParent().toFile());
|
||||
}
|
||||
}
|
||||
|
||||
private static final String[] linuxBrowsers = {
|
||||
"xdg-open",
|
||||
"google-chrome",
|
||||
"firefox",
|
||||
"microsoft-edge",
|
||||
"opera",
|
||||
"konqueror",
|
||||
"mozilla"
|
||||
};
|
||||
|
||||
/**
|
||||
* Open URL by java.awt.Desktop
|
||||
* Open URL in browser
|
||||
*
|
||||
* @param link null is allowed but will be ignored
|
||||
*/
|
||||
public static void openLink(String link) {
|
||||
if (link == null)
|
||||
return;
|
||||
|
||||
thread(() -> {
|
||||
if (java.awt.Desktop.isDesktopSupported()) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
|
||||
LOG.warning("An exception occurred while calling rundll32", e);
|
||||
}
|
||||
}
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
String path = which(browser);
|
||||
if (path != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{browser, link});
|
||||
return;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
LOG.warning("No known browser found");
|
||||
}
|
||||
}
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
LOG.warning("Unable to open link: " + link, ex);
|
||||
}
|
||||
LOG.warning("Failed to open link: " + link, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void bindInt(JFXTextField textField, Property<Number> property) {
|
||||
textField.textProperty().bindBidirectional(property, SafeStringConverter.fromInteger());
|
||||
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
|
||||
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
|
||||
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
|
||||
textField.focusedProperty().addListener((ChangeListener<Boolean>) listener);
|
||||
property.addListener(listener);
|
||||
}
|
||||
|
||||
public static void unbindInt(JFXTextField textField, Property<Number> property) {
|
||||
textField.textProperty().unbindBidirectional(property);
|
||||
public static void bindInt(JFXTextField textField, Property<Number> property) {
|
||||
bind(textField, property, SafeStringConverter.fromInteger());
|
||||
}
|
||||
|
||||
public static void bindString(JFXTextField textField, Property<String> property) {
|
||||
textField.textProperty().bindBidirectional(property);
|
||||
bind(textField, property, null);
|
||||
}
|
||||
|
||||
public static void unbindString(JFXTextField textField, Property<String> property) {
|
||||
textField.textProperty().unbindBidirectional(property);
|
||||
public static void unbind(JFXTextField textField, Property<?> property) {
|
||||
TextFieldBindingListener<?> listener = new TextFieldBindingListener<>(textField, property, null);
|
||||
textField.focusedProperty().removeListener((ChangeListener<Boolean>) listener);
|
||||
property.removeListener(listener);
|
||||
}
|
||||
|
||||
private static final class TextFieldBindingListener<T> implements ChangeListener<Boolean>, InvalidationListener {
|
||||
private final int hashCode;
|
||||
private final WeakReference<JFXTextField> textFieldRef;
|
||||
private final WeakReference<Property<T>> propertyRef;
|
||||
private final StringConverter<T> converter;
|
||||
|
||||
TextFieldBindingListener(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
|
||||
this.textFieldRef = new WeakReference<>(textField);
|
||||
this.propertyRef = new WeakReference<>(property);
|
||||
this.converter = converter;
|
||||
this.hashCode = System.identityHashCode(textField) ^ System.identityHashCode(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean focused) { // On TextField changed
|
||||
JFXTextField textField = textFieldRef.get();
|
||||
Property<T> property = this.propertyRef.get();
|
||||
|
||||
if (textField != null && property != null && oldValue == Boolean.TRUE && focused == Boolean.FALSE) {
|
||||
if (textField.validate()) {
|
||||
String newText = textField.getText();
|
||||
@SuppressWarnings("unchecked")
|
||||
T newValue = converter == null ? (T) newText : converter.fromString(newText);
|
||||
|
||||
if (!Objects.equals(newValue, property.getValue()))
|
||||
property.setValue(newValue);
|
||||
} else {
|
||||
// Rollback to old value
|
||||
invalidated(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) { // On property change
|
||||
JFXTextField textField = textFieldRef.get();
|
||||
Property<T> property = this.propertyRef.get();
|
||||
|
||||
if (textField != null && property != null) {
|
||||
T value = property.getValue();
|
||||
textField.setText(converter == null ? (String) value : converter.toString(value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof TextFieldBindingListener))
|
||||
return false;
|
||||
TextFieldBindingListener<?> other = (TextFieldBindingListener<?>) obj;
|
||||
return this.hashCode == other.hashCode
|
||||
&& this.textFieldRef.get() == other.textFieldRef.get()
|
||||
&& this.propertyRef.get() == other.propertyRef.get();
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindBoolean(JFXToggleButton toggleButton, Property<Boolean> property) {
|
||||
@@ -409,18 +614,21 @@ public final class FXUtils {
|
||||
/**
|
||||
* Bind combo box selection with given enum property bidirectionally.
|
||||
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
|
||||
*
|
||||
* @param comboBox the combo box being bound with {@code property}.
|
||||
* @param property the property being bound with {@code combo box}.
|
||||
* @see #unbindEnum(JFXComboBox)
|
||||
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
|
||||
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Deprecated
|
||||
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum<?>> property) {
|
||||
public static <T extends Enum<T>> void bindEnum(JFXComboBox<T> comboBox, Property<T> property) {
|
||||
unbindEnum(comboBox);
|
||||
ChangeListener<Number> listener = (a, b, newValue) ->
|
||||
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
|
||||
comboBox.getSelectionModel().select(property.getValue().ordinal());
|
||||
|
||||
T currentValue = property.getValue();
|
||||
@SuppressWarnings("unchecked")
|
||||
T[] enumConstants = (T[]) currentValue.getClass().getEnumConstants();
|
||||
ChangeListener<Number> listener = (a, b, newValue) -> property.setValue(enumConstants[newValue.intValue()]);
|
||||
|
||||
comboBox.getSelectionModel().select(currentValue.ordinal());
|
||||
comboBox.getProperties().put("FXUtils.bindEnum.listener", listener);
|
||||
comboBox.getSelectionModel().selectedIndexProperty().addListener(listener);
|
||||
}
|
||||
@@ -428,32 +636,223 @@ public final class FXUtils {
|
||||
/**
|
||||
* Unbind combo box selection with given enum property bidirectionally.
|
||||
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
|
||||
*
|
||||
* @param comboBox the combo box being bound with the property which can be inferred by {@code bindEnum}.
|
||||
* @see #bindEnum(JFXComboBox, Property)
|
||||
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Deprecated
|
||||
public static void unbindEnum(JFXComboBox<?> comboBox) {
|
||||
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
|
||||
if (listener == null) return;
|
||||
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
|
||||
public static void unbindEnum(JFXComboBox<? extends Enum<?>> comboBox) {
|
||||
@SuppressWarnings("unchecked")
|
||||
ChangeListener<Number> listener = (ChangeListener<Number>) comboBox.getProperties().remove("FXUtils.bindEnum.listener");
|
||||
if (listener != null)
|
||||
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
|
||||
}
|
||||
|
||||
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
int itemCount = children.length;
|
||||
int childSelectedCount = 0;
|
||||
for (BooleanProperty child : children) {
|
||||
if (child.get())
|
||||
childSelectedCount++;
|
||||
}
|
||||
|
||||
allEnabled.set(childSelectedCount == itemCount);
|
||||
|
||||
class Listener implements InvalidationListener {
|
||||
private int childSelectedCount;
|
||||
private boolean updating = false;
|
||||
|
||||
public Listener(int childSelectedCount) {
|
||||
this.childSelectedCount = childSelectedCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
if (updating)
|
||||
return;
|
||||
|
||||
updating = true;
|
||||
try {
|
||||
boolean value = ((BooleanProperty) observable).get();
|
||||
|
||||
if (observable == allEnabled) {
|
||||
for (BooleanProperty child : children) {
|
||||
child.setValue(value);
|
||||
}
|
||||
childSelectedCount = value ? itemCount : 0;
|
||||
} else {
|
||||
if (value)
|
||||
childSelectedCount++;
|
||||
else
|
||||
childSelectedCount--;
|
||||
|
||||
allEnabled.set(childSelectedCount == itemCount);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InvalidationListener listener = new Listener(childSelectedCount);
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(listener);
|
||||
allEnabled.addListener(listener);
|
||||
for (BooleanProperty child : children) {
|
||||
child.addListener(weakListener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setIcon(Stage stage) {
|
||||
String icon;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
icon = "/assets/img/icon.png";
|
||||
} else {
|
||||
icon = "/assets/img/icon@4x.png";
|
||||
}
|
||||
stage.getIcons().add(newBuiltinImage(icon));
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress IllegalArgumentException since the url is supposed to be correct definitely.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file within the jar.
|
||||
* @return the image resource within the jar.
|
||||
* @see org.jackhuang.hmcl.util.CrashReporter
|
||||
* @see ResourceNotFoundError
|
||||
*/
|
||||
public static Image newImage(String url) {
|
||||
public static Image newBuiltinImage(String url) {
|
||||
try {
|
||||
return new Image(url);
|
||||
return builtinImageCache.computeIfAbsent(url, Image::new);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ResourceNotFoundError("Cannot access image: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress IllegalArgumentException since the url is supposed to be correct definitely.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file within the jar.
|
||||
* @param requestedWidth the image's bounding box width
|
||||
* @param requestedHeight the image's bounding box height
|
||||
* @param preserveRatio indicates whether to preserve the aspect ratio of
|
||||
* the original image when scaling to fit the image within the
|
||||
* specified bounding box
|
||||
* @param smooth indicates whether to use a better quality filtering
|
||||
* algorithm or a faster one when scaling this image to fit within
|
||||
* the specified bounding box
|
||||
* @return the image resource within the jar.
|
||||
* @see org.jackhuang.hmcl.util.CrashReporter
|
||||
* @see ResourceNotFoundError
|
||||
*/
|
||||
public static Image newBuiltinImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
try {
|
||||
return new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ResourceNotFoundError("Cannot access image: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from the internet. It will cache the data of images for the further usage.
|
||||
* The cached data will be deleted when HMCL is closed or hidden.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file on the internet.
|
||||
* @return the image resource within the jar.
|
||||
*/
|
||||
public static Image newRemoteImage(String url) {
|
||||
return newRemoteImage(url, 0, 0, false, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from the internet. It will cache the data of images for the further usage.
|
||||
* The cached data will be deleted when HMCL is closed or hidden.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file on the internet.
|
||||
* @param requestedWidth the image's bounding box width
|
||||
* @param requestedHeight the image's bounding box height
|
||||
* @param preserveRatio indicates whether to preserve the aspect ratio of
|
||||
* the original image when scaling to fit the image within the
|
||||
* specified bounding box
|
||||
* @param smooth indicates whether to use a better quality filtering
|
||||
* algorithm or a faster one when scaling this image to fit within
|
||||
* the specified bounding box
|
||||
* @return the image resource within the jar.
|
||||
*/
|
||||
public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) {
|
||||
Path currentPath = remoteImageCache.get(url);
|
||||
if (currentPath != null) {
|
||||
if (Files.isReadable(currentPath)) {
|
||||
try (InputStream inputStream = Files.newInputStream(currentPath)) {
|
||||
return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("An exception encountered while reading data from cached image file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// The file is unavailable or unreadable.
|
||||
remoteImageCache.remove(url);
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(currentPath);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("An exception encountered while deleting broken cached image file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading);
|
||||
image.progressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) {
|
||||
Task.runAsync(() -> {
|
||||
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
|
||||
try ( // Make sure the file is released from JVM before we put the path into remoteImageCache.
|
||||
OutputStream outputStream = Files.newOutputStream(newPath);
|
||||
PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL)
|
||||
) {
|
||||
writer.write(PNGJavaFXUtils.asArgbImage(image));
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
Files.delete(newPath);
|
||||
} catch (IOException e2) {
|
||||
e2.addSuppressed(e);
|
||||
throw e2;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (remoteImageCache.putIfAbsent(url, newPath) != null) {
|
||||
Files.delete(newPath); // The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
});
|
||||
return image;
|
||||
}
|
||||
|
||||
public static JFXButton newRaisedButton(String text) {
|
||||
JFXButton button = new JFXButton(text);
|
||||
button.getStyleClass().add("jfx-button-raised");
|
||||
button.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
return button;
|
||||
}
|
||||
|
||||
public static JFXButton newBorderButton(String text) {
|
||||
JFXButton button = new JFXButton(text);
|
||||
button.getStyleClass().add("jfx-button-border");
|
||||
button.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
return button;
|
||||
}
|
||||
|
||||
public static Label truncatedLabel(String text, int limit) {
|
||||
Label label = new Label();
|
||||
if (text.length() <= limit) {
|
||||
label.setText(text);
|
||||
} else {
|
||||
label.setText(StringUtils.truncate(text, limit));
|
||||
installFastTooltip(label, text);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
|
||||
applyDragListener(node, filter, callback, null);
|
||||
}
|
||||
@@ -498,10 +897,17 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
return view -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
if (!empty) {
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(graphicBuilder.apply(item));
|
||||
@@ -510,6 +916,19 @@ public final class FXUtils {
|
||||
};
|
||||
}
|
||||
|
||||
public static ColumnConstraints getColumnFillingWidth() {
|
||||
ColumnConstraints constraint = new ColumnConstraints();
|
||||
constraint.setFillWidth(true);
|
||||
return constraint;
|
||||
}
|
||||
|
||||
public static ColumnConstraints getColumnHgrowing() {
|
||||
ColumnConstraints constraint = new ColumnConstraints();
|
||||
constraint.setFillWidth(true);
|
||||
constraint.setHgrow(Priority.ALWAYS);
|
||||
return constraint;
|
||||
}
|
||||
|
||||
public static final Interpolator SINE = new Interpolator() {
|
||||
@Override
|
||||
protected double curve(double t) {
|
||||
@@ -522,10 +941,97 @@ public final class FXUtils {
|
||||
}
|
||||
};
|
||||
|
||||
public static Runnable withJFXPopupClosing(Runnable runnable, JFXPopup popup) {
|
||||
return () -> {
|
||||
runnable.run();
|
||||
popup.hide();
|
||||
};
|
||||
public static void onEscPressed(Node node, Runnable action) {
|
||||
node.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.ESCAPE) {
|
||||
action.run();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void onClicked(Node node, Runnable action) {
|
||||
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 1) {
|
||||
action.run();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void copyText(String text) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(text);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
|
||||
if (!Controllers.isStopped()) {
|
||||
Controllers.showToast(i18n("message.copied"));
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
if (segment.indexOf('<') < 0)
|
||||
return Collections.singletonList(new Text(segment));
|
||||
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document doc = builder.parse(new InputSource(new StringReader("<body>" + segment + "</body>")));
|
||||
Element r = doc.getDocumentElement();
|
||||
|
||||
NodeList children = r.getChildNodes();
|
||||
List<Node> texts = new ArrayList<>();
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
org.w3c.dom.Node node = children.item(i);
|
||||
|
||||
if (node instanceof Element) {
|
||||
Element element = (Element) node;
|
||||
if ("a".equals(element.getTagName())) {
|
||||
String href = element.getAttribute("href");
|
||||
Text text = new Text(element.getTextContent());
|
||||
onClicked(text, () -> {
|
||||
String link = href;
|
||||
try {
|
||||
link = new URI(href).toASCIIString();
|
||||
} catch (URISyntaxException ignored) {
|
||||
}
|
||||
hyperlinkAction.accept(link);
|
||||
});
|
||||
text.setCursor(Cursor.HAND);
|
||||
text.setFill(Color.web("#0070E0"));
|
||||
text.setUnderline(true);
|
||||
texts.add(text);
|
||||
} else if ("b".equals(element.getTagName())) {
|
||||
Text text = new Text(element.getTextContent());
|
||||
text.getStyleClass().add("bold");
|
||||
texts.add(text);
|
||||
} else if ("br".equals(element.getTagName())) {
|
||||
texts.add(new Text("\n"));
|
||||
} else {
|
||||
throw new IllegalArgumentException("unsupported tag " + element.getTagName());
|
||||
}
|
||||
} else {
|
||||
texts.add(new Text(node.getTextContent()));
|
||||
}
|
||||
}
|
||||
return texts;
|
||||
} catch (SAXException | ParserConfigurationException | IOException e) {
|
||||
LOG.warning("Failed to parse xml", e);
|
||||
return Collections.singletonList(new Text(segment));
|
||||
}
|
||||
}
|
||||
|
||||
public static TextFlow segmentToTextFlow(final String segment, Consumer<String> hyperlinkAction) {
|
||||
TextFlow tf = new TextFlow();
|
||||
tf.getChildren().setAll(parseSegment(segment, hyperlinkAction));
|
||||
return tf;
|
||||
}
|
||||
|
||||
public static String toWeb(Color color) {
|
||||
int r = (int) Math.round(color.getRed() * 255.0);
|
||||
int g = (int) Math.round(color.getGreen() * 255.0);
|
||||
int b = (int) Math.round(color.getBlue() * 255.0);
|
||||
|
||||
return String.format("#%02x%02x%02x", r, g, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2023 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameCrashWindow extends Stage {
|
||||
private final Version version;
|
||||
private final String memory;
|
||||
private final String total_memory;
|
||||
private final String java;
|
||||
private final LibraryAnalyzer analyzer;
|
||||
private final TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown")));
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty();
|
||||
private final TextFlow feedbackTextFlow = new TextFlow();
|
||||
|
||||
private final ManagedProcess managedProcess;
|
||||
private final DefaultGameRepository repository;
|
||||
private final ProcessListener.ExitType exitType;
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
|
||||
private final List<Log> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.launchOptions = launchOptions;
|
||||
this.logs = logs;
|
||||
this.analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null));
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
|
||||
total_memory = OperatingSystem.TOTAL_MEMORY + " MB";
|
||||
|
||||
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
? launchOptions.getJava().getVersion()
|
||||
: launchOptions.getJava().getVersion() + " (" + launchOptions.getJava().getArchitecture().getDisplayName() + ")";
|
||||
|
||||
this.view = new View();
|
||||
|
||||
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
|
||||
|
||||
setScene(new Scene(view, 800, 480));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("game.crash.title"));
|
||||
FXUtils.setIcon(this);
|
||||
|
||||
analyzeCrashReport();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void analyzeCrashReport() {
|
||||
loading.set(true);
|
||||
Task.allOf(Task.supplyAsync(() -> {
|
||||
String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n"));
|
||||
|
||||
// Get the crash-report from the crash-reports/xxx, or the output of console.
|
||||
String crashReport = null;
|
||||
try {
|
||||
crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read crash report", e);
|
||||
}
|
||||
if (crashReport == null) {
|
||||
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
|
||||
}
|
||||
|
||||
return pair(CrashReportAnalyzer.anaylze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
|
||||
}), Task.supplyAsync(() -> {
|
||||
Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log");
|
||||
if (!Files.isReadable(latestLog)) {
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
}
|
||||
|
||||
String log;
|
||||
try {
|
||||
log = FileUtils.readText(latestLog);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read logs/latest.log", e);
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
}
|
||||
|
||||
return pair(CrashReportAnalyzer.anaylze(log), CrashReportAnalyzer.findKeywordsFromCrashReport(log));
|
||||
})).whenComplete(Schedulers.javafx(), (taskResult, exception) -> {
|
||||
loading.set(false);
|
||||
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to analyze crash report", exception);
|
||||
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
|
||||
} else {
|
||||
EnumMap<CrashReportAnalyzer.Rule, CrashReportAnalyzer.Result> results = new EnumMap<>(CrashReportAnalyzer.Rule.class);
|
||||
Set<String> keywords = new HashSet<>();
|
||||
for (Pair<Set<CrashReportAnalyzer.Result>, Set<String>> pair : (List<Pair<Set<CrashReportAnalyzer.Result>, Set<String>>>) (List<?>) taskResult) {
|
||||
for (CrashReportAnalyzer.Result result : pair.getKey()) {
|
||||
results.put(result.getRule(), result);
|
||||
}
|
||||
keywords.addAll(pair.getValue());
|
||||
}
|
||||
|
||||
List<Node> segments = new ArrayList<>(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
|
||||
|
||||
LOG.info("Number of reasons: " + results.size());
|
||||
if (results.size() > 1) {
|
||||
segments.add(new Text("\n"));
|
||||
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.multiple"), Controllers::onHyperlinkAction));
|
||||
} else {
|
||||
segments.add(new Text("\n\n"));
|
||||
}
|
||||
|
||||
for (CrashReportAnalyzer.Result result : results.values()) {
|
||||
String message;
|
||||
switch (result.getRule()) {
|
||||
case TOO_OLD_JAVA:
|
||||
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected"))));
|
||||
break;
|
||||
case MOD_RESOLUTION_CONFLICT:
|
||||
case MOD_RESOLUTION_MISSING:
|
||||
case MOD_RESOLUTION_COLLECTION:
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("sourcemod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod")));
|
||||
break;
|
||||
case MOD_RESOLUTION_MISSING_MINECRAFT:
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("mod")),
|
||||
result.getMatcher().group("version"));
|
||||
break;
|
||||
case MOD_FOREST_OPTIFINE:
|
||||
case TWILIGHT_FOREST_OPTIFINE:
|
||||
case PERFORMANT_FOREST_OPTIFINE:
|
||||
case JADE_FOREST_OPTIFINE:
|
||||
case NEOFORGE_FOREST_OPTIFINE:
|
||||
message = i18n("game.crash.reason.mod", "OptiFine");
|
||||
LOG.info("Crash cause: " + result.getRule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
|
||||
break;
|
||||
default:
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
|
||||
.toArray());
|
||||
break;
|
||||
}
|
||||
LOG.info("Crash cause: " + result.getRule() + ": " + message);
|
||||
segments.addAll(FXUtils.parseSegment(message, Controllers::onHyperlinkAction));
|
||||
segments.add(new Text("\n\n"));
|
||||
}
|
||||
if (results.isEmpty()) {
|
||||
if (!keywords.isEmpty()) {
|
||||
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.stacktrace", String.join(", ", keywords))));
|
||||
LOG.info("Crash reason unknown, but some log keywords have been found: " + String.join(", ", keywords));
|
||||
} else {
|
||||
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
|
||||
LOG.info("Crash reason unknown");
|
||||
}
|
||||
} else {
|
||||
feedbackTextFlow.setVisible(false);
|
||||
reasonTextFlow.getChildren().setAll(segments);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static final Pattern FABRIC_MOD_ID = Pattern.compile("\\{(?<modid>.*?) @ (?<version>.*?)}");
|
||||
|
||||
private String translateFabricModId(String modName) {
|
||||
switch (modName) {
|
||||
case "fabricloader":
|
||||
return "Fabric";
|
||||
case "fabric":
|
||||
return "Fabric API";
|
||||
case "minecraft":
|
||||
return "Minecraft";
|
||||
default:
|
||||
return modName;
|
||||
}
|
||||
}
|
||||
|
||||
private String parseFabricModId(String modName) {
|
||||
Matcher matcher = FABRIC_MOD_ID.matcher(modName);
|
||||
if (matcher.find()) {
|
||||
String modid = matcher.group("modid");
|
||||
String version = matcher.group("version");
|
||||
if ("[*]".equals(version)) {
|
||||
return i18n("game.crash.reason.mod_resolution_mod_version.any", translateFabricModId(modid));
|
||||
} else {
|
||||
return i18n("game.crash.reason.mod_resolution_mod_version", translateFabricModId(modid), version);
|
||||
}
|
||||
}
|
||||
return translateFabricModId(modName);
|
||||
}
|
||||
|
||||
private void showLogWindow() {
|
||||
LogWindow logWindow = new LogWindow(managedProcess);
|
||||
|
||||
logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO));
|
||||
if (managedProcess.getClasspath() != null)
|
||||
logWindow.logLine(new Log("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO));
|
||||
logWindow.logLines(logs);
|
||||
logWindow.show();
|
||||
}
|
||||
|
||||
private void exportGameCrashInfo() {
|
||||
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
|
||||
|
||||
CompletableFuture.supplyAsync(() ->
|
||||
logs.stream().map(Log::getLog).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.handleAsync((result, exception) -> {
|
||||
Alert alert;
|
||||
|
||||
if (exception == null) {
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
} else {
|
||||
LOG.warning("Failed to export game crash info", exception);
|
||||
alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception));
|
||||
}
|
||||
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private final class View extends VBox {
|
||||
|
||||
View() {
|
||||
setStyle("-fx-background-color: white");
|
||||
|
||||
HBox titlePane = new HBox();
|
||||
{
|
||||
Label title = new Label();
|
||||
HBox.setHgrow(title, Priority.ALWAYS);
|
||||
|
||||
switch (exitType) {
|
||||
case JVM_ERROR:
|
||||
title.setText(i18n("launch.failed.cannot_create_jvm"));
|
||||
break;
|
||||
case APPLICATION_ERROR:
|
||||
title.setText(i18n("launch.failed.exited_abnormally"));
|
||||
break;
|
||||
case SIGKILL:
|
||||
title.setText(i18n("launch.failed.sigkill"));
|
||||
break;
|
||||
}
|
||||
|
||||
titlePane.setAlignment(Pos.CENTER);
|
||||
titlePane.getStyleClass().addAll("jfx-tool-bar-second", "depth-1", "padding-8");
|
||||
titlePane.getChildren().setAll(title);
|
||||
}
|
||||
|
||||
HBox infoPane = new HBox(8);
|
||||
{
|
||||
infoPane.setPadding(new Insets(8));
|
||||
infoPane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
TwoLineListItem launcher = new TwoLineListItem();
|
||||
launcher.getStyleClass().setAll("two-line-item-second-large");
|
||||
launcher.setTitle(i18n("launcher"));
|
||||
launcher.setSubtitle(Metadata.VERSION);
|
||||
|
||||
TwoLineListItem version = new TwoLineListItem();
|
||||
version.getStyleClass().setAll("two-line-item-second-large");
|
||||
version.setTitle(i18n("game.version"));
|
||||
version.setSubtitle(GameCrashWindow.this.version.getId());
|
||||
|
||||
TwoLineListItem total_memory = new TwoLineListItem();
|
||||
total_memory.getStyleClass().setAll("two-line-item-second-large");
|
||||
total_memory.setTitle(i18n("settings.physical_memory"));
|
||||
total_memory.setSubtitle(GameCrashWindow.this.total_memory);
|
||||
|
||||
TwoLineListItem memory = new TwoLineListItem();
|
||||
memory.getStyleClass().setAll("two-line-item-second-large");
|
||||
memory.setTitle(i18n("settings.memory"));
|
||||
memory.setSubtitle(GameCrashWindow.this.memory);
|
||||
|
||||
TwoLineListItem java = new TwoLineListItem();
|
||||
java.getStyleClass().setAll("two-line-item-second-large");
|
||||
java.setTitle("Java");
|
||||
java.setSubtitle(GameCrashWindow.this.java);
|
||||
|
||||
TwoLineListItem os = new TwoLineListItem();
|
||||
os.getStyleClass().setAll("two-line-item-second-large");
|
||||
os.setTitle(i18n("system.operating_system"));
|
||||
os.setSubtitle(Lang.requireNonNullElse(OperatingSystem.OS_RELEASE_NAME, OperatingSystem.SYSTEM_NAME));
|
||||
|
||||
TwoLineListItem arch = new TwoLineListItem();
|
||||
arch.getStyleClass().setAll("two-line-item-second-large");
|
||||
arch.setTitle(i18n("system.architecture"));
|
||||
arch.setSubtitle(Architecture.SYSTEM_ARCH.getDisplayName());
|
||||
|
||||
infoPane.getChildren().setAll(launcher, version, total_memory, memory, java, os, arch);
|
||||
}
|
||||
|
||||
HBox moddedPane = new HBox(8);
|
||||
{
|
||||
moddedPane.setPadding(new Insets(8));
|
||||
moddedPane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
if (!type.getPatchId().isEmpty()) {
|
||||
analyzer.getVersion(type).ifPresent(ver -> {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.getStyleClass().setAll("two-line-item-second-large");
|
||||
item.setTitle(i18n("install.installer." + type.getPatchId()));
|
||||
item.setSubtitle(ver);
|
||||
moddedPane.getChildren().add(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VBox gameDirPane = new VBox(8);
|
||||
{
|
||||
TwoLineListItem gameDir = new TwoLineListItem();
|
||||
gameDir.getStyleClass().setAll("two-line-item-second-large");
|
||||
gameDir.setTitle(i18n("game.directory"));
|
||||
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
|
||||
FXUtils.installFastTooltip(gameDir, i18n("game.directory"));
|
||||
|
||||
TwoLineListItem javaDir = new TwoLineListItem();
|
||||
javaDir.getStyleClass().setAll("two-line-item-second-large");
|
||||
javaDir.setTitle(i18n("settings.game.java_directory"));
|
||||
javaDir.setSubtitle(launchOptions.getJava().getBinary().toAbsolutePath().toString());
|
||||
FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory"));
|
||||
|
||||
Label reasonTitle = new Label(i18n("game.crash.reason"));
|
||||
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
|
||||
|
||||
ScrollPane reasonPane = new ScrollPane(reasonTextFlow);
|
||||
reasonPane.setFitToWidth(true);
|
||||
reasonPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
|
||||
gameDirPane.setPadding(new Insets(8));
|
||||
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
|
||||
FXUtils.onChangeAndOperate(feedbackTextFlow.visibleProperty(), visible -> {
|
||||
if (visible) {
|
||||
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane, feedbackTextFlow));
|
||||
} else {
|
||||
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
HBox toolBar = new HBox();
|
||||
{
|
||||
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
|
||||
|
||||
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
|
||||
logButton.setOnAction(e -> showLogWindow());
|
||||
|
||||
JFXButton helpButton = FXUtils.newRaisedButton(i18n("help"));
|
||||
helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
|
||||
FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"));
|
||||
|
||||
|
||||
toolBar.setPadding(new Insets(8));
|
||||
toolBar.setSpacing(8);
|
||||
toolBar.getStyleClass().add("jfx-tool-bar");
|
||||
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class HTMLRenderer {
|
||||
private static URI resolveLink(Node linkNode) {
|
||||
String href = linkNode.absUrl("href");
|
||||
if (href.isEmpty())
|
||||
return null;
|
||||
|
||||
try {
|
||||
return new URI(href);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<javafx.scene.Node> children = new ArrayList<>();
|
||||
private final List<Node> stack = new ArrayList<>();
|
||||
|
||||
private boolean bold;
|
||||
private boolean italic;
|
||||
private boolean underline;
|
||||
private boolean strike;
|
||||
private boolean highlight;
|
||||
private String headerLevel;
|
||||
private Node hyperlink;
|
||||
|
||||
private final Consumer<URI> onClickHyperlink;
|
||||
|
||||
public HTMLRenderer(Consumer<URI> onClickHyperlink) {
|
||||
this.onClickHyperlink = onClickHyperlink;
|
||||
}
|
||||
|
||||
private void updateStyle() {
|
||||
bold = false;
|
||||
italic = false;
|
||||
underline = false;
|
||||
strike = false;
|
||||
highlight = false;
|
||||
headerLevel = null;
|
||||
hyperlink = null;
|
||||
|
||||
for (Node node : stack) {
|
||||
String nodeName = node.nodeName();
|
||||
switch (nodeName) {
|
||||
case "b":
|
||||
case "strong":
|
||||
bold = true;
|
||||
break;
|
||||
case "i":
|
||||
case "em":
|
||||
italic = true;
|
||||
break;
|
||||
case "ins":
|
||||
underline = true;
|
||||
break;
|
||||
case "del":
|
||||
strike = true;
|
||||
break;
|
||||
case "mark":
|
||||
highlight = true;
|
||||
break;
|
||||
case "a":
|
||||
hyperlink = node;
|
||||
break;
|
||||
case "h1":
|
||||
case "h2":
|
||||
case "h3":
|
||||
case "h4":
|
||||
case "h5":
|
||||
case "h6":
|
||||
headerLevel = nodeName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void pushNode(Node node) {
|
||||
stack.add(node);
|
||||
updateStyle();
|
||||
}
|
||||
|
||||
private void popNode() {
|
||||
stack.remove(stack.size() - 1);
|
||||
updateStyle();
|
||||
}
|
||||
|
||||
private void applyStyle(Text text) {
|
||||
if (hyperlink != null) {
|
||||
URI target = resolveLink(hyperlink);
|
||||
if (target != null) {
|
||||
FXUtils.onClicked(text, () -> onClickHyperlink.accept(target));
|
||||
text.setCursor(Cursor.HAND);
|
||||
}
|
||||
text.getStyleClass().add("html-hyperlink");
|
||||
}
|
||||
|
||||
if (hyperlink != null || underline)
|
||||
text.setUnderline(true);
|
||||
|
||||
if (strike)
|
||||
text.setStrikethrough(true);
|
||||
|
||||
if (bold || highlight)
|
||||
text.getStyleClass().add("html-bold");
|
||||
|
||||
if (italic)
|
||||
text.getStyleClass().add("html-italic");
|
||||
|
||||
if (headerLevel != null)
|
||||
text.getStyleClass().add("html-" + headerLevel);
|
||||
}
|
||||
|
||||
private void appendText(String text) {
|
||||
Text textNode = new Text(text);
|
||||
applyStyle(textNode);
|
||||
children.add(textNode);
|
||||
}
|
||||
|
||||
private void appendImage(Node node) {
|
||||
String src = node.absUrl("src");
|
||||
URI imageUri = null;
|
||||
try {
|
||||
if (!src.isEmpty())
|
||||
imageUri = URI.create(src);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
String alt = node.attr("alt");
|
||||
|
||||
if (imageUri != null) {
|
||||
URI uri = URI.create(src);
|
||||
|
||||
String widthAttr = node.attr("width");
|
||||
String heightAttr = node.attr("height");
|
||||
|
||||
double width = 0;
|
||||
double height = 0;
|
||||
|
||||
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
|
||||
try {
|
||||
width = Double.parseDouble(widthAttr);
|
||||
height = Double.parseDouble(heightAttr);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
width = 0;
|
||||
height = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
|
||||
if (image.isError()) {
|
||||
LOG.warning("Failed to load image: " + uri, image.getException());
|
||||
} else {
|
||||
ImageView imageView = new ImageView(image);
|
||||
if (hyperlink != null) {
|
||||
URI target = resolveLink(hyperlink);
|
||||
if (target != null) {
|
||||
FXUtils.onClicked(imageView, () -> onClickHyperlink.accept(target));
|
||||
imageView.setCursor(Cursor.HAND);
|
||||
}
|
||||
}
|
||||
children.add(imageView);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alt.isEmpty())
|
||||
appendText(alt);
|
||||
}
|
||||
|
||||
public void appendNode(Node node) {
|
||||
if (node instanceof TextNode) {
|
||||
appendText(((TextNode) node).text());
|
||||
}
|
||||
|
||||
String name = node.nodeName();
|
||||
switch (name) {
|
||||
case "img":
|
||||
appendImage(node);
|
||||
break;
|
||||
case "li":
|
||||
appendText("\n \u2022 ");
|
||||
break;
|
||||
case "dt":
|
||||
appendText(" ");
|
||||
break;
|
||||
case "p":
|
||||
case "h1":
|
||||
case "h2":
|
||||
case "h3":
|
||||
case "h4":
|
||||
case "h5":
|
||||
case "h6":
|
||||
case "tr":
|
||||
if (!children.isEmpty())
|
||||
appendText("\n\n");
|
||||
break;
|
||||
}
|
||||
|
||||
if (node.childNodeSize() > 0) {
|
||||
pushNode(node);
|
||||
for (Node childNode : node.childNodes()) {
|
||||
appendNode(childNode);
|
||||
}
|
||||
popNode();
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "br":
|
||||
case "dd":
|
||||
case "p":
|
||||
case "h1":
|
||||
case "h2":
|
||||
case "h3":
|
||||
case "h4":
|
||||
case "h5":
|
||||
case "h6":
|
||||
appendText("\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public TextFlow render() {
|
||||
TextFlow textFlow = new TextFlow();
|
||||
textFlow.getStyleClass().add("html");
|
||||
textFlow.getChildren().setAll(children);
|
||||
return textFlow;
|
||||
}
|
||||
}
|
||||
@@ -18,26 +18,33 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -47,168 +54,389 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
*/
|
||||
public class InstallerItem extends Control {
|
||||
private final String id;
|
||||
private final String imageUrl;
|
||||
public final StringProperty libraryVersion = new SimpleStringProperty();
|
||||
public final StringProperty incompatibleLibraryName = new SimpleStringProperty();
|
||||
public final BooleanProperty incompatibleWithGame = new SimpleBooleanProperty();
|
||||
public final BooleanProperty removable = new SimpleBooleanProperty();
|
||||
public final BooleanProperty upgradable = new SimpleBooleanProperty(false);
|
||||
public final BooleanProperty installable = new SimpleBooleanProperty(true);
|
||||
public final ObjectProperty<EventHandler<? super MouseEvent>> removeAction = new SimpleObjectProperty<>();
|
||||
public final ObjectProperty<EventHandler<? super MouseEvent>> action = new SimpleObjectProperty<>();
|
||||
private final VersionIconType iconType;
|
||||
private final Style style;
|
||||
private final ObjectProperty<InstalledState> versionProperty = new SimpleObjectProperty<>(this, "version", null);
|
||||
private final ObjectProperty<State> resolvedStateProperty = new SimpleObjectProperty<>(this, "resolvedState", InstallableState.INSTANCE);
|
||||
|
||||
public InstallerItem(LibraryAnalyzer.LibraryType id) {
|
||||
this(id.getPatchId());
|
||||
private final ObjectProperty<Runnable> onInstall = new SimpleObjectProperty<>(this, "onInstall");
|
||||
private final ObjectProperty<Runnable> onRemove = new SimpleObjectProperty<>(this, "onRemove");
|
||||
|
||||
public interface State {
|
||||
}
|
||||
|
||||
public InstallerItem(String id) {
|
||||
public static final class InstallableState implements State {
|
||||
public static final InstallableState INSTANCE = new InstallableState();
|
||||
|
||||
private InstallableState() {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class IncompatibleState implements State {
|
||||
private final String incompatibleItemName;
|
||||
private final String incompatibleItemVersion;
|
||||
|
||||
public IncompatibleState(String incompatibleItemName, String incompatibleItemVersion) {
|
||||
this.incompatibleItemName = incompatibleItemName;
|
||||
this.incompatibleItemVersion = incompatibleItemVersion;
|
||||
}
|
||||
|
||||
public String getIncompatibleItemName() {
|
||||
return incompatibleItemName;
|
||||
}
|
||||
|
||||
public String getIncompatibleItemVersion() {
|
||||
return incompatibleItemVersion;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class InstalledState implements State {
|
||||
private final String version;
|
||||
private final boolean external;
|
||||
private final boolean incompatibleWithGame;
|
||||
|
||||
public InstalledState(String version, boolean external, boolean incompatibleWithGame) {
|
||||
this.version = version;
|
||||
this.external = external;
|
||||
this.incompatibleWithGame = incompatibleWithGame;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public boolean isExternal() {
|
||||
return external;
|
||||
}
|
||||
|
||||
public boolean isIncompatibleWithGame() {
|
||||
return incompatibleWithGame;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Style {
|
||||
LIST_ITEM,
|
||||
CARD,
|
||||
}
|
||||
|
||||
public InstallerItem(LibraryAnalyzer.LibraryType id, Style style) {
|
||||
this(id.getPatchId(), style);
|
||||
}
|
||||
|
||||
public InstallerItem(String id, Style style) {
|
||||
this.id = id;
|
||||
this.style = style;
|
||||
|
||||
switch (id) {
|
||||
case "game":
|
||||
imageUrl = "/assets/img/grass.png";
|
||||
iconType = VersionIconType.GRASS;
|
||||
break;
|
||||
case "fabric":
|
||||
imageUrl = "/assets/img/fabric.png";
|
||||
case "fabric-api":
|
||||
iconType = VersionIconType.FABRIC;
|
||||
break;
|
||||
case "forge":
|
||||
imageUrl = "/assets/img/forge.png";
|
||||
iconType = VersionIconType.FORGE;
|
||||
break;
|
||||
case "liteloader":
|
||||
imageUrl = "/assets/img/chicken.png";
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
break;
|
||||
case "optifine":
|
||||
imageUrl = "/assets/img/command.png";
|
||||
iconType = VersionIconType.OPTIFINE;
|
||||
break;
|
||||
case "quilt":
|
||||
case "quilt-api":
|
||||
iconType = VersionIconType.QUILT;
|
||||
break;
|
||||
case "neoforge":
|
||||
iconType = VersionIconType.NEO_FORGE;
|
||||
break;
|
||||
default:
|
||||
imageUrl = null;
|
||||
iconType = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setState(String libraryVersion, boolean incompatibleWithGame, boolean removable) {
|
||||
this.libraryVersion.set(libraryVersion);
|
||||
this.incompatibleWithGame.set(incompatibleWithGame);
|
||||
this.removable.set(removable);
|
||||
}
|
||||
|
||||
public String getLibraryId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ObjectProperty<InstalledState> versionProperty() {
|
||||
return versionProperty;
|
||||
}
|
||||
|
||||
public ObjectProperty<State> resolvedStateProperty() {
|
||||
return resolvedStateProperty;
|
||||
}
|
||||
|
||||
public ObjectProperty<Runnable> onInstallProperty() {
|
||||
return onInstall;
|
||||
}
|
||||
|
||||
public Runnable getOnInstall() {
|
||||
return onInstall.get();
|
||||
}
|
||||
|
||||
public void setOnInstall(Runnable onInstall) {
|
||||
this.onInstall.set(onInstall);
|
||||
}
|
||||
|
||||
public ObjectProperty<Runnable> onRemoveProperty() {
|
||||
return onRemove;
|
||||
}
|
||||
|
||||
public Runnable getOnRemove() {
|
||||
return onRemove.get();
|
||||
}
|
||||
|
||||
public void setOnRemove(Runnable onRemove) {
|
||||
this.onRemove.set(onRemove);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new InstallerItemSkin(this);
|
||||
}
|
||||
|
||||
public static class InstallerItemGroup {
|
||||
public InstallerItem game = new InstallerItem(MINECRAFT);
|
||||
public InstallerItem fabric = new InstallerItem(FABRIC);
|
||||
public InstallerItem forge = new InstallerItem(FORGE);
|
||||
public InstallerItem liteLoader = new InstallerItem(LITELOADER);
|
||||
public InstallerItem optiFine = new InstallerItem(OPTIFINE);
|
||||
public final static class InstallerItemGroup {
|
||||
private final InstallerItem game;
|
||||
|
||||
public InstallerItemGroup() {
|
||||
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion));
|
||||
private final InstallerItem[] libraries;
|
||||
|
||||
liteLoader.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion));
|
||||
private Set<InstallerItem> getIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item) {
|
||||
return incompatibleMap.computeIfAbsent(item, it -> new HashSet<>());
|
||||
}
|
||||
|
||||
optiFine.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion));
|
||||
private void addIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item, InstallerItem... others) {
|
||||
Set<InstallerItem> set = getIncompatibles(incompatibleMap, item);
|
||||
for (InstallerItem other : others) {
|
||||
set.add(other);
|
||||
getIncompatibles(incompatibleMap, other).add(item);
|
||||
}
|
||||
}
|
||||
|
||||
fabric.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId();
|
||||
if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId();
|
||||
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
|
||||
return null;
|
||||
}, optiFine.libraryVersion, forge.libraryVersion));
|
||||
private void mutualIncompatible(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem... items) {
|
||||
for (InstallerItem item : items) {
|
||||
Set<InstallerItem> set = getIncompatibles(incompatibleMap, item);
|
||||
|
||||
for (InstallerItem item2 : items) {
|
||||
if (item2 != item) {
|
||||
set.add(item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InstallerItemGroup(String gameVersion, Style style) {
|
||||
game = new InstallerItem(MINECRAFT, style);
|
||||
InstallerItem fabric = new InstallerItem(FABRIC, style);
|
||||
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
|
||||
InstallerItem forge = new InstallerItem(FORGE, style);
|
||||
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
|
||||
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
|
||||
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
|
||||
InstallerItem quilt = new InstallerItem(QUILT, style);
|
||||
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
|
||||
|
||||
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
|
||||
|
||||
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
|
||||
InstallerItem item = entry.getKey();
|
||||
Set<InstallerItem> incompatibleItems = entry.getValue();
|
||||
|
||||
Observable[] bindings = new Observable[incompatibleItems.size() + 1];
|
||||
bindings[0] = item.versionProperty;
|
||||
int i = 1;
|
||||
for (InstallerItem other : incompatibleItems) {
|
||||
bindings[i++] = other.versionProperty;
|
||||
}
|
||||
|
||||
item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> {
|
||||
InstalledState itemVersion = item.versionProperty.get();
|
||||
if (itemVersion != null) {
|
||||
return itemVersion;
|
||||
}
|
||||
|
||||
for (InstallerItem other : incompatibleItems) {
|
||||
InstalledState otherVersion = other.versionProperty.get();
|
||||
if (otherVersion != null) {
|
||||
return new IncompatibleState(other.id, otherVersion.version);
|
||||
}
|
||||
}
|
||||
|
||||
return InstallableState.INSTANCE;
|
||||
}, bindings));
|
||||
}
|
||||
|
||||
if (gameVersion != null) {
|
||||
game.versionProperty.set(new InstalledState(gameVersion, false, false));
|
||||
}
|
||||
|
||||
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
|
||||
for (InstallerItem item : all) {
|
||||
if (!item.resolvedStateProperty.isBound()) {
|
||||
item.resolvedStateProperty.bind(item.versionProperty);
|
||||
}
|
||||
}
|
||||
|
||||
if (gameVersion == null) {
|
||||
this.libraries = all;
|
||||
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
|
||||
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
|
||||
} else {
|
||||
this.libraries = new InstallerItem[]{game, forge, neoForge, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
}
|
||||
}
|
||||
|
||||
public InstallerItem getGame() {
|
||||
return game;
|
||||
}
|
||||
|
||||
public InstallerItem[] getLibraries() {
|
||||
return new InstallerItem[]{game, fabric, forge, liteLoader, optiFine};
|
||||
return libraries;
|
||||
}
|
||||
}
|
||||
|
||||
public static class InstallerItemSkin extends SkinBase<InstallerItem> {
|
||||
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
|
||||
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
|
||||
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
|
||||
private static final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
InstallerItemSkin(InstallerItem control) {
|
||||
super(control);
|
||||
|
||||
HBox hbox = new HBox();
|
||||
getChildren().setAll(hbox);
|
||||
JFXDepthManager.setDepth(hbox, 1);
|
||||
Pane pane;
|
||||
if (control.style == Style.CARD) {
|
||||
pane = new VBox();
|
||||
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
|
||||
} else {
|
||||
pane = new HBox();
|
||||
}
|
||||
pane.getStyleClass().add("installer-item");
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
getChildren().setAll(container);
|
||||
|
||||
hbox.getStyleClass().add("card");
|
||||
pane.pseudoClassStateChanged(LIST_ITEM, control.style == Style.LIST_ITEM);
|
||||
pane.pseudoClassStateChanged(CARD, control.style == Style.CARD);
|
||||
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
if (control.iconType != null) {
|
||||
ImageView view = new ImageView(control.iconType.getIcon());
|
||||
Node node = FXUtils.limitingSize(view, 32, 32);
|
||||
node.setMouseTransparent(true);
|
||||
node.getStyleClass().add("installer-item-image");
|
||||
pane.getChildren().add(node);
|
||||
|
||||
if (control.imageUrl != null) {
|
||||
hbox.getChildren().add(FXUtils.limitingSize(new ImageView(new Image(control.imageUrl, 32, 32, true, true)), 32, 32));
|
||||
if (control.style == Style.CARD) {
|
||||
VBox.setMargin(node, new Insets(8, 0, 16, 0));
|
||||
}
|
||||
}
|
||||
|
||||
Label nameLabel = new Label();
|
||||
hbox.getChildren().add(nameLabel);
|
||||
nameLabel.setPrefWidth(80);
|
||||
nameLabel.getStyleClass().add("installer-item-name");
|
||||
nameLabel.setMouseTransparent(true);
|
||||
pane.getChildren().add(nameLabel);
|
||||
nameLabel.textProperty().set(I18n.hasKey("install.installer." + control.id) ? i18n("install.installer." + control.id) : control.id);
|
||||
HBox.setMargin(nameLabel, new Insets(0, 4, 0, 4));
|
||||
|
||||
Label label = new Label();
|
||||
hbox.getChildren().add(label);
|
||||
label.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(label, Priority.ALWAYS);
|
||||
label.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
String incompatibleWith = control.incompatibleLibraryName.get();
|
||||
String version = control.libraryVersion.get();
|
||||
if (control.incompatibleWithGame.get()) {
|
||||
return i18n("install.installer.change_version", version);
|
||||
} else if (incompatibleWith != null) {
|
||||
return i18n("install.installer.incompatible", i18n("install.installer." + incompatibleWith));
|
||||
} else if (version == null) {
|
||||
Label statusLabel = new Label();
|
||||
statusLabel.getStyleClass().add("installer-item-status");
|
||||
statusLabel.setMouseTransparent(true);
|
||||
pane.getChildren().add(statusLabel);
|
||||
HBox.setHgrow(statusLabel, Priority.ALWAYS);
|
||||
statusLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
State state = control.resolvedStateProperty.get();
|
||||
|
||||
if (state instanceof InstalledState) {
|
||||
InstalledState s = (InstalledState) state;
|
||||
if (s.incompatibleWithGame) {
|
||||
return i18n("install.installer.change_version", s.version);
|
||||
}
|
||||
if (s.external) {
|
||||
return i18n("install.installer.external_version", s.version);
|
||||
}
|
||||
return i18n("install.installer.version", s.version);
|
||||
} else if (state instanceof InstallableState) {
|
||||
return i18n("install.installer.not_installed");
|
||||
} else if (state instanceof IncompatibleState) {
|
||||
return i18n("install.installer.incompatible", i18n("install.installer." + ((IncompatibleState) state).incompatibleItemName));
|
||||
} else {
|
||||
return i18n("install.installer.version", version);
|
||||
throw new AssertionError("Unknown state type: " + state.getClass());
|
||||
}
|
||||
}, control.incompatibleLibraryName, control.incompatibleWithGame, control.libraryVersion));
|
||||
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
}, control.resolvedStateProperty));
|
||||
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
|
||||
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
|
||||
|
||||
JFXButton closeButton = new JFXButton();
|
||||
closeButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
|
||||
closeButton.getStyleClass().add("toggle-icon4");
|
||||
closeButton.visibleProperty().bind(control.removable);
|
||||
closeButton.managedProperty().bind(closeButton.visibleProperty());
|
||||
closeButton.onMouseClickedProperty().bind(control.removeAction);
|
||||
hbox.getChildren().add(closeButton);
|
||||
HBox buttonsContainer = new HBox();
|
||||
buttonsContainer.setSpacing(8);
|
||||
buttonsContainer.setAlignment(Pos.CENTER);
|
||||
pane.getChildren().add(buttonsContainer);
|
||||
|
||||
JFXButton arrowButton = new JFXButton();
|
||||
arrowButton.graphicProperty().bind(Bindings.createObjectBinding(() -> control.upgradable.get()
|
||||
? SVG.update(Theme.blackFillBinding(), -1, -1)
|
||||
: SVG.arrowRight(Theme.blackFillBinding(), -1, -1),
|
||||
control.upgradable));
|
||||
arrowButton.getStyleClass().add("toggle-icon4");
|
||||
arrowButton.visibleProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> control.installable.get() && control.incompatibleLibraryName.get() == null,
|
||||
control.installable, control.incompatibleLibraryName));
|
||||
arrowButton.managedProperty().bind(arrowButton.visibleProperty());
|
||||
arrowButton.onMouseClickedProperty().bind(control.action);
|
||||
hbox.getChildren().add(arrowButton);
|
||||
JFXButton removeButton = new JFXButton();
|
||||
removeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1, -1));
|
||||
removeButton.getStyleClass().add("toggle-icon4");
|
||||
if (control.id.equals(MINECRAFT.getPatchId())) {
|
||||
removeButton.setVisible(false);
|
||||
} else {
|
||||
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty));
|
||||
}
|
||||
removeButton.managedProperty().bind(removeButton.visibleProperty());
|
||||
removeButton.setOnAction(e -> {
|
||||
Runnable onRemove = control.getOnRemove();
|
||||
if (onRemove != null)
|
||||
onRemove.run();
|
||||
});
|
||||
buttonsContainer.getChildren().add(removeButton);
|
||||
|
||||
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
|
||||
JFXButton installButton = new JFXButton();
|
||||
installButton.graphicProperty().bind(Bindings.createObjectBinding(() ->
|
||||
control.resolvedStateProperty.get() instanceof InstallableState ?
|
||||
SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), -1, -1) :
|
||||
SVG.UPDATE.createIcon(Theme.blackFill(), -1, -1),
|
||||
control.resolvedStateProperty
|
||||
));
|
||||
installButton.getStyleClass().add("toggle-icon4");
|
||||
installButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
if (control.getOnInstall() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
State state = control.resolvedStateProperty.get();
|
||||
if (state instanceof InstallableState) {
|
||||
return true;
|
||||
}
|
||||
if (state instanceof InstalledState) {
|
||||
return !((InstalledState) state).external;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, control.resolvedStateProperty, control.onInstall));
|
||||
installButton.managedProperty().bind(installButton.visibleProperty());
|
||||
installButton.setOnAction(e -> {
|
||||
Runnable onInstall = control.getOnInstall();
|
||||
if (onInstall != null)
|
||||
onInstall.run();
|
||||
});
|
||||
buttonsContainer.getChildren().add(installButton);
|
||||
|
||||
FXUtils.onChangeAndOperate(installButton.visibleProperty(), clickable -> {
|
||||
if (clickable) {
|
||||
hbox.onMouseClickedProperty().bind(control.action);
|
||||
hbox.setCursor(Cursor.HAND);
|
||||
container.setOnMouseClicked(event -> {
|
||||
Runnable onInstall = control.getOnInstall();
|
||||
if (onInstall != null && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
|
||||
onInstall.run();
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
pane.setCursor(Cursor.HAND);
|
||||
} else {
|
||||
hbox.onMouseClickedProperty().unbind();
|
||||
hbox.onMouseClickedProperty().set(null);
|
||||
hbox.setCursor(Cursor.DEFAULT);
|
||||
container.setOnMouseClicked(null);
|
||||
pane.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,17 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.Control;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.construct.SpinnerPane.FAILED_ACTION;
|
||||
|
||||
public class ListPageBase<T> extends Control {
|
||||
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
|
||||
private final StringProperty failedReason = new SimpleStringProperty(this, "failed");
|
||||
|
||||
public ObservableList<T> getItems() {
|
||||
return items.get();
|
||||
@@ -52,4 +54,35 @@ public class ListPageBase<T> extends Control {
|
||||
public BooleanProperty loadingProperty() {
|
||||
return loading;
|
||||
}
|
||||
|
||||
public String getFailedReason() {
|
||||
return failedReason.get();
|
||||
}
|
||||
|
||||
public StringProperty failedReasonProperty() {
|
||||
return failedReason;
|
||||
}
|
||||
|
||||
public void setFailedReason(String failedReason) {
|
||||
this.failedReason.set(failedReason);
|
||||
}
|
||||
|
||||
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
|
||||
return onFailedAction;
|
||||
}
|
||||
|
||||
public final void setOnFailedAction(EventHandler<Event> value) {
|
||||
onFailedActionProperty().set(value);
|
||||
}
|
||||
|
||||
public final EventHandler<Event> getOnFailedAction() {
|
||||
return onFailedActionProperty().get();
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(FAILED_ACTION, get());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -56,31 +55,30 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
VBox vBox = new VBox();
|
||||
{
|
||||
vBox.getStyleClass().add("card-list");
|
||||
vBox.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
vBox.setPickOnBounds(false);
|
||||
vBox.setPadding(new Insets(15));
|
||||
vBox.setSpacing(15);
|
||||
|
||||
JFXButton btnAdd = new JFXButton();
|
||||
FXUtils.setLimitWidth(btnAdd, 40);
|
||||
FXUtils.setLimitHeight(btnAdd, 40);
|
||||
btnAdd.getStyleClass().add("jfx-button-raised-round");
|
||||
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1));
|
||||
btnAdd.setOnMouseClicked(e -> skinnable.add());
|
||||
btnAdd.setGraphic(SVG.PLUS.createIcon(Theme.whiteFill(), -1, -1));
|
||||
btnAdd.setOnAction(e -> skinnable.add());
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
FXUtils.setLimitWidth(btnRefresh, 40);
|
||||
FXUtils.setLimitHeight(btnRefresh, 40);
|
||||
btnRefresh.getStyleClass().add("jfx-button-raised-round");
|
||||
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnRefresh.setGraphic(SVG.refresh(Theme.whiteFillBinding(), -1, -1));
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.whiteFill(), -1, -1));
|
||||
btnRefresh.setOnAction(e -> skinnable.refresh());
|
||||
|
||||
vBox.getChildren().setAll(btnAdd);
|
||||
|
||||
|
||||
@@ -25,153 +25,163 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class LogWindow extends Stage {
|
||||
|
||||
private final ArrayDeque<Log> logs = new ArrayDeque<>();
|
||||
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<Log4jLevel, SimpleIntegerProperty>(Log4jLevel.class) {
|
||||
{
|
||||
for (Log4jLevel level : Log4jLevel.values()) put(level, new SimpleIntegerProperty());
|
||||
private static final Log4jLevel[] LEVELS = {Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
|
||||
|
||||
private final CircularArrayList<Log> logs;
|
||||
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<>(Log4jLevel.class);
|
||||
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<>(Log4jLevel.class);
|
||||
|
||||
{
|
||||
for (Log4jLevel level : Log4jLevel.values()) {
|
||||
levelCountMap.put(level, new SimpleIntegerProperty());
|
||||
levelShownMap.put(level, new SimpleBooleanProperty(true));
|
||||
}
|
||||
};
|
||||
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<Log4jLevel, SimpleBooleanProperty>(Log4jLevel.class) {
|
||||
{
|
||||
for (Log4jLevel level : Log4jLevel.values()) {
|
||||
SimpleBooleanProperty property = new SimpleBooleanProperty(true);
|
||||
put(level, property);
|
||||
}
|
||||
}
|
||||
};
|
||||
private final LogWindowImpl impl = new LogWindowImpl();
|
||||
private final WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
|
||||
|
||||
private boolean stopCheckLogCount = false;
|
||||
|
||||
public LogWindow() {
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
|
||||
setTitle(i18n("logwindow.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
|
||||
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
|
||||
}
|
||||
|
||||
public void logLine(String line, Log4jLevel level) {
|
||||
Log log = new Log(parseEscapeSequence(line), level);
|
||||
private final LogWindowImpl impl;
|
||||
private final ManagedProcess gameProcess;
|
||||
|
||||
public LogWindow(ManagedProcess gameProcess) {
|
||||
this(gameProcess, new CircularArrayList<>());
|
||||
}
|
||||
|
||||
public LogWindow(ManagedProcess gameProcess, CircularArrayList<Log> logs) {
|
||||
this.logs = logs;
|
||||
this.impl = new LogWindowImpl();
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("logwindow.title"));
|
||||
FXUtils.setIcon(this);
|
||||
|
||||
for (SimpleBooleanProperty property : levelShownMap.values()) {
|
||||
property.addListener(o -> shakeLogs());
|
||||
}
|
||||
|
||||
this.gameProcess = gameProcess;
|
||||
}
|
||||
|
||||
public void logLine(Log log) {
|
||||
Log4jLevel level = log.getLevel();
|
||||
logs.add(log);
|
||||
if (levelShownMap.get(level).get())
|
||||
impl.listView.getItems().add(log);
|
||||
|
||||
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
|
||||
if (!stopCheckLogCount) checkLogCount();
|
||||
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
|
||||
property.set(property.get() + 1);
|
||||
checkLogCount();
|
||||
autoScroll();
|
||||
}
|
||||
|
||||
public void showGameCrashReport() {
|
||||
stopCheckLogCount = true;
|
||||
for (Log log : impl.listView.getItems()) {
|
||||
if (log.log.contains("Minecraft Crash Report")) {
|
||||
Platform.runLater(() -> {
|
||||
impl.listView.scrollTo(log);
|
||||
});
|
||||
break;
|
||||
}
|
||||
public void logLines(List<Log> logs) {
|
||||
for (Log log : logs) {
|
||||
Log4jLevel level = log.getLevel();
|
||||
this.logs.add(log);
|
||||
if (levelShownMap.get(level).get())
|
||||
impl.listView.getItems().add(log);
|
||||
|
||||
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
|
||||
property.set(property.get() + 1);
|
||||
}
|
||||
show();
|
||||
checkLogCount();
|
||||
autoScroll();
|
||||
}
|
||||
|
||||
private void shakeLogs() {
|
||||
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList()));
|
||||
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.getLevel()).get()).collect(Collectors.toList()));
|
||||
autoScroll();
|
||||
}
|
||||
|
||||
private void checkLogCount() {
|
||||
while (logs.size() > config().getLogLines()) {
|
||||
int nRemove = logs.size() - Log.getLogLines();
|
||||
if (nRemove <= 0)
|
||||
return;
|
||||
|
||||
ObservableList<Log> items = impl.listView.getItems();
|
||||
int itemsSize = items.size();
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < nRemove; i++) {
|
||||
Log removedLog = logs.removeFirst();
|
||||
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
|
||||
// TODO: fix O(n)
|
||||
impl.listView.getItems().remove(0);
|
||||
}
|
||||
if (itemsSize > count && items.get(count) == removedLog)
|
||||
count++;
|
||||
}
|
||||
|
||||
items.remove(0, count);
|
||||
}
|
||||
|
||||
private static class Log {
|
||||
private final String log;
|
||||
private final Log4jLevel level;
|
||||
|
||||
public Log(String log, Log4jLevel level) {
|
||||
this.log = log;
|
||||
this.level = level;
|
||||
}
|
||||
private void autoScroll() {
|
||||
if (!impl.listView.getItems().isEmpty() && impl.autoScroll.get())
|
||||
impl.listView.scrollTo(impl.listView.getItems().size() - 1);
|
||||
}
|
||||
|
||||
public class LogWindowImpl extends Control {
|
||||
private final class LogWindowImpl extends Control {
|
||||
|
||||
private ListView<Log> listView = new JFXListView<>();
|
||||
private BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
||||
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||
private JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||
private final ListView<Log> listView = new JFXListView<>();
|
||||
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
|
||||
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
|
||||
private final JFXComboBox<Integer> cboLines = new JFXComboBox<>();
|
||||
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
boolean flag = false;
|
||||
cboLines.getItems().setAll("500", "2000", "5000");
|
||||
for (String i : cboLines.getItems())
|
||||
if (Integer.toString(config().getLogLines()).equals(i)) {
|
||||
cboLines.getSelectionModel().select(i);
|
||||
flag = true;
|
||||
}
|
||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
|
||||
|
||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
||||
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
|
||||
});
|
||||
for (int i = 0; i < LEVELS.length; i++) {
|
||||
buttonText[i] = new SimpleStringProperty();
|
||||
showLevel[i] = new SimpleBooleanProperty(true);
|
||||
}
|
||||
|
||||
if (!flag)
|
||||
cboLines.getSelectionModel().select(0);
|
||||
cboLines.getItems().setAll(500, 2000, 5000, 10000);
|
||||
cboLines.setValue(Log.getLogLines());
|
||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(newValue));
|
||||
|
||||
Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
|
||||
String[] suffix = new String[]{"fatals", "errors", "warns", "infos", "debugs"};
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
buttonText.get(i).bind(Bindings.concat(levelCountMap.get(levels[i]), " " + suffix[i]));
|
||||
levelShownMap.get(levels[i]).bind(showLevel.get(i));
|
||||
for (int i = 0; i < LEVELS.length; ++i) {
|
||||
buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s"));
|
||||
levelShownMap.get(LEVELS[i]).bind(showLevel[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,19 +198,46 @@ public final class LogWindow extends Stage {
|
||||
thread(() -> {
|
||||
Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||
try {
|
||||
Files.write(logFile, logs.stream().map(x -> x.log).collect(Collectors.toList()));
|
||||
Files.write(logFile, logs.stream().map(Log::getLog).collect(Collectors.toList()));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to export logs", e);
|
||||
LOG.warning("Failed to export logs", e);
|
||||
return;
|
||||
}
|
||||
|
||||
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
});
|
||||
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
});
|
||||
}
|
||||
|
||||
private void onExportDump(SpinnerPane pane) {
|
||||
assert SystemUtils.supportJVMAttachment();
|
||||
|
||||
pane.setLoading(true);
|
||||
|
||||
thread(() -> {
|
||||
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||
|
||||
try {
|
||||
if (gameProcess.isRunning()) {
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
|
||||
FXUtils.showFileInExplorer(dumpFile);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to create minecraft jstack dump", e);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump"));
|
||||
alert.setTitle(i18n("message.error"));
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
Platform.runLater(() -> pane.setLoading(false));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -210,26 +247,19 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
}
|
||||
|
||||
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
||||
private static PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||
private static PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||
private static PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
||||
private static PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
private static final class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
||||
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||
private static final PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
||||
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
|
||||
ToggleButton button = new ToggleButton();
|
||||
button.setStyle("-fx-background-color: " + backgroundColor + ";");
|
||||
button.getStyleClass().add("log-toggle");
|
||||
button.textProperty().bind(buttonText);
|
||||
button.setSelected(true);
|
||||
showLevel.bind(button.selectedProperty());
|
||||
return button;
|
||||
}
|
||||
private final Set<ListCell<Log>> selected = new HashSet<>();
|
||||
|
||||
protected LogWindowSkin(LogWindowImpl control) {
|
||||
LogWindowSkin(LogWindowImpl control) {
|
||||
super(control);
|
||||
|
||||
VBox vbox = new VBox(3);
|
||||
@@ -254,13 +284,16 @@ public final class LogWindow extends Stage {
|
||||
|
||||
{
|
||||
HBox hBox = new HBox(3);
|
||||
hBox.getChildren().setAll(
|
||||
createToggleButton("#F7A699", control.buttonText.get(0), control.showLevel.get(0)),
|
||||
createToggleButton("#FFCCBB", control.buttonText.get(1), control.showLevel.get(1)),
|
||||
createToggleButton("#FFEECC", control.buttonText.get(2), control.showLevel.get(2)),
|
||||
createToggleButton("#FBFBFB", control.buttonText.get(3), control.showLevel.get(3)),
|
||||
createToggleButton("#EEE9E0", control.buttonText.get(4), control.showLevel.get(4))
|
||||
);
|
||||
for (int i = 0; i < LEVELS.length; i++) {
|
||||
ToggleButton button = new ToggleButton();
|
||||
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
|
||||
button.getStyleClass().add("log-toggle");
|
||||
button.textProperty().bind(control.buttonText[i]);
|
||||
button.setSelected(true);
|
||||
control.showLevel[i].bind(button.selectedProperty());
|
||||
hBox.getChildren().add(button);
|
||||
}
|
||||
|
||||
borderPane.setRight(hBox);
|
||||
}
|
||||
|
||||
@@ -273,10 +306,14 @@ public final class LogWindow extends Stage {
|
||||
if (!listView.getItems().isEmpty() && control.autoScroll.get())
|
||||
listView.scrollTo(listView.getItems().size() - 1);
|
||||
});
|
||||
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
|
||||
|
||||
listView.setStyle("-fx-font-family: " + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT)
|
||||
+ "; -fx-font-size: " + config().getFontSize() + "px;");
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||
{
|
||||
Region clippedContainer = (Region)listView.lookup(".clipped-container");
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
if (clippedContainer != null) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
@@ -284,32 +321,85 @@ public final class LogWindow extends Stage {
|
||||
setPadding(new Insets(2));
|
||||
setWrapText(true);
|
||||
setGraphic(null);
|
||||
|
||||
setOnMouseClicked(event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY)
|
||||
return;
|
||||
|
||||
if (!event.isControlDown()) {
|
||||
for (ListCell<Log> logListCell : selected) {
|
||||
if (logListCell != this) {
|
||||
logListCell.pseudoClassStateChanged(SELECTED, false);
|
||||
if (logListCell.getItem() != null) {
|
||||
logListCell.getItem().setSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected.clear();
|
||||
}
|
||||
|
||||
selected.add(this);
|
||||
pseudoClassStateChanged(SELECTED, true);
|
||||
if (getItem() != null) {
|
||||
getItem().setSelected(true);
|
||||
}
|
||||
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Log item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
pseudoClassStateChanged(EMPTY, empty);
|
||||
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
|
||||
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
|
||||
pseudoClassStateChanged(WARN, !empty && item.level == Log4jLevel.WARN);
|
||||
pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO);
|
||||
pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG);
|
||||
pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE);
|
||||
pseudoClassStateChanged(FATAL, !empty && item.getLevel() == Log4jLevel.FATAL);
|
||||
pseudoClassStateChanged(ERROR, !empty && item.getLevel() == Log4jLevel.ERROR);
|
||||
pseudoClassStateChanged(WARN, !empty && item.getLevel() == Log4jLevel.WARN);
|
||||
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
|
||||
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
|
||||
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
|
||||
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(item.log);
|
||||
setText(item.getLog());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
listView.setOnKeyPressed(event -> {
|
||||
if (event.isControlDown() && event.getCode() == KeyCode.C) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
for (Log item : listView.getItems()) {
|
||||
if (item != null && item.isSelected()) {
|
||||
if (item.getLog() != null)
|
||||
stringBuilder.append(item.getLog());
|
||||
stringBuilder.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
FXUtils.copyText(stringBuilder.toString());
|
||||
}
|
||||
});
|
||||
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
vbox.getChildren().add(listView);
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane bottom = new BorderPane();
|
||||
|
||||
HBox hBox = new HBox(3);
|
||||
bottom.setRight(hBox);
|
||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
hBox.setPadding(new Insets(0, 3, 0, 3));
|
||||
|
||||
@@ -317,17 +407,27 @@ public final class LogWindow extends Stage {
|
||||
autoScrollCheckBox.setSelected(true);
|
||||
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
|
||||
|
||||
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
|
||||
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
|
||||
|
||||
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
|
||||
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
|
||||
exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs());
|
||||
|
||||
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
|
||||
terminateButton.setOnAction(e -> getSkinnable().onTerminateGame());
|
||||
|
||||
SpinnerPane exportDumpPane = new SpinnerPane();
|
||||
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
|
||||
if (SystemUtils.supportJVMAttachment()) {
|
||||
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
|
||||
} else {
|
||||
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency")));
|
||||
exportDumpButton.setDisable(true);
|
||||
}
|
||||
exportDumpPane.setContent(exportDumpButton);
|
||||
|
||||
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
||||
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
|
||||
clearButton.setOnAction(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
|
||||
|
||||
vbox.getChildren().add(hBox);
|
||||
vbox.getChildren().add(bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
@@ -25,20 +25,105 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
|
||||
public final class SVG {
|
||||
private SVG() {
|
||||
public enum SVG {
|
||||
GEAR("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"),
|
||||
GEAR_OUTLINE("M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z"),
|
||||
BACK("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"),
|
||||
CANCEL("M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C10.1 4 8.4 4.6 7.1 5.7L18.3 16.9C19.3 15.5 20 13.8 20 12C20 7.6 16.4 4 12 4M16.9 18.3L5.7 7.1C4.6 8.4 4 10.1 4 12C4 16.4 7.6 20 12 20C13.9 20 15.6 19.4 16.9 18.3Z"),
|
||||
CHAT("M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M4,4V17.17L5.17,16H20V4H4M6,7H18V9H6V7M6,11H15V13H6V11Z"),
|
||||
CLOSE("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"),
|
||||
COPY("M15 16H7V6h8m.4-2H6.6A1.6 1.6 90 005 5.6V16.4a1.6 1.6 90 001.6 1.6h8.8A1.6 1.6 90 0017 16.4V5.6A1.6 1.6 90 0015.4 4M13 1H3.6A1.6 1.6 90 002 2.6V13h2V3H13V1Z"),
|
||||
DOTS_VERTICAL("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"),
|
||||
DOTS_HORIZONTAL("M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"),
|
||||
DELETE("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"),
|
||||
DELETE_OUTLINE("M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z"),
|
||||
ACCOUNT_EDIT("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z"),
|
||||
EXPAND("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"),
|
||||
COLLAPSE("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"),
|
||||
NAVIGATE("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
|
||||
ROCKET_LAUNCH_OUTLINE("M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M19.22 4C19.5 4 19.75 4 19.96 4.05C20.13 5.44 19.94 8.3 16.66 11.58C14.96 13.29 12.93 14.6 10.65 15.47L8.5 13.37C9.42 11.06 10.73 9.03 12.42 7.34C15.18 4.58 17.64 4 19.22 4M19.22 2C17.24 2 14.24 2.69 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.62 17.27 10.13 17.5 10.66 17.5C10.89 17.5 11.13 17.44 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39S20.7 2 19.22 2M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.47 5.64 14.12L2 17.76V19.17Z"),
|
||||
LAUNCH_OUTLINE("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
|
||||
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"),
|
||||
PENCIL("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z"),
|
||||
PENCIL_OUTLINE("M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z"),
|
||||
REFRESH("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"),
|
||||
FOLDER_OPEN("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z"),
|
||||
FOLDER_OUTLINE("M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z"),
|
||||
UPDATE("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z"),
|
||||
CLOSE_CIRCLE("M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"),
|
||||
CHECK_CIRCLE("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"),
|
||||
INFO_CIRCLE("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
|
||||
HELP_CIRCLE("M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"),
|
||||
HELP_CIRCLE_OUTLINE("M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z"),
|
||||
ALERT("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"),
|
||||
ALERT_OUTLINE("M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16"),
|
||||
PLUS("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"),
|
||||
PLUS_CIRCLE_OUTLINE("M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z"),
|
||||
IMPORT_ICON("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z"),
|
||||
EXPORT("M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z"),
|
||||
OPEN_IN_NEW("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
|
||||
TRIANGLE("M1,21H23L12,2"),
|
||||
HOME("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"),
|
||||
VIEW_LIST("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z"),
|
||||
CHECK("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"),
|
||||
ARROW_RIGHT("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z"),
|
||||
WRENCH("M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z"),
|
||||
WRENCH_OUTLINE("M22.61,19L13.53,9.91C14.46,7.57 14,4.81 12.09,2.91C9.79,0.61 6.21,0.4 3.66,2.26L7.5,6.11L6.08,7.5L2.25,3.69C0.39,6.23 0.6,9.82 2.9,12.11C4.76,13.97 7.47,14.46 9.79,13.59L18.9,22.7C19.29,23.09 19.92,23.09 20.31,22.7L22.61,20.4C23,20 23,19.39 22.61,19M19.61,20.59L10.15,11.13C9.54,11.58 8.86,11.85 8.15,11.95C6.79,12.15 5.36,11.74 4.32,10.7C3.37,9.76 2.93,8.5 3,7.26L6.09,10.35L10.33,6.11L7.24,3C8.5,2.95 9.73,3.39 10.68,4.33C11.76,5.41 12.17,6.9 11.92,8.29C11.8,9 11.5,9.66 11.04,10.25L20.5,19.7L19.61,20.59Z"),
|
||||
UPLOAD("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z"),
|
||||
HANGER("M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z"),
|
||||
PUZZLE("M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z"),
|
||||
CUBE("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z"),
|
||||
PACK("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z"),
|
||||
TEXTURE_BOX("M20 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V4C22 2.9 21.11 2 20 2M4 6L6 4H10.9L4 10.9V6M4 13.7L13.7 4H18.6L4 18.6V13.7M20 18L18 20H13.1L20 13.1V18M20 10.3L10.3 20H5.4L20 5.4V10.3Z"),
|
||||
GAMEPAD("M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z"),
|
||||
FIRE("M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z"),
|
||||
MONITOR_SCREENSHOT("M9,6H5V10H7V8H9M19,10H17V12H15V14H19M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2"),
|
||||
TEXTURE("M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z"),
|
||||
ALPHA_CIRCLE_OUTLINE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"),
|
||||
BETA_CIRCLE_OUTLINE("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"),
|
||||
RELEASE_CIRCLE_OUTLINE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"),
|
||||
INFORMATION_OUTLINE("M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"),
|
||||
HAND_HEAR_OUTLINE("M16 3.23C16.71 2.41 17.61 2 18.7 2C19.61 2 20.37 2.33 21 3C21.63 3.67 21.96 4.43 22 5.3C22 6 21.67 6.81 21 7.76S19.68 9.5 19.03 10.15C18.38 10.79 17.37 11.74 16 13C14.61 11.74 13.59 10.79 12.94 10.15S11.63 8.71 10.97 7.76C10.31 6.81 10 6 10 5.3C10 4.39 10.32 3.63 10.97 3C11.62 2.37 12.4 2.04 13.31 2C14.38 2 15.27 2.41 16 3.23M22 19V20L14 22.5L7 20.56V22H1V11H8.97L15.13 13.3C16.25 13.72 17 14.8 17 16H19C20.66 16 22 17.34 22 19M5 20V13H3V20H5M19.9 18.57C19.74 18.24 19.39 18 19 18H13.65C13.11 18 12.58 17.92 12.07 17.75L9.69 16.96L10.32 15.06L12.7 15.85C13 15.95 15 16 15 16C15 15.63 14.77 15.3 14.43 15.17L8.61 13H7V18.5L13.97 20.41L19.9 18.57Z"),
|
||||
MOJANG("m 13.965847,0 c -1.010612,0.82802228 -1.197232,2.1950303 -1.265172,3.4179557 0.02123,1.0021189 1.341654,1.2994159 1.953117,0.590289 C 15.091158,2.6579315 14.369242,1.2738804 13.965847,0 Z M 10.913012,2.9296764 C 10.755901,3.6982508 10.628413,4.4668999 10.492533,5.2354744 8.9893533,3.9913178 7.1504705,3.0824837 5.1419856,3.2947971 3.4944341,3.4646478 0.94293227,2.6961479 0.14038761,4.645185 -0.12288102,8.3139606 0.07223999,12.01236 0.03402357,15.689629 c -0.12314178,1.222925 0.86170213,2.420422 2.14407513,2.280295 4.2207899,0.03397 8.4502143,0.04723 12.6710043,-0.0038 1.265389,0.135918 1.957646,-1.010748 2.13599,-2.0893 C 13.269608,16.437357 9.1760813,16.929609 5.7111265,15.129192 2.5986124,13.58355 2.246023,8.3138817 5.5581114,6.7979639 9.3203049,5.1758896 13.859607,8.0382886 14.942405,11.787743 15.613316,12.11046 16.284433,12.424684 16.95959,12.743154 16.624135,10.348258 16.653651,7.800456 15.579346,5.5881508 15.057054,4.7473897 14.097531,6.2714182 13.379911,5.6217388 12.416008,4.865903 11.749527,3.8128948 10.913012,2.9296713 Z"),
|
||||
MICROSOFT("M2,3H11V12H2V3M11,22H2V13H11V22M21,3V12H12V3H21M21,22H12V13H21V22Z"),
|
||||
ACCOUNT_OUTLINE("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z"),
|
||||
ACCOUNT_GROUP_OUTLINE("M12,5A3.5,3.5 0 0,0 8.5,8.5A3.5,3.5 0 0,0 12,12A3.5,3.5 0 0,0 15.5,8.5A3.5,3.5 0 0,0 12,5M12,7A1.5,1.5 0 0,1 13.5,8.5A1.5,1.5 0 0,1 12,10A1.5,1.5 0 0,1 10.5,8.5A1.5,1.5 0 0,1 12,7M5.5,8A2.5,2.5 0 0,0 3,10.5C3,11.44 3.53,12.25 4.29,12.68C4.65,12.88 5.06,13 5.5,13C5.94,13 6.35,12.88 6.71,12.68C7.08,12.47 7.39,12.17 7.62,11.81C6.89,10.86 6.5,9.7 6.5,8.5C6.5,8.41 6.5,8.31 6.5,8.22C6.2,8.08 5.86,8 5.5,8M18.5,8C18.14,8 17.8,8.08 17.5,8.22C17.5,8.31 17.5,8.41 17.5,8.5C17.5,9.7 17.11,10.86 16.38,11.81C16.5,12 16.63,12.15 16.78,12.3C16.94,12.45 17.1,12.58 17.29,12.68C17.65,12.88 18.06,13 18.5,13C18.94,13 19.35,12.88 19.71,12.68C20.47,12.25 21,11.44 21,10.5A2.5,2.5 0 0,0 18.5,8M12,14C9.66,14 5,15.17 5,17.5V19H19V17.5C19,15.17 14.34,14 12,14M4.71,14.55C2.78,14.78 0,15.76 0,17.5V19H3V17.07C3,16.06 3.69,15.22 4.71,14.55M19.29,14.55C20.31,15.22 21,16.06 21,17.07V19H24V17.5C24,15.76 21.22,14.78 19.29,14.55M12,16C13.53,16 15.24,16.5 16.23,17H7.77C8.76,16.5 10.47,16 12,16Z"),
|
||||
ACCOUNT_ARROW_RIGHT_OUTLINE("M19,21V19H15V17H19V15L22,18L19,21M13,18C13,18.71 13.15,19.39 13.42,20H2V17C2,14.79 5.58,13 10,13C11,13 11.96,13.09 12.85,13.26C13.68,13.42 14.44,13.64 15.11,13.92C13.83,14.83 13,16.32 13,18M4,17V18H11C11,16.96 11.23,15.97 11.64,15.08L10,15C6.69,15 4,15.9 4,17M10,4A4,4 0 0,1 14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,6A2,2 0 0,0 8,8A2,2 0 0,0 10,10A2,2 0 0,0 12,8A2,2 0 0,0 10,6Z"),
|
||||
STYLE_OUTLINE("M2.5 19.6L3.8 20.2V11.2L1.4 17C1 18.1 1.5 19.2 2.5 19.6M15.2 4.8L20.2 16.8L12.9 19.8L7.9 7.9V7.8L15.2 4.8M15.3 2.8C15 2.8 14.8 2.8 14.5 2.9L7.1 6C6.4 6.3 5.9 7 5.9 7.8C5.9 8 5.9 8.3 6 8.6L11 20.5C11.3 21.3 12 21.7 12.8 21.7C13.1 21.7 13.3 21.7 13.6 21.6L21 18.5C22 18.1 22.5 16.9 22.1 15.9L17.1 4C16.8 3.2 16 2.8 15.3 2.8M10.5 9.9C9.9 9.9 9.5 9.5 9.5 8.9S9.9 7.9 10.5 7.9C11.1 7.9 11.5 8.4 11.5 8.9S11.1 9.9 10.5 9.9M5.9 19.8C5.9 20.9 6.8 21.8 7.9 21.8H9.3L5.9 13.5V19.8Z"),
|
||||
DOWNLOAD_OUTLINE("M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z"),
|
||||
APPLICATION_OUTLINE("M21 2H3C1.9 2 1 2.9 1 4V20C1 21.1 1.9 22 3 22H21C22.1 22 23 21.1 23 20V4C23 2.9 22.1 2 21 2M21 20H3V6H21V20Z"),
|
||||
EARTH("M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
|
||||
BELL("M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21"),
|
||||
CONTENT_SAVE_MOVE_OUTLINE("M13 17H17V14L22 18.5L17 23V20H13V17M14 12.8C13.5 12.31 12.78 12 12 12C10.34 12 9 13.34 9 15C9 16.31 9.84 17.41 11 17.82C11.07 15.67 12.27 13.8 14 12.8M11.09 19H5V5H16.17L19 7.83V12.35C19.75 12.61 20.42 13 21 13.54V7L17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H11.81C11.46 20.39 11.21 19.72 11.09 19M6 10H15V6H6V10Z"),
|
||||
ACCOUNT("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"),
|
||||
SERVER("M13,19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H4A1,1 0 0,1 3,16V12A1,1 0 0,1 4,11H20A1,1 0 0,1 21,12V16A1,1 0 0,1 20,17H13V19M4,3H20A1,1 0 0,1 21,4V8A1,1 0 0,1 20,9H4A1,1 0 0,1 3,8V4A1,1 0 0,1 4,3M9,7H10V5H9V7M9,15H10V13H9V15M5,5V7H7V5H5M5,13V15H7V13H5Z"),
|
||||
MESSAGE_ALERT_OUTLINE("M13,10H11V6H13V10M13,12H11V14H13V12M22,4V16A2,2 0 0,1 20,18H6L2,22V4A2,2 0 0,1 4,2H20A2,2 0 0,1 22,4M20,4H4V17.2L5.2,16H20V4Z"),
|
||||
CHECK_CIRCLE_OUTLINE("M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z"),
|
||||
CLOSE_CIRCLE_OUTLINE("M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z"),
|
||||
CLOCK_OUTLINE("M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z"),
|
||||
MAGNIFY("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"),
|
||||
MENU_UP("M7,15L12,10L17,15H7Z"),
|
||||
MENU_DOWN("M7,10L12,15L17,10H7Z"),
|
||||
RESTORE("M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z"),
|
||||
BUG("M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"),
|
||||
DISCORD("M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z"),
|
||||
LAN("M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z"),
|
||||
THUMB_UP_OUTLINE("M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z"),
|
||||
THUMB_DOWN_OUTLINE("M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z"),
|
||||
SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z"),
|
||||
JAVA("m 12.400746,23.498132 c 0,0 -1.047682,0.609185 0.745753,0.816159 2.173248,0.247515 3.284943,0.212301 5.680103,-0.241112 0,0 0.629462,0.394742 1.509642,0.737207 -5.369636,2.301245 -12.1518319,-0.133351 -7.934431,-1.312254 z m -0.656135,-3.003246 c 0,0 -1.175708,0.870569 0.619861,1.056206 2.321545,0.238977 4.155521,0.25925 7.32844,-0.35207 0,0 0.43849,0.444887 1.128765,0.688135 -6.492002,1.897965 -13.7233551,0.149351 -9.077066,-1.392271 z m 5.531806,-5.094318 c 1.322937,1.523496 -0.347807,2.894427 -0.347807,2.894427 0,0 3.359626,-1.733668 1.816908,-3.905821 -1.441364,-2.024926 -2.545591,-3.030987 3.43644,-6.5004569 0,0 -9.389665,2.3449859 -4.905541,7.5129179 z m 7.101193,10.318794 c 0,0 0.775626,0.639057 -0.854578,1.133019 C 20.42373,27.79123 10.623314,28.075018 7.9006204,26.88973 6.9222859,26.464048 8.75733,25.873001 9.3345153,25.749244 9.9362392,25.619093 10.280844,25.642568 10.280844,25.642568 9.1926195,24.875486 3.2457961,27.147927 7.2604887,27.798718 18.208875,29.573994 27.21766,26.999632 24.37761,25.718316 Z M 12.904316,17.383886 c 0,0 -4.9855587,1.184229 -1.765696,1.614178 1.359213,0.182426 4.069103,0.140825 6.594422,-0.07042 2.063359,-0.173899 4.135252,-0.544104 4.135252,-0.544104 0,0 -0.727616,0.311527 -1.253592,0.671064 -5.062375,1.331456 -14.8414526,0.711604 -12.02594,-0.649727 2.38129,-1.151156 4.316621,-1.019929 4.316621,-1.019929 z m 8.943706,4.998298 c 5.146658,-2.673583 2.766435,-5.24368 1.106361,-4.898013 -0.407551,0.08428 -0.58892,0.1579 -0.58892,0.1579 0,0 0.151501,-0.236845 0.439557,-0.339265 3.284942,-1.155424 5.812393,3.406524 -1.060485,5.213807 0,0 0.08003,-0.07147 0.103478,-0.134425 z M 18.744451,2.2855 c 0,0 2.849653,2.8506848 -2.703489,7.2344656 -4.453183,3.5164124 -1.015676,5.5221324 -0.0021,7.8127104 C 13.439893,14.987688 11.532301,12.92329 12.811497,11.001852 14.690284,8.1810394 19.893487,6.8143757 18.743384,2.2855 Z M 13.41002,29.628384 c 4.939684,0.315794 12.525242,-0.174974 12.70448,-2.512485 0,0 -0.345672,0.886571 -4.081906,1.589641 -4.216334,0.793754 -9.416337,0.700936 -12.4996379,0.192025 0,0 0.6315969,0.522768 3.8781309,0.730807 z");
|
||||
|
||||
private final String path;
|
||||
|
||||
SVG(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public interface SVGIcon {
|
||||
Node createIcon(ObjectBinding<? extends Paint> fill, double width, double height);
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
SVGPath path = new SVGPath();
|
||||
path.getStyleClass().add("svg");
|
||||
path.setContent(d);
|
||||
if (fill != null) path.fillProperty().bind(fill);
|
||||
|
||||
private static Node createIcon(SVGPath path, double width, double height) {
|
||||
if (width < 0 || height < 0) {
|
||||
StackPane pane = new StackPane(path);
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
@@ -46,152 +131,31 @@ public final class SVG {
|
||||
}
|
||||
|
||||
Group svg = new Group(path);
|
||||
double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight());
|
||||
double scale = Math.min(width / 24, height / 24);
|
||||
svg.setScaleX(scale);
|
||||
svg.setScaleY(scale);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
// default fill: white, width: 20, height 20
|
||||
public Node createIcon(ObservableValue<? extends Paint> fill, double width, double height) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.getStyleClass().add("svg");
|
||||
p.setContent(path);
|
||||
if (fill != null)
|
||||
p.fillProperty().bind(fill);
|
||||
|
||||
public static Node gear(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height);
|
||||
return createIcon(p, width, height);
|
||||
}
|
||||
|
||||
public static Node back(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height);
|
||||
public Node createIcon(Paint fill, double width, double height) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.getStyleClass().add("svg");
|
||||
p.setContent(path);
|
||||
if (fill != null)
|
||||
p.fillProperty().set(fill);
|
||||
|
||||
return createIcon(p, width, height);
|
||||
}
|
||||
|
||||
public static Node close(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node copy(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node dotsHorizontal(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node delete(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node accountEdit(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node expand(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node collapse(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node navigate(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node launch(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node launch2(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node script(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node pencil(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node refresh(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node folderOpen(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node update(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node closeCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node checkCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node infoCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node helpCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node alert(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node plus(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node importIcon(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node export(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node openInNew(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node triangle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M1,21H23L12,2", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node home(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node arrowRight(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node wrench(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node upload(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node hanger(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z", fill, width, height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copy from https://github.com/palexdev/MaterialFX/blob/c8038ce2090f5cddf923a19d79cc601db86a4d17/materialfx/src/main/java/io/github/palexdev/materialfx/utils/ScrollUtils.java
|
||||
|
||||
/*
|
||||
* Copyright (C) 2022 Parisi Alessandro
|
||||
* This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
|
||||
*
|
||||
* MaterialFX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MaterialFX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Animation.Status;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Utility class for ScrollPanes.
|
||||
*/
|
||||
final class ScrollUtils {
|
||||
|
||||
public enum ScrollDirection {
|
||||
UP(-1), RIGHT(-1), DOWN(1), LEFT(1);
|
||||
|
||||
final int intDirection;
|
||||
|
||||
ScrollDirection(int intDirection) {
|
||||
this.intDirection = intDirection;
|
||||
}
|
||||
|
||||
public int intDirection() {
|
||||
return intDirection;
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given ScrollEvent comes from a trackpad.
|
||||
* <p></p>
|
||||
* Although this method works in most cases, it is not very accurate.
|
||||
* Since in JavaFX there's no way to tell if a ScrollEvent comes from a trackpad or a mouse
|
||||
* we use this trick: I noticed that a mouse scroll has a delta of 32 (don't know if it changes depending on the device or OS)
|
||||
* and trackpad scrolls have a way smaller delta. So depending on the scroll direction we check if the delta is lesser than 10
|
||||
* (trackpad event) or greater(mouse event).
|
||||
*
|
||||
* @see ScrollEvent#getDeltaX()
|
||||
* @see ScrollEvent#getDeltaY()
|
||||
*/
|
||||
public static boolean isTrackPad(ScrollEvent event, ScrollDirection scrollDirection) {
|
||||
switch (scrollDirection) {
|
||||
case UP:
|
||||
case DOWN:
|
||||
return Math.abs(event.getDeltaY()) < 10;
|
||||
case LEFT:
|
||||
case RIGHT:
|
||||
return Math.abs(event.getDeltaX()) < 10;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the scroll direction of the given ScrollEvent.
|
||||
* <p></p>
|
||||
* Although this method works fine, it is not very accurate.
|
||||
* In JavaFX there's no concept of scroll direction, if you try to scroll with a trackpad
|
||||
* you'll notice that you can scroll in both directions at the same time, both deltaX and deltaY won't be 0.
|
||||
* <p></p>
|
||||
* For this method to work we assume that this behavior is not possible.
|
||||
* <p></p>
|
||||
* If deltaY is 0 we return LEFT or RIGHT depending on deltaX (respectively if lesser or greater than 0).
|
||||
* <p>
|
||||
* Else we return DOWN or UP depending on deltaY (respectively if lesser or greater than 0).
|
||||
*
|
||||
* @see ScrollEvent#getDeltaX()
|
||||
* @see ScrollEvent#getDeltaY()
|
||||
*/
|
||||
public static ScrollDirection determineScrollDirection(ScrollEvent event) {
|
||||
double deltaX = event.getDeltaX();
|
||||
double deltaY = event.getDeltaY();
|
||||
|
||||
if (deltaY == 0.0) {
|
||||
return deltaX < 0 ? ScrollDirection.LEFT : ScrollDirection.RIGHT;
|
||||
} else {
|
||||
return deltaY < 0 ? ScrollDirection.DOWN : ScrollDirection.UP;
|
||||
}
|
||||
}
|
||||
|
||||
//================================================================================
|
||||
// ScrollPanes
|
||||
//================================================================================
|
||||
|
||||
/**
|
||||
* Adds a smooth scrolling effect to the given scroll pane,
|
||||
* calls {@link #addSmoothScrolling(ScrollPane, double)} with a
|
||||
* default speed value of 1.
|
||||
*/
|
||||
public static void addSmoothScrolling(ScrollPane scrollPane) {
|
||||
addSmoothScrolling(scrollPane, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a smooth scrolling effect to the given scroll pane with the given scroll speed.
|
||||
* Calls {@link #addSmoothScrolling(ScrollPane, double, double)}
|
||||
* with a default trackPadAdjustment of 7.
|
||||
*/
|
||||
public static void addSmoothScrolling(ScrollPane scrollPane, double speed) {
|
||||
addSmoothScrolling(scrollPane, speed, 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a smooth scrolling effect to the given scroll pane with the given
|
||||
* scroll speed and the given trackPadAdjustment.
|
||||
* <p></p>
|
||||
* The trackPadAdjustment is a value used to slow down the scrolling if a trackpad is used.
|
||||
* This is kind of a workaround and it's not perfect, but at least it's way better than before.
|
||||
* The default value is 7, tested up to 10, further values can cause scrolling misbehavior.
|
||||
*/
|
||||
public static void addSmoothScrolling(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
|
||||
smoothScroll(scrollPane, speed, trackPadAdjustment);
|
||||
}
|
||||
|
||||
private static void smoothScroll(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
|
||||
final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
|
||||
final double[] derivatives = new double[frictions.length];
|
||||
AtomicReference<Double> atomicSpeed = new AtomicReference<>(speed);
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
AtomicReference<ScrollDirection> scrollDirection = new AtomicReference<>();
|
||||
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
|
||||
final EventHandler<ScrollEvent> scrollHandler = event -> {
|
||||
if (event.getEventType() == ScrollEvent.SCROLL) {
|
||||
scrollDirection.set(determineScrollDirection(event));
|
||||
if (isTrackPad(event, scrollDirection.get())) {
|
||||
atomicSpeed.set(speed / trackPadAdjustment);
|
||||
} else {
|
||||
atomicSpeed.set(speed);
|
||||
}
|
||||
derivatives[0] += scrollDirection.get().intDirection * atomicSpeed.get();
|
||||
if (timeline.getStatus() == Status.STOPPED) {
|
||||
timeline.play();
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
};
|
||||
if (scrollPane.getContent().getParent() != null) {
|
||||
scrollPane.getContent().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (oldValue != null) {
|
||||
oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
if (newValue != null) {
|
||||
newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
});
|
||||
|
||||
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> {
|
||||
for (int i = 0; i < derivatives.length; i++) {
|
||||
derivatives[i] *= frictions[i];
|
||||
}
|
||||
for (int i = 1; i < derivatives.length; i++) {
|
||||
derivatives[i] += derivatives[i - 1];
|
||||
}
|
||||
|
||||
double dy = derivatives[derivatives.length - 1];
|
||||
Function<Bounds, Double> sizeFunction = (scrollDirection.get() == ScrollDirection.UP || scrollDirection.get() == ScrollDirection.DOWN) ? Bounds::getHeight : Bounds::getWidth;
|
||||
double size = sizeFunction.apply(scrollPane.getContent().getLayoutBounds());
|
||||
double value;
|
||||
switch (scrollDirection.get()) {
|
||||
case LEFT:
|
||||
case RIGHT:
|
||||
value = Math.min(Math.max(scrollPane.hvalueProperty().get() + dy / size, 0), 1);
|
||||
scrollPane.hvalueProperty().set(value);
|
||||
break;
|
||||
case UP:
|
||||
case DOWN:
|
||||
value = Math.min(Math.max(scrollPane.vvalueProperty().get() + dy / size, 0), 1);
|
||||
scrollPane.vvalueProperty().set(value);
|
||||
break;
|
||||
}
|
||||
|
||||
if (Math.abs(dy) < 0.001) {
|
||||
timeline.stop();
|
||||
}
|
||||
}));
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public final class SwingUtils {
|
||||
static {
|
||||
if (System.getProperty("swing.defaultlaf") == null) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SwingUtils() {
|
||||
}
|
||||
|
||||
public static void initLookAndFeel() {
|
||||
// Make sure the static constructor is called
|
||||
}
|
||||
|
||||
public static void showInfoDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Info", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showWarningDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
@@ -18,18 +18,18 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
import java.util.List;
|
||||
@@ -40,37 +40,39 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
|
||||
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
|
||||
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
StackPane.setMargin(root, new Insets(10));
|
||||
|
||||
List<Node> toolbarButtons = initializeToolbar(skinnable);
|
||||
if (!toolbarButtons.isEmpty()) {
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.getStyleClass().add("jfx-tool-bar-second");
|
||||
JFXDepthManager.setDepth(toolbar, 1);
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
toolbar.setPickOnBounds(false);
|
||||
toolbar.getChildren().setAll(toolbarButtons);
|
||||
root.setTop(toolbar);
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
{
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
ComponentList.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
VBox content = new VBox();
|
||||
content.setSpacing(10);
|
||||
content.setPadding(new Insets(10));
|
||||
|
||||
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
|
||||
root.setCenter(scrollPane);
|
||||
root.getContent().add(scrollPane);
|
||||
}
|
||||
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.setContent(root);
|
||||
|
||||
getChildren().setAll(spinnerPane);
|
||||
@@ -83,23 +85,32 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton(String text, SVG.SVGIcon creator, Runnable onClick) {
|
||||
public static JFXButton createToolbarButton(String text, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
|
||||
ret.setText(text);
|
||||
ret.setOnMouseClicked(e -> onClick.run());
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, Runnable onClick) {
|
||||
public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1, -1)));
|
||||
ret.setText(text);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-decorator-button");
|
||||
ret.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
|
||||
FXUtils.installFastTooltip(ret, tooltip);
|
||||
ret.setOnMouseClicked(e -> onClick.run());
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public class TooltipInstaller {
|
||||
public static final TooltipInstaller INSTALLER;
|
||||
|
||||
static {
|
||||
TooltipInstaller installer = null;
|
||||
|
||||
try {
|
||||
installer = new NewInstaller();
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
installer = new OldInstaller();
|
||||
} catch (Throwable e2) {
|
||||
e2.addSuppressed(e);
|
||||
LOG.warning("Failed to initialize TooltipInstaller", e2);
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLER = installer != null ? installer : new TooltipInstaller();
|
||||
}
|
||||
|
||||
TooltipInstaller() {
|
||||
}
|
||||
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
|
||||
// For Java 8
|
||||
private static final class OldInstaller extends TooltipInstaller {
|
||||
private static final Constructor<?> createTooltipBehavior;
|
||||
private static final Method installTooltipBehavior;
|
||||
|
||||
static {
|
||||
try {
|
||||
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
|
||||
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
|
||||
createTooltipBehavior.setAccessible(true);
|
||||
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
|
||||
installTooltipBehavior.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
try {
|
||||
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
|
||||
installTooltipBehavior.invoke(behavior, node, tooltip);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
LOG.warning("Failed to set tooltip show delay", e);
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For Java 9+
|
||||
private static final class NewInstaller extends TooltipInstaller {
|
||||
private static final MethodHandle setTooltipShowDelay;
|
||||
private static final MethodHandle setTooltipShowDuration;
|
||||
private static final MethodHandle setTooltipHideDelay;
|
||||
|
||||
static {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
MethodType methodType = MethodType.methodType(void.class, Duration.class);
|
||||
|
||||
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
|
||||
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
|
||||
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
|
||||
} catch (Exception e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
try {
|
||||
setTooltipShowDelay.invokeExact(tooltip, showDelay);
|
||||
setTooltipShowDuration.invokeExact(tooltip, showDuration);
|
||||
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to set tooltip show delay", e);
|
||||
}
|
||||
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,46 +20,72 @@ package org.jackhuang.hmcl.ui;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Node;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class UpgradeDialog extends JFXDialogLayout {
|
||||
public UpgradeDialog(Runnable updateRunnable) {
|
||||
{
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
}
|
||||
public final class UpgradeDialog extends JFXDialogLayout {
|
||||
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
setBody(new ProgressIndicator());
|
||||
|
||||
{
|
||||
WebView webView = new WebView();
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
WebEngine engine = webView.getEngine();
|
||||
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
|
||||
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
|
||||
String url = engine.getLoadWorker().getMessage().trim();
|
||||
if (!url.startsWith(CHANGELOG_URL)) {
|
||||
engine.getLoadWorker().cancel();
|
||||
FXUtils.openLink(url);
|
||||
}
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
Document document = Jsoup.parse(new URL(url), 30 * 1000);
|
||||
Node node = document.selectFirst("#nowchange");
|
||||
if (node == null)
|
||||
throw new IOException("Cannot find #nowchange in document");
|
||||
|
||||
HTMLRenderer renderer = new HTMLRenderer(uri -> {
|
||||
LOG.info("Open link: " + uri);
|
||||
FXUtils.openLink(uri.toString());
|
||||
});
|
||||
setBody(webView);
|
||||
}
|
||||
|
||||
{
|
||||
JFXButton updateButton = new JFXButton(i18n("update.accept"));
|
||||
updateButton.getStyleClass().add("dialog-accept");
|
||||
updateButton.setOnMouseClicked(e -> updateRunnable.run());
|
||||
do {
|
||||
renderer.appendNode(node);
|
||||
node = node.nextSibling();
|
||||
} while (node != null);
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
|
||||
return renderer.render();
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
ScrollPane scrollPane = new ScrollPane(result);
|
||||
scrollPane.setFitToWidth(true);
|
||||
setBody(scrollPane);
|
||||
} else {
|
||||
LOG.warning("Failed to load update log, trying to open it in browser");
|
||||
FXUtils.openLink(url);
|
||||
setBody();
|
||||
}
|
||||
}).start();
|
||||
|
||||
setActions(updateButton, cancelButton);
|
||||
}
|
||||
JFXHyperlink openInBrowser = new JFXHyperlink(i18n("web.view_in_browser"));
|
||||
openInBrowser.setExternalLink(url);
|
||||
|
||||
JFXButton updateButton = new JFXButton(i18n("update.accept"));
|
||||
updateButton.getStyleClass().add("dialog-accept");
|
||||
updateButton.setOnAction(e -> updateRunnable.run());
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
|
||||
setActions(openInBrowser, updateButton, cancelButton);
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
}
|
||||
}
|
||||