Compare commits
1897
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9706a799be | ||
|
|
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 | ||
|
|
61597cb83d | ||
|
|
2f17552b2c | ||
|
|
f2d3d24338 | ||
|
|
e8a64f52f0 | ||
|
|
f55fe0fc45 | ||
|
|
1e3a5f9515 | ||
|
|
a9c7eb0954 | ||
|
|
289472d8ba | ||
|
|
f191e80100 | ||
|
|
607248e8b3 | ||
|
|
cecabe5bc4 | ||
|
|
947a2e3d91 | ||
|
|
382e0f3b1d | ||
|
|
6e440783dc | ||
|
|
fcd7d8ba83 | ||
|
|
a53d2a1370 | ||
|
|
20148d0b02 | ||
|
|
3114e33067 | ||
|
|
e137d72b76 | ||
|
|
cc087dee81 | ||
|
|
eed475cb28 | ||
|
|
d07c45f0b5 | ||
|
|
0aa0fa1b78 | ||
|
|
e91ec13dbd | ||
|
|
e36105d937 | ||
|
|
07d4df312d | ||
|
|
b521c5b76a | ||
|
|
c1596443f5 | ||
|
|
cadeb1f59c | ||
|
|
2d4223f7ed | ||
|
|
c453bbc7cd | ||
|
|
2184961e3c | ||
|
|
8afd8bde8b | ||
|
|
65952a5d81 | ||
|
|
ac1f2ec1ed | ||
|
|
454cc1e702 | ||
|
|
e3fa7428bf | ||
|
|
7ece35e28a | ||
|
|
d22dae9834 | ||
|
|
63bd707741 | ||
|
|
734c1e14f0 | ||
|
|
46869ad5f7 | ||
|
|
13fa713d58 | ||
|
|
94cd33af5c | ||
|
|
7efd9607e5 | ||
|
|
94539ad77b | ||
|
|
f2a86857bc | ||
|
|
3f2be63d08 | ||
|
|
2d7c1393d6 | ||
|
|
3d33230e25 | ||
|
|
dd4683e693 | ||
|
|
8bc5d2112f | ||
|
|
a828896989 | ||
|
|
5e8734aa95 | ||
|
|
1ac844f398 | ||
|
|
3f85ac208c | ||
|
|
c191186023 | ||
|
|
4715a95a54 | ||
|
|
8108f11c45 | ||
|
|
f4f1850271 | ||
|
|
5a81972f68 | ||
|
|
674dfca34d | ||
|
|
cafcb74e22 | ||
|
|
88c595dab2 | ||
|
|
48e2fcc601 | ||
|
|
2f10948239 | ||
|
|
7f674f237b | ||
|
|
c3f19d2c4a | ||
|
|
94b2cafa7a | ||
|
|
1d66ebef58 | ||
|
|
66459a124c | ||
|
|
5bbb565bd4 | ||
|
|
58819b6361 | ||
|
|
2de6a45ca1 | ||
|
|
c60261d917 | ||
|
|
94b6d7596e | ||
|
|
ae63b9712e | ||
|
|
00b945f6e3 | ||
|
|
37c47fc2dc | ||
|
|
51325d2423 | ||
|
|
469a7df4d4 | ||
|
|
ec56e24b7e | ||
|
|
00c350cd82 | ||
|
|
55d78aeba7 | ||
|
|
85c01cde00 | ||
|
|
4ce0a83845 | ||
|
|
d6859d1956 | ||
|
|
61447f0964 | ||
|
|
22a02faced | ||
|
|
a60c288335 | ||
|
|
3c224198e7 | ||
|
|
dc03f28d56 | ||
|
|
bd18355a1a | ||
|
|
1208d6dabc | ||
|
|
63082f5a5e | ||
|
|
c24ef2a200 | ||
|
|
891b64ee0f | ||
|
|
82763f3c32 | ||
|
|
3119b4d88d | ||
|
|
6532dc72db | ||
|
|
fd24fcc0f2 | ||
|
|
0d3e204f0d | ||
|
|
64d9fa7c6b | ||
|
|
53ff657106 | ||
|
|
6eb0c16fcc | ||
|
|
0984c23253 | ||
|
|
5805914ebb | ||
|
|
bfb55a3813 | ||
|
|
476c41b29b | ||
|
|
a2ef477ae0 | ||
|
|
42d42e9750 | ||
|
|
182e8ba2e8 | ||
|
|
e6d11fe9a3 | ||
|
|
7c510f9fb8 | ||
|
|
b53a3ebcc2 | ||
|
|
8e9f706d37 | ||
|
|
8997895a82 | ||
|
|
94db91b7c1 | ||
|
|
4f70465395 | ||
|
|
b2693cd3f9 | ||
|
|
3ae1c3467b | ||
|
|
1bba1b427c | ||
|
|
63e2fdf56d | ||
|
|
cf1b91ec55 | ||
|
|
4bac9380d1 | ||
|
|
fec5c9a5ce | ||
|
|
810ea9ace9 | ||
|
|
b933938f1b | ||
|
|
c408cccd35 | ||
|
|
bc842437fa | ||
|
|
9bda50f66e | ||
|
|
886ebe6885 | ||
|
|
d574d8d78b | ||
|
|
a3b74e0c75 | ||
|
|
eb9d123176 | ||
|
|
383de6cb2e | ||
|
|
43de254a98 | ||
|
|
02cb2a3fb1 | ||
|
|
62b6c07401 | ||
|
|
e630c6dff7 | ||
|
|
0639d82914 | ||
|
|
2701a84b94 | ||
|
|
859da37d71 | ||
|
|
5893eae87f | ||
|
|
99f42db370 | ||
|
|
32e9076198 | ||
|
|
e1cacad6b8 | ||
|
|
02ce950020 | ||
|
|
1eeba344f7 | ||
|
|
f84c4310f9 | ||
|
|
97e8ff91ea | ||
|
|
1c865e3d8d | ||
|
|
6ca7a86189 | ||
|
|
3c716ea170 | ||
|
|
3a7800db61 | ||
|
|
b04430528e | ||
|
|
4382326dec | ||
|
|
5adbac3cc9 | ||
|
|
d8b84a2574 | ||
|
|
0d59e52ff9 | ||
|
|
feb30a7349 | ||
|
|
3082e4cd16 | ||
|
|
c28dfd0ae0 | ||
|
|
986a430e92 | ||
|
|
80f27f5ea4 | ||
|
|
4c44a3c410 | ||
|
|
14cfd6abeb | ||
|
|
b957ca1931 | ||
|
|
9372d818a5 | ||
|
|
aee9c9bb32 | ||
|
|
5ed42d1654 | ||
|
|
dd94d2254f | ||
|
|
bf4c831047 | ||
|
|
33df069803 | ||
|
|
c360a4abb0 | ||
|
|
d29a198f7f | ||
|
|
8df651c5a8 | ||
|
|
191b88ab2a | ||
|
|
d00b269786 | ||
|
|
cbc85dd99e | ||
|
|
cf25e6c27b | ||
|
|
76807e1cd2 | ||
|
|
d84877dfcc | ||
|
|
f38d3b0026 | ||
|
|
511b555255 | ||
|
|
e79206bcb6 | ||
|
|
d8fa97e8c7 | ||
|
|
be2b55cc67 | ||
|
|
e68a3c7cd4 | ||
|
|
b3bf9d5c94 | ||
|
|
e166651fe7 | ||
|
|
516c73ee15 | ||
|
|
ac532c9400 | ||
|
|
b2f6ef72c3 | ||
|
|
3e2bb9678d | ||
|
|
0074f12a14 | ||
|
|
6a982b2a5c | ||
|
|
bde76707f3 | ||
|
|
2bc7bdba8a | ||
|
|
2e4f6ce076 | ||
|
|
5d3a382d26 | ||
|
|
ee8be3166f | ||
|
|
8fd500e931 | ||
|
|
2b9f070279 | ||
|
|
a53603dbbf | ||
|
|
4332bc124e | ||
|
|
20d0a2ae62 | ||
|
|
646bfa5be7 | ||
|
|
16d92454f4 | ||
|
|
129ce9a545 | ||
|
|
7b6a96b4b6 | ||
|
|
7260ae9b5e | ||
|
|
d333f54430 | ||
|
|
3efec78732 | ||
|
|
4f51ab00a1 | ||
|
|
6857200d75 | ||
|
|
1c3b3bf050 | ||
|
|
e11a5389c6 | ||
|
|
120238b234 | ||
|
|
c093c6e23d | ||
|
|
11d47bf9b1 | ||
|
|
fa13b1fc0d | ||
|
|
334e93196b | ||
|
|
de6cdeab52 | ||
|
|
24a6c2d3ba | ||
|
|
58cdc68b6e | ||
|
|
7389e210a7 | ||
|
|
b117769711 | ||
|
|
1ca4045a2f | ||
|
|
fb4a499fe9 | ||
|
|
4aac45f3b4 | ||
|
|
506786e932 | ||
|
|
1726cc7ec7 | ||
|
|
5b1c72ad5c | ||
|
|
0fa324452f | ||
|
|
6ad5ba3736 | ||
|
|
5707e1d57e | ||
|
|
f3424e0796 | ||
|
|
0b0118446f | ||
|
|
5c19dcd208 | ||
|
|
0a68e52fca | ||
|
|
5c25ab3eda | ||
|
|
91682aeafa | ||
|
|
cd9180c69a | ||
|
|
3269841207 | ||
|
|
b4ec71d65f | ||
|
|
1a0be11ae3 | ||
|
|
b72d42a09a | ||
|
|
ed5e225cdc | ||
|
|
c26579bebd | ||
|
|
1ea9170b2e | ||
|
|
a144a5eee0 | ||
|
|
1b1f532988 | ||
|
|
ca577475fc | ||
|
|
bc4b41e8cd | ||
|
|
a1226012ff | ||
|
|
cfd51915d3 | ||
|
|
030d54bc5c | ||
|
|
97a5d7e71a | ||
|
|
8b16e8be4d | ||
|
|
89dbc1be4a | ||
|
|
25bc8f5cb3 | ||
|
|
37d62b665e | ||
|
|
aa2838d219 | ||
|
|
979e58edf1 | ||
|
|
0f72d98067 | ||
|
|
eea401f99d | ||
|
|
b1301dcdec | ||
|
|
bdb6784a9b | ||
|
|
0d906e0adf | ||
|
|
4923370897 | ||
|
|
bdee33456b | ||
|
|
5a8446524a | ||
|
|
7c75e3d4a0 | ||
|
|
e67a1d16f5 | ||
|
|
b864ea89d7 | ||
|
|
04aa257b48 | ||
|
|
1b466eb33f | ||
|
|
a162828f0a | ||
|
|
b154871207 | ||
|
|
c7e3639155 | ||
|
|
9c1905bb51 | ||
|
|
31e39900b6 | ||
|
|
2b604bfd93 | ||
|
|
29f138ab94 | ||
|
|
8325021f9d | ||
|
|
25e4fa3834 | ||
|
|
9849b5659b | ||
|
|
43428c054a | ||
|
|
1d579c52eb | ||
|
|
214ff9de94 | ||
|
|
bb2e74009e | ||
|
|
53366ee284 | ||
|
|
8ec9a7a218 | ||
|
|
f3434fe2a8 | ||
|
|
acda1952ca | ||
|
|
084b15e113 | ||
|
|
d6d8c60e5b | ||
|
|
d3f24f0147 | ||
|
|
30499aa623 | ||
|
|
20d5957331 | ||
|
|
d859eeeb19 | ||
|
|
076733e6ce | ||
|
|
8fab4349bf | ||
|
|
829d92c8bb | ||
|
|
71131dadb7 | ||
|
|
61abed04f3 | ||
|
|
6e79d287f3 | ||
|
|
30f8657174 | ||
|
|
f516854226 | ||
|
|
0cee859e15 | ||
|
|
7b987c9b95 | ||
|
|
8cdd8b3f70 | ||
|
|
956b4e4a6a | ||
|
|
4e0ffc8d1e | ||
|
|
177935241f | ||
|
|
f133fcf66c | ||
|
|
869f57cbf1 | ||
|
|
de7c2e4f07 | ||
|
|
8bc2650f7b | ||
|
|
cef9f0b2bb | ||
|
|
e36b28c957 | ||
|
|
a770a7ee5c | ||
|
|
e3e9a19f6f | ||
|
|
4772a144ae | ||
|
|
d32ba75d5f | ||
|
|
96f869278b | ||
|
|
df1f9c76ad | ||
|
|
0b934bf6b5 | ||
|
|
f5319fa94c | ||
|
|
28229471f4 | ||
|
|
d00c61b89f | ||
|
|
1f7d3591b9 | ||
|
|
4e537de894 | ||
|
|
e130a14ebe | ||
|
|
31aa2f3a27 | ||
|
|
1f9c94dbc4 | ||
|
|
a2540c2e9b | ||
|
|
4737240c91 | ||
|
|
944d88b8af | ||
|
|
529c4ed420 | ||
|
|
e0ebb02530 | ||
|
|
be22d55cec | ||
|
|
993ce660aa | ||
|
|
79e120dd74 | ||
|
|
d4155bc14d | ||
|
|
ba699c4ed5 | ||
|
|
bc96afd0cd | ||
|
|
cf1a66e858 | ||
|
|
5eba896e6d | ||
|
|
e69d149c34 | ||
|
|
2f9955febd | ||
|
|
df39be708f | ||
|
|
ce3b9e3b1f | ||
|
|
a9feb65fc8 | ||
|
|
93837a28fa | ||
|
|
678f3b8eb7 | ||
|
|
0833fa9e7d | ||
|
|
369b9bb6cf | ||
|
|
c9b6f2f62f | ||
|
|
e2787af044 | ||
|
|
30c27bbb67 | ||
|
|
7aee7e33d0 | ||
|
|
a26061c388 | ||
|
|
11e66ac13c | ||
|
|
a2ac3c1f18 | ||
|
|
e7142e4032 | ||
|
|
0dd9dc6c17 | ||
|
|
a53bfed060 | ||
|
|
c5919f2719 | ||
|
|
fb24516b90 | ||
|
|
60fa4eb0a1 | ||
|
|
536576084d | ||
|
|
c62cc499c5 | ||
|
|
c11bbf8836 | ||
|
|
71c0cede24 | ||
|
|
ad16bbc96d | ||
|
|
520ffe4c09 | ||
|
|
e3f603c145 | ||
|
|
880b8cb072 | ||
|
|
86bf3d473a | ||
|
|
eb6baa8ad1 | ||
|
|
d53d56d93a | ||
|
|
d143ff48fa | ||
|
|
b11acb7762 | ||
|
|
51aa0dd851 | ||
|
|
861b0a1c62 | ||
|
|
27a33f567f | ||
|
|
e862a4b46b | ||
|
|
2b3e8ebfc0 | ||
|
|
6118877ad6 | ||
|
|
0454ff9bf7 | ||
|
|
4faad50d99 | ||
|
|
272599b448 | ||
|
|
c12d76e2fa | ||
|
|
55550f1773 | ||
|
|
e8992d5ad7 | ||
|
|
a8bd589b18 | ||
|
|
602cf7b110 | ||
|
|
1e7b731485 | ||
|
|
824b8e410c | ||
|
|
ba47953485 | ||
|
|
c7e666261f | ||
|
|
0c4c98d2c9 | ||
|
|
0c63be37bb | ||
|
|
9623a311a0 | ||
|
|
13da61d895 | ||
|
|
a7c02e2dea | ||
|
|
cfb9610afc | ||
|
|
a141c6b137 | ||
|
|
300efb99d2 | ||
|
|
6fb3610bcc | ||
|
|
43fb3ca10a | ||
|
|
adc360eaaa | ||
|
|
5e659352d7 | ||
|
|
6595e0a3cf | ||
|
|
c43b2b3bb9 | ||
|
|
ea19e6efd8 | ||
|
|
55d38f2b36 | ||
|
|
e0abf3e0c1 | ||
|
|
974cba8dee | ||
|
|
019cf5fc8f | ||
|
|
1956ba7a73 | ||
|
|
c209617ee7 | ||
|
|
f4b5e061a4 | ||
|
|
06b67a4b9a | ||
|
|
a385265956 | ||
|
|
6ac32a40c7 | ||
|
|
20c612c660 | ||
|
|
7e8cc5fe6c | ||
|
|
9ed14b227b | ||
|
|
ee466d0f4c | ||
|
|
b198eea9e4 | ||
|
|
8c7580488e | ||
|
|
3f4364c67c | ||
|
|
23e2df5e58 | ||
|
|
79136db162 | ||
|
|
3d008dcab5 | ||
|
|
5953cc6411 | ||
|
|
2dce675fe2 | ||
|
|
aa013a86dd | ||
|
|
f866f692e2 | ||
|
|
68a2514cef | ||
|
|
32562d7435 | ||
|
|
78be1aaa7c | ||
|
|
1eb4b1ca9e | ||
|
|
bf37db2cbe | ||
|
|
5b30617a6b | ||
|
|
266c686a3a | ||
|
|
32d0d6a7ee | ||
|
|
d9a336e50b | ||
|
|
3c958d06c8 | ||
|
|
15a75a69b3 | ||
|
|
cae32cff4c | ||
|
|
eeef76594f | ||
|
|
8f2a1030bc | ||
|
|
dc1df98277 | ||
|
|
def45cc3e4 | ||
|
|
8007f1958e | ||
|
|
b22e724a3f | ||
|
|
ed66b016db | ||
|
|
34890f21b3 | ||
|
|
9f85e4a63b | ||
|
|
7fcd4d94a6 | ||
|
|
84fbca7905 | ||
|
|
9add2387b9 | ||
|
|
a388450f94 | ||
|
|
fff7abc2ac | ||
|
|
e5587418c7 | ||
|
|
7c89fac1e9 | ||
|
|
bc9a9ffb72 | ||
|
|
46d27a04a2 | ||
|
|
0f33981c96 | ||
|
|
b396e0b208 | ||
|
|
1bf2f17538 | ||
|
|
608a531655 | ||
|
|
e57180eb15 | ||
|
|
3ae826d9ca | ||
|
|
e940845c60 | ||
|
|
d8aa989cc4 | ||
|
|
31ed4268b6 | ||
|
|
c698eaac6f | ||
|
|
d0dd23fa14 | ||
|
|
c6f1aa21c5 | ||
|
|
a3dc7798e7 | ||
|
|
88f6fd1965 | ||
|
|
9062c2e447 | ||
|
|
e3d2a4bf9d | ||
|
|
c2d609cdb4 | ||
|
|
b3721e29c6 | ||
|
|
eab4463c2d | ||
|
|
394873864f | ||
|
|
758216245f | ||
|
|
6edfd112a2 | ||
|
|
625215b068 | ||
|
|
4759e4957c | ||
|
|
b62f9494ae | ||
|
|
12b47e7d51 | ||
|
|
9ced29aa93 | ||
|
|
5095640ba2 | ||
|
|
c889d7cb3f | ||
|
|
84636f3f50 | ||
|
|
9a6f07d7c9 | ||
|
|
e8f5088049 | ||
|
|
0bee306d67 | ||
|
|
8d75b2d413 | ||
|
|
426ea607e8 | ||
|
|
507cade50d | ||
|
|
f0c7f54cc6 | ||
|
|
62c38482f6 | ||
|
|
0ff6ca18ed | ||
|
|
bbe6c388a3 | ||
|
|
639bc58e84 | ||
|
|
c0bcafd747 | ||
|
|
bcaa019551 | ||
|
|
38725abc48 | ||
|
|
00f1173bac | ||
|
|
68a02966d0 | ||
|
|
4721fcea66 | ||
|
|
5ea63273be | ||
|
|
7cee25aab2 | ||
|
|
9298f5e030 | ||
|
|
93f943d40c | ||
|
|
f20c11a4e2 | ||
|
|
0c25f31493 | ||
|
|
8d774e267f | ||
|
|
6c1de5d485 | ||
|
|
77c86ed3bc | ||
|
|
d75c3bbf96 | ||
|
|
8bb6cb714c | ||
|
|
28d002f6d3 | ||
|
|
78fcc69d29 | ||
|
|
cdfae54ab9 | ||
|
|
0813211c20 | ||
|
|
056a4eead2 | ||
|
|
2635d31425 | ||
|
|
76d35af5b9 | ||
|
|
bcff1695d3 | ||
|
|
3d04447c2f | ||
|
|
2736b88a10 | ||
|
|
0804a2fbf8 | ||
|
|
7224ed6a7d | ||
|
|
c41737364c | ||
|
|
320e579844 | ||
|
|
fc3f2e045b | ||
|
|
481b47bf10 | ||
|
|
0fb87f0b1d | ||
|
|
1252d7700f | ||
|
|
30c2d88f62 | ||
|
|
718a98d7ec | ||
|
|
fb3ba220b6 | ||
|
|
d655c9ec6b | ||
|
|
8a67f33cf6 | ||
|
|
2e53688c52 | ||
|
|
11ce04fba4 | ||
|
|
8b191e4a3b | ||
|
|
6405263616 | ||
|
|
a2119ab008 | ||
|
|
b21d8f094a | ||
|
|
90c41a83b1 | ||
|
|
a1ec774089 | ||
|
|
631056064a | ||
|
|
97e90de71e | ||
|
|
bbaed35160 | ||
|
|
778f26d55f | ||
|
|
fab5bdc2e1 | ||
|
|
b7723a88c3 | ||
|
|
1d2b59d5a2 | ||
|
|
7d42c0f581 | ||
|
|
bfe7398e17 | ||
|
|
c4f0a6a197 | ||
|
|
1ca9d0fa2c | ||
|
|
a015585bd1 | ||
|
|
bc34427793 | ||
|
|
0fe867cd93 | ||
|
|
70f0393bde | ||
|
|
9f060be3fd | ||
|
|
176968d883 | ||
|
|
e6bd55aa69 | ||
|
|
f5ffa875b8 | ||
|
|
2cb23cbfcb | ||
|
|
31dc9b0925 | ||
|
|
af7cd9a821 | ||
|
|
b34a64f425 | ||
|
|
11820e31a8 | ||
|
|
672b6054a6 | ||
|
|
d2486732d2 | ||
|
|
924067e4dd | ||
|
|
7820f748a0 | ||
|
|
e8757deb0d | ||
|
|
c371924dc0 | ||
|
|
cf28b27d50 | ||
|
|
b78f9c8cd5 | ||
|
|
91bd67dc26 | ||
|
|
0671b49c98 | ||
|
|
900f483a03 | ||
|
|
4d7af2e68b | ||
|
|
db59dcea65 | ||
|
|
ef6cfa785f | ||
|
|
2b792e73c9 | ||
|
|
0f2922dab4 | ||
|
|
45b55e2853 | ||
|
|
9c7795687f | ||
|
|
b273c80a02 | ||
|
|
8cdab4710b | ||
|
|
ea5b31d537 | ||
|
|
bb0c11fa12 | ||
|
|
37186439cb | ||
|
|
ab9f93d68e | ||
|
|
ca6920e82a | ||
|
|
d9db108a31 | ||
|
|
34b7e1d582 | ||
|
|
4d73187376 | ||
|
|
dcd0a42705 | ||
|
|
1b40916046 | ||
|
|
a4e4782f2f | ||
|
|
875b0c27ef | ||
|
|
a38a1775a0 | ||
|
|
fd6ae986c3 | ||
|
|
74a87e65c8 | ||
|
|
a285d2c27d | ||
|
|
44ac3bc900 | ||
|
|
1c346c97a8 | ||
|
|
d890b41380 | ||
|
|
cbcc89b48f | ||
|
|
521bd00a70 | ||
|
|
b4693423ef | ||
|
|
594a2741d7 | ||
|
|
a1d9b52b9d | ||
|
|
c9d825b6c1 | ||
|
|
7dcd0eb616 | ||
|
|
e79a95b885 | ||
|
|
5d232c3f5e | ||
|
|
ee1c0d362d | ||
|
|
aafcd57c82 | ||
|
|
66a4566dc5 | ||
|
|
4c317f5b03 | ||
|
|
12d706a812 | ||
|
|
c006068580 | ||
|
|
f8a838506d | ||
|
|
270844bbb1 | ||
|
|
c9466cf8ef | ||
|
|
7d6f1f98f1 | ||
|
|
fb9418dead | ||
|
|
4a1b06b80c | ||
|
|
8905f2e855 | ||
|
|
6c74364fa5 | ||
|
|
f0720c1755 | ||
|
|
f1acc1d2c1 | ||
|
|
82bddbfa6e | ||
|
|
841755fe56 | ||
|
|
340ee751e8 | ||
|
|
cf70f72f9c | ||
|
|
16ea88a659 | ||
|
|
eaa6790013 | ||
|
|
af571d533d | ||
|
|
004c3c588a | ||
|
|
2a44167c22 | ||
|
|
cf6b735026 | ||
|
|
5918b04b48 | ||
|
|
8004cb3a64 | ||
|
|
9981423927 | ||
|
|
0e5250dfb3 | ||
|
|
2dc956b357 | ||
|
|
b8bd0a08e0 | ||
|
|
fb31eebcf2 | ||
|
|
3c8689a3f1 | ||
|
|
2957d66dd6 | ||
|
|
6f6efe0874 | ||
|
|
355fc04069 | ||
|
|
b4738f383b | ||
|
|
b12711969c | ||
|
|
94727353c4 | ||
|
|
5eb7449b74 | ||
|
|
dae41dfb30 | ||
|
|
07a701e572 | ||
|
|
5323aaed6d | ||
|
|
daf659373f | ||
|
|
0eb81c29df | ||
|
|
8aea89c103 | ||
|
|
f1d97454c2 | ||
|
|
ffb6a45e28 | ||
|
|
f0d1c9b361 | ||
|
|
aedd4c290e | ||
|
|
749ecfe37f | ||
|
|
c51da28e31 | ||
|
|
f3dca75a10 | ||
|
|
c3dcb0292c | ||
|
|
2a42ecda78 | ||
|
|
08be1896ee | ||
|
|
6b3152c4b5 | ||
|
|
19ea27dece | ||
|
|
b9e26a4112 | ||
|
|
a71aea8e43 | ||
|
|
1b1f22b9fa | ||
|
|
705a5659b6 | ||
|
|
4121921aca | ||
|
|
8cb4ca0e8b | ||
|
|
6205937a10 | ||
|
|
2f522b744f | ||
|
|
b8a84f831a | ||
|
|
5f9490d780 | ||
|
|
a1c5b87b55 | ||
|
|
641e308c53 | ||
|
|
64bc70a15d | ||
|
|
acadbdb5c9 | ||
|
|
3323ea2be0 | ||
|
|
45f55b2075 | ||
|
|
372005dae1 | ||
|
|
2c7ca6d354 | ||
|
|
e21f315239 | ||
|
|
223c2bc7c7 | ||
|
|
3facbba4ee | ||
|
|
b8bf56c8c3 | ||
|
|
2fa08e0a70 | ||
|
|
8e1e13058b | ||
|
|
92520e46a3 | ||
|
|
6ae6c9c971 | ||
|
|
caf85d2a92 | ||
|
|
030b577dee | ||
|
|
83516ba679 | ||
|
|
c8f74ee8e3 | ||
|
|
c50e41ab91 | ||
|
|
c03a0feb75 | ||
|
|
7da5b8fbc8 | ||
|
|
83f7e61d37 | ||
|
|
86765275c8 | ||
|
|
a01e15d841 | ||
|
|
5e5db332d0 | ||
|
|
2de2f45124 | ||
|
|
b1b3eb9369 | ||
|
|
507665cd91 | ||
|
|
c779172cc2 | ||
|
|
684813e131 | ||
|
|
76ef47efc4 | ||
|
|
71a8fa0e66 | ||
|
|
48d98b2424 | ||
|
|
5a0e17b121 | ||
|
|
5f686f7205 | ||
|
|
518969d2f2 | ||
|
|
01ff1327f7 | ||
|
|
4d366397a8 | ||
|
|
80f79d20a5 | ||
|
|
1611d11544 | ||
|
|
c8e8b25c85 | ||
|
|
92cfc3c1d3 | ||
|
|
c4cebe72b4 | ||
|
|
6d346a3ad6 | ||
|
|
6e2696dea3 | ||
|
|
ba67c968cf | ||
|
|
332719f49a | ||
|
|
fb18975c5c | ||
|
|
08a7c27353 | ||
|
|
4c9cb99a94 | ||
|
|
cbecca5f55 | ||
|
|
4a0831a155 | ||
|
|
fa38c53915 | ||
|
|
e7c2170ce3 | ||
|
|
9b69fe8c56 | ||
|
|
ed91771594 | ||
|
|
b33e014f71 | ||
|
|
19632ac162 | ||
|
|
74e3a9e2d1 | ||
|
|
b15291dc57 | ||
|
|
1b5d4c1b42 | ||
|
|
923924638e | ||
|
|
8b52c46664 | ||
|
|
aa8c386cdd | ||
|
|
bafe17107c | ||
|
|
9c169fb560 | ||
|
|
61ab8a6769 | ||
|
|
b1cd7057db | ||
|
|
d426952d47 | ||
|
|
67b6a60382 | ||
|
|
9d5da5a39d | ||
|
|
efb14572d6 | ||
|
|
bc3ae5cc7d | ||
|
|
9752dea0e1 | ||
|
|
9afdc99157 | ||
|
|
07febc18d4 | ||
|
|
68fdfb36fd | ||
|
|
09287f9c72 | ||
|
|
4a5ac1b899 | ||
|
|
afad520669 | ||
|
|
9950bef260 | ||
|
|
1753b4d27e | ||
|
|
2bfc85b18f | ||
|
|
24f3dec453 | ||
|
|
4e6237c062 | ||
|
|
5cbde9cc33 | ||
|
|
25d79a94f8 | ||
|
|
a5997be9c4 | ||
|
|
c679667079 | ||
|
|
6b311c679d | ||
|
|
0f1f881367 | ||
|
|
090665528f | ||
|
|
387beb8484 | ||
|
|
daa1a38a63 | ||
|
|
d8a34aac1c | ||
|
|
ce9d882c9f | ||
|
|
84c18c8693 | ||
|
|
9360854150 | ||
|
|
ada63f1452 | ||
|
|
c3cd0dd8a0 | ||
|
|
60143d5299 | ||
|
|
1019c891e2 | ||
|
|
1392ad413a | ||
|
|
61aee90c8d | ||
|
|
19725012fb | ||
|
|
94a71dc88f | ||
|
|
d8cb93c36a | ||
|
|
151097a884 | ||
|
|
e97bad88c6 | ||
|
|
9f494a35ec | ||
|
|
d9d20c969a | ||
|
|
7e08cfedd6 | ||
|
|
1145e31162 | ||
|
|
e4e521c2ef | ||
|
|
d01c9a425a | ||
|
|
a9be348ddf | ||
|
|
07079ee295 | ||
|
|
eb1bab453b | ||
|
|
fd3c496a3d | ||
|
|
35e1e0bdd4 | ||
|
|
28aa137dc1 | ||
|
|
16ee0a040a | ||
|
|
437391a860 | ||
|
|
f61181438f | ||
|
|
d74ab35279 | ||
|
|
bbc2b251dd | ||
|
|
222e39dc48 | ||
|
|
f5b310d959 | ||
|
|
5fb0035b2f | ||
|
|
b8793f7575 | ||
|
|
5c5f091769 | ||
|
|
cf6485aea8 | ||
|
|
f2f0c96e62 | ||
|
|
64e7cc74b2 | ||
|
|
531fd87ff3 | ||
|
|
efa280b839 | ||
|
|
b3d84c4f99 | ||
|
|
015a1e3482 | ||
|
|
5efc8d6fa9 | ||
|
|
bb1654828f | ||
|
|
93cabde9f4 | ||
|
|
aeb5eb9129 | ||
|
|
e9e8b8a1ad | ||
|
|
08564e5ea2 | ||
|
|
1695619ef5 | ||
|
|
8f2f57c543 | ||
|
|
60454c6af3 | ||
|
|
40c7c72973 | ||
|
|
f9127272b1 | ||
|
|
ec295664d9 | ||
|
|
ec2b9b19be | ||
|
|
1566e069ed | ||
|
|
9c149f3529 | ||
|
|
2f35baef39 | ||
|
|
7c77c317df | ||
|
|
ed3a04d531 | ||
|
|
c3ec7741dc | ||
|
|
f4f89fac1f | ||
|
|
5262150d3c | ||
|
|
b5f56eb2b5 | ||
|
|
9a375fd0d1 | ||
|
|
6ca4aeb5a2 | ||
|
|
8ed4a9a5f2 | ||
|
|
a4578405cf | ||
|
|
73f147b7e2 | ||
|
|
5c3d735315 | ||
|
|
200621996d | ||
|
|
a912a0a07e | ||
|
|
4146ceb048 | ||
|
|
ad568d2b9f | ||
|
|
c7bfb9a11d | ||
|
|
c9391181bd | ||
|
|
b08eb519bf | ||
|
|
47c4fc2f7e | ||
|
|
dfd177433a | ||
|
|
27381e9696 | ||
|
|
1c7827c53c | ||
|
|
9512bdb5d6 | ||
|
|
33d1f1d692 | ||
|
|
c6b49545ff | ||
|
|
a4a833bfdb | ||
|
|
2e1b6e4630 | ||
|
|
6911cfa387 | ||
|
|
aa29f890de | ||
|
|
0f4e6a01bc | ||
|
|
1d3d8d6710 | ||
|
|
005e06c56f | ||
|
|
0a7c32b3cb | ||
|
|
82916a04ea | ||
|
|
ba2e419894 | ||
|
|
0812c9025d | ||
|
|
4c59ed3ccf | ||
|
|
43d8d96608 | ||
|
|
26a6a7d883 | ||
|
|
2e9c237367 | ||
|
|
99d2816ea7 | ||
|
|
7c3853b0e6 | ||
|
|
53aecf38e0 | ||
|
|
41fab0a781 | ||
|
|
3df35aa373 | ||
|
|
543eaa4f66 | ||
|
|
3a851b7f1a | ||
|
|
089ae1e313 | ||
|
|
bfe5802d02 | ||
|
|
3ec6c97380 | ||
|
|
e376da208d | ||
|
|
3b02c5e4e0 | ||
|
|
1bcc79c415 | ||
|
|
9e2cb25d01 | ||
|
|
7e820ba2a5 | ||
|
|
b079940cd9 | ||
|
|
593ee24429 | ||
|
|
f7a91bf10c | ||
|
|
dad7ac706f | ||
|
|
13c48ce78c | ||
|
|
8ecbba1003 | ||
|
|
8bf6a9ec9d | ||
|
|
64092a4bd4 | ||
|
|
8e6bcd969e | ||
|
|
6777dd7ea7 | ||
|
|
f16885568e | ||
|
|
ab109bcfcc | ||
|
|
17f71eaa21 | ||
|
|
3a294e354a | ||
|
|
54814181b9 | ||
|
|
935c2bd6e5 | ||
|
|
a2b74b8e78 | ||
|
|
09730d74e0 | ||
|
|
2bf03291ce | ||
|
|
93e5851167 | ||
|
|
61fd581ef5 | ||
|
|
51afcf2dee | ||
|
|
cadafe13e1 | ||
|
|
47031cb70e | ||
|
|
b3e776b4bc | ||
|
|
e57a1a8da6 | ||
|
|
7f2a392a33 | ||
|
|
055379f569 | ||
|
|
2b8843ca19 | ||
|
|
ace830bced | ||
|
|
3c2f232acb | ||
|
|
86030e64ca | ||
|
|
b1ce3c31f5 | ||
|
|
04553c2c56 | ||
|
|
bdb7afa860 | ||
|
|
7096135cdf | ||
|
|
be37014206 | ||
|
|
b78beb3305 | ||
|
|
38f27d134d | ||
|
|
3b42ba9375 | ||
|
|
4f52bb2ffe | ||
|
|
dd45e1b3db | ||
|
|
e38bfdfc73 | ||
|
|
73caebff00 | ||
|
|
e0608cdf3e | ||
|
|
c815fa80e8 | ||
|
|
e91e1cab47 | ||
|
|
b791878651 | ||
|
|
8192ffec03 | ||
|
|
afa6eb3bad | ||
|
|
6f7d22518e | ||
|
|
2663312dfb | ||
|
|
1332517a9f | ||
|
|
13e227851f | ||
|
|
e1b6c18017 | ||
|
|
a88c8ed85f | ||
|
|
25a90fc481 | ||
|
|
1371206323 | ||
|
|
129f6bb5cc | ||
|
|
545c17d052 | ||
|
|
add00ddba1 | ||
|
|
d9aeb0cc2b | ||
|
|
49e70e7195 | ||
|
|
f8abebf110 | ||
|
|
8e2dfc7341 | ||
|
|
e650ffffce | ||
|
|
d549a300ab | ||
|
|
82a62e73c1 | ||
|
|
45c7e0c231 | ||
|
|
b98a4b11a3 | ||
|
|
484a103f27 | ||
|
|
e31120be21 | ||
|
|
247f02d423 | ||
|
|
3da0c1d8e3 | ||
|
|
c830bbc703 | ||
|
|
7b619fef1d | ||
|
|
bac265a077 | ||
|
|
39df0f8d43 | ||
|
|
6af7f916ee | ||
|
|
ae64097edf | ||
|
|
cb6d8a88f1 | ||
|
|
12838994ad | ||
|
|
d30758a2a8 | ||
|
|
b820aad358 | ||
|
|
170189c344 | ||
|
|
f9e9c9d38b | ||
|
|
088af981df | ||
|
|
357fef8672 | ||
|
|
c38848ea0e | ||
|
|
be8cd13b08 | ||
|
|
487816ab74 | ||
|
|
bcd9f8240c | ||
|
|
1b62972f06 | ||
|
|
1117c3bfab | ||
|
|
748d195db5 | ||
|
|
3a9017adb8 | ||
|
|
eff3186ab5 | ||
|
|
e5f5fe878e | ||
|
|
a47f0f6ecf | ||
|
|
9896165b68 | ||
|
|
5e3adcee93 | ||
|
|
b252266a46 | ||
|
|
1d5adea114 | ||
|
|
a64a51c7df | ||
|
|
ec41c13b45 | ||
|
|
486ca434ff | ||
|
|
fe87a899c8 | ||
|
|
06806bd383 | ||
|
|
2f6c7e1ce2 | ||
|
|
141cda13f1 | ||
|
|
3e8c8afcd1 | ||
|
|
8236f613e0 | ||
|
|
ea6108dca7 | ||
|
|
985a4f07e0 | ||
|
|
5642c579a4 | ||
|
|
8e0559fb3b | ||
|
|
a6a1755fee | ||
|
|
846f383f7b | ||
|
|
bbbe3726f2 | ||
|
|
6d83fd9903 | ||
|
|
fac62a8d24 | ||
|
|
b0d71a5511 | ||
|
|
124ba34d48 | ||
|
|
75270b8ad6 | ||
|
|
5cf6ef88a6 | ||
|
|
b5f72af513 | ||
|
|
1532893c5f | ||
|
|
a3fbc9595a | ||
|
|
f789343b6e | ||
|
|
ff74417352 | ||
|
|
a0ff843d0c | ||
|
|
da93cb0dbb | ||
|
|
0de446c608 | ||
|
|
03bccf16d4 | ||
|
|
635ae8c11e | ||
|
|
dfd5a82112 | ||
|
|
7dad8d452f | ||
|
|
07f8294876 | ||
|
|
7185cc410b | ||
|
|
2d985af60d | ||
|
|
bee2e64482 | ||
|
|
d5dd43c0c1 | ||
|
|
09470eeb8a | ||
|
|
094249a6fa | ||
|
|
6b5302907a | ||
|
|
adc5b08765 | ||
|
|
0150e2a8d7 | ||
|
|
45eaa0c60f | ||
|
|
66ed683196 | ||
|
|
aa4239721e | ||
|
|
5b0b926cbd | ||
|
|
929ce398eb | ||
|
|
b3fdbe021d | ||
|
|
423bd68ce8 | ||
|
|
9bffc68c70 | ||
|
|
b7a5a60091 | ||
|
|
333b11c19f | ||
|
|
fbd4d1b772 | ||
|
|
06a58514b3 | ||
|
|
80815fc3a0 | ||
|
|
b97c277a3f | ||
|
|
310d693e2b | ||
|
|
c1596f6a11 | ||
|
|
76a04343d9 | ||
|
|
25261c99f1 | ||
|
|
d0b994fa9d | ||
|
|
f259601397 | ||
|
|
87904109ca | ||
|
|
785790ccc4 | ||
|
|
759921d00e | ||
|
|
e9db183d54 | ||
|
|
a853374a88 | ||
|
|
3fb5048f10 | ||
|
|
0dd2e758e8 | ||
|
|
d1a562452a | ||
|
|
9205a6cbf4 | ||
|
|
4c8961f39a | ||
|
|
22bdc23a09 | ||
|
|
764cd16e21 | ||
|
|
7583f4651d | ||
|
|
13e931cda4 | ||
|
|
9743cf8351 | ||
|
|
e5ee5702f8 | ||
|
|
5bd5b25b77 | ||
|
|
55d59f3779 | ||
|
|
7d2ea0b77e | ||
|
|
1f30c299a5 | ||
|
|
c887d7e649 | ||
|
|
3fc7743a14 | ||
|
|
2a232f70db | ||
|
|
6fae131287 | ||
|
|
7ee7ef25f9 | ||
|
|
2bd4bc1472 | ||
|
|
aa170b647e | ||
|
|
4fa01216b1 | ||
|
|
a82eb996a4 | ||
|
|
9315684ee3 | ||
|
|
34f7774ce9 | ||
|
|
75031a04cc | ||
|
|
cfb0526cbb | ||
|
|
9fa56a9cd4 | ||
|
|
c4db9c9e12 | ||
|
|
71272371e4 | ||
|
|
91c6c4d233 | ||
|
|
8a6653a926 | ||
|
|
76caf84e39 | ||
|
|
0fee286b84 | ||
|
|
4af18a1575 | ||
|
|
d9293f4da8 | ||
|
|
db29f97397 | ||
|
|
573f41611d | ||
|
|
28ed103ec0 | ||
|
|
5e37750f00 | ||
|
|
5fe3cd9406 | ||
|
|
b00cc99408 | ||
|
|
9a66c3bba9 | ||
|
|
546921b445 | ||
|
|
8b698d3a4c | ||
|
|
c4608a949b | ||
|
|
bba1c559a3 | ||
|
|
bdee9ea72f | ||
|
|
520923ba08 | ||
|
|
936a74597f | ||
|
|
97132745cc | ||
|
|
2bb30f65f3 | ||
|
|
ec04d55793 | ||
|
|
88671db566 | ||
|
|
538f501bc0 | ||
|
|
3b75d41a2f | ||
|
|
110149d0e6 | ||
|
|
89b68781fa | ||
|
|
4cdc54b9c1 | ||
|
|
7673595a43 | ||
|
|
76a259c107 | ||
|
|
5f40d01fb4 | ||
|
|
df29d47c0a | ||
|
|
10d4084dde | ||
|
|
ba8cdb1b42 | ||
|
|
22732fee6a | ||
|
|
bfbc4613b9 | ||
|
|
92e5eecaf2 | ||
|
|
593159f4a6 | ||
|
|
53cad0e56a | ||
|
|
d61006dd67 | ||
|
|
eb565285b2 | ||
|
|
630f164877 | ||
|
|
38c6bce2c0 | ||
|
|
034734ec66 | ||
|
|
d875eeb22a | ||
|
|
61db6b8fb2 | ||
|
|
945d4cf20d | ||
|
|
631337c451 | ||
|
|
4f51e377ae | ||
|
|
e71c06a0d0 | ||
|
|
13aabde5d9 | ||
|
|
e3f371f457 | ||
|
|
6ee99b5736 | ||
|
|
895188b25f | ||
|
|
9fba923a51 | ||
|
|
37a39098f8 | ||
|
|
6fb548e6c3 | ||
|
|
110b7cb06f | ||
|
|
e9070a77c3 | ||
|
|
475e6e5511 | ||
|
|
eed4c7c11a | ||
|
|
d986d7ac03 | ||
|
|
f640922a50 | ||
|
|
118a6cf0d1 | ||
|
|
72871e6d9a | ||
|
|
61acbcec0d | ||
|
|
06af79e2b4 | ||
|
|
6782602141 | ||
|
|
8df17ae38a | ||
|
|
1645a93adf | ||
|
|
6a04ab37d7 | ||
|
|
27c9abd9ed | ||
|
|
a08be2e11d | ||
|
|
17615b1791 | ||
|
|
d75c3b584a | ||
|
|
20f919b93e | ||
|
|
20244fe5cc | ||
|
|
6a89f0a661 | ||
|
|
3a9efdb03e | ||
|
|
2ae00db94b | ||
|
|
a9261b759b | ||
|
|
6b02dc3cd5 | ||
|
|
afa37c0d2f | ||
|
|
2575f10497 | ||
|
|
27c4a68a13 | ||
|
|
f09cb54894 | ||
|
|
5431cfef79 | ||
|
|
35c0f3cddc | ||
|
|
efe22ca613 | ||
|
|
3408a8ffcf | ||
|
|
60a140efb3 | ||
|
|
bbe584bd91 | ||
|
|
3866377a28 | ||
|
|
ebc0b4b2b0 | ||
|
|
d6c65a320d | ||
|
|
2e46d76165 | ||
|
|
23037da406 | ||
|
|
617261c74d | ||
|
|
6c0d6ee0cf | ||
|
|
663e1ef7ab | ||
|
|
1a47d355f0 | ||
|
|
e407994103 | ||
|
|
bda2149c02 | ||
|
|
b925423d61 | ||
|
|
f3906a37bf | ||
|
|
a1e115ea6b | ||
|
|
03383510ef | ||
|
|
e8cf9644eb | ||
|
|
dbfcf1facc | ||
|
|
44aff94937 | ||
|
|
fb1fc92b7c | ||
|
|
71cfa64b16 | ||
|
|
b200105a85 | ||
|
|
ce83f3c5e1 | ||
|
|
57d3275856 | ||
|
|
eb1a109afc | ||
|
|
a03e3fea12 | ||
|
|
c60ae111ef | ||
|
|
5eb46395ae | ||
|
|
db08b61cfa | ||
|
|
41d313baff | ||
|
|
58c3ed9254 | ||
|
|
affe36a568 | ||
|
|
9ba6cddde7 | ||
|
|
5656c9f351 | ||
|
|
ecf24a746e | ||
|
|
5b5a58a867 | ||
|
|
c6ab7878e7 | ||
|
|
99e85eba37 | ||
|
|
3bf92e6f12 | ||
|
|
ba359305f6 | ||
|
|
f826eb224c | ||
|
|
a675f4cb68 | ||
|
|
5e1e2c6047 | ||
|
|
eba0990538 | ||
|
|
1bcef2bab0 | ||
|
|
f83777e1d2 | ||
|
|
9fa48090ce | ||
|
|
7c8ac4fee8 | ||
|
|
76c254ee98 | ||
|
|
e75ad64483 | ||
|
|
17d1af181d | ||
|
|
a72d27a431 | ||
|
|
4e9fc57aa4 | ||
|
|
494b27cc22 | ||
|
|
3606186975 | ||
|
|
41dbdfc016 | ||
|
|
655e5ca7e0 | ||
|
|
3067d323de | ||
|
|
1404a080ad | ||
|
|
cbb2a1b575 | ||
|
|
30b276ec1a | ||
|
|
13a24d5b00 | ||
|
|
40360cf18a | ||
|
|
139e2ee78d | ||
|
|
3ff466462c | ||
|
|
9ff2442789 | ||
|
|
b11e4ab1db | ||
|
|
a52cc8c093 | ||
|
|
506c409303 | ||
|
|
53a8446f4a | ||
|
|
ab5bb8ea6c | ||
|
|
d097b84c82 | ||
|
|
f285635e76 | ||
|
|
0f7b5299e9 | ||
|
|
7a788d1e6e | ||
|
|
6c4231a6b2 | ||
|
|
6e202341ad | ||
|
|
95a509b86c | ||
|
|
602114af55 | ||
|
|
e5ccc40358 | ||
|
|
2310f6e1c0 | ||
|
|
9ae952bb78 | ||
|
|
57a7eaccf5 | ||
|
|
1934bfa962 | ||
|
|
a5f3ec7b9b | ||
|
|
3cfcf1e772 | ||
|
|
b45907a9f6 | ||
|
|
8fd9395e23 | ||
|
|
126b2a19e7 | ||
|
|
bde01af89d | ||
|
|
65491ef18e | ||
|
|
44a9470613 | ||
|
|
470045f228 | ||
|
|
d081a6ee92 | ||
|
|
95f404bb64 | ||
|
|
dcea76bcd3 | ||
|
|
e9b816fb87 | ||
|
|
20a7039f59 | ||
|
|
4ad172dda4 | ||
|
|
adf6c19996 | ||
|
|
24ec0adacf | ||
|
|
83bc6748a3 | ||
|
|
bfed651263 | ||
|
|
8bd4f2b5b5 | ||
|
|
3d7bc0b593 | ||
|
|
473c6f2b3c | ||
|
|
484ce6c895 | ||
|
|
eeccdbca72 | ||
|
|
552238676c |
No files matched your search
@@ -0,0 +1,29 @@
|
||||
# 公告
|
||||
|
||||
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
|
||||
|
||||
**如果你在阅读完本公告后仍然希望在此处发布 issue,请将公告内容删去,并按照下面的模板填入相关信息。**
|
||||
|
||||
|
||||
|
||||
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。
|
||||
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 [Discord频道](https://discord.gg/jVvC7HfM6U) [开黑啦频道](https://kaihei.co/Kx7n3t) 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
|
||||
|
||||
如果你希望在 QQ 群中反馈问题,请[赞助 HMCL](https://afdian.net/@huanghongxun) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
|
||||
|
||||
|
||||
|
||||
# 问题提交
|
||||
|
||||
*完整地填下面的问题提交表对我们很重要,这可以加快我们分析问题原因的速度。*
|
||||
|
||||
* 启动器版本:
|
||||
* 操作系统:
|
||||
* Java 版本:
|
||||
* 错误截图(最好请将整个电脑屏幕的截图发上来):
|
||||
* 游戏版本(如果是启动通过启动器自带的自动安装功能安装的游戏):
|
||||
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
|
||||
* 游戏崩溃报告(如果有):
|
||||
* 启动器崩溃报告(如果有):
|
||||
* 启动器日志文件(出现问题后点击启动器设置中的【导出启动器日志】按钮):
|
||||
* 问题描述(如何触发问题):
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Check Style
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain
|
||||
- name: Check style test
|
||||
run: ./gradlew checkstyleTest
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Check Translations
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
- name: Check style test
|
||||
run: ./gradlew checkTranslations
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Check Update
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- javafx
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
if: ${{ github.repository_owner == 'huanghongxun' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HMCL_BASE_VERSION: '3.4'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- 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_BUILD_NUMBER=`cat ci.json | jq -M -r '.id'`
|
||||
export HMCL_VERSION="$HMCL_BASE_VERSION.$HMCL_BUILD_NUMBER"
|
||||
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
|
||||
|
||||
if [ "${#HMCL_BUILD_NUMBER}" != 3 ]; then exit 1; fi
|
||||
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
|
||||
|
||||
echo "HMCL_BUILD_NUMBER=$HMCL_BUILD_NUMBER" >> $GITHUB_ENV
|
||||
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.sha1"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> 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 }}.exe.sha1
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Java CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check and set HMCL version
|
||||
run: |
|
||||
export HMCL_VERSION=`echo ${{ github.ref }} | awk '{print substr($0,12)}'`
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
|
||||
if [ -z `echo $HMCL_VERSION | grep "^[0-9]\.[0-9]\.[0-9]\{3\}\$"` ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "HMCL_BUILD_NUMBER=`echo $HMCL_VERSION | awk '{print substr($0,5)}'`" >> $GITHUB_ENV
|
||||
- name: Download artifacts
|
||||
run: |
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
run: |
|
||||
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
|
||||
+28
-4
@@ -4,22 +4,46 @@
|
||||
hs_err_pid*
|
||||
|
||||
.gradle
|
||||
.idea
|
||||
|
||||
*.lck
|
||||
*.1
|
||||
*.2
|
||||
*.log
|
||||
.mine*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
*.json
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
/HMCL/build/
|
||||
/HMCLCore/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
|
||||
# idea output
|
||||
# idea
|
||||
.idea
|
||||
/out/
|
||||
/HMCL/out/
|
||||
/HMCLCore/out/
|
||||
/HMCLCore/out/
|
||||
/HMCLTransformerDiscoveryService/out/
|
||||
|
||||
# eclipse
|
||||
/bin/
|
||||
/HMCL/bin/
|
||||
/HMCLCore/bin/
|
||||
/HMCLTransformerDiscoveryService/bin/
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
# netbeans
|
||||
.nb-gradle
|
||||
|
||||
*.exe
|
||||
!/HMCL/src/main/resources/assets/HMCLauncher.exe
|
||||
|
||||
# macos
|
||||
.DS_Store
|
||||
|
||||
# test
|
||||
/hmcl.json
|
||||
/.hmcl.json
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "JSTUN"]
|
||||
path = JSTUN
|
||||
url = https://github.com/huanghongxun/JSTUN
|
||||
@@ -0,0 +1,2 @@
|
||||
/data.csv
|
||||
/data.json
|
||||
Binary file not shown.
Binary file not shown.
+281
-130
@@ -1,162 +1,313 @@
|
||||
import java.security.MessageDigest
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.Pack200
|
||||
import java.util.zip.GZIPOutputStream
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
classpath 'org.glavo:pack200:0.3.0'
|
||||
|
||||
if (!hasProperty('mainClass')) {
|
||||
ext.mainClass = 'org.jackhuang.hmcl.Main'
|
||||
classpath 'com.guardsquare:proguard-gradle:7.1.0' // The ProGuard Gradle plugin.
|
||||
}
|
||||
}
|
||||
|
||||
def buildnumber = System.getenv("TRAVIS_BUILD_NUMBER")
|
||||
if (buildnumber == null)
|
||||
buildnumber = System.getenv("BUILD_NUMBER")
|
||||
if (buildnumber == null)
|
||||
buildnumber = "SNAPSHOT"
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '7.0.0'
|
||||
}
|
||||
|
||||
def versionroot = System.getenv("VERSION_ROOT")
|
||||
if (versionroot == null)
|
||||
versionroot = "3.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.zip.GZIPOutputStream
|
||||
import java.util.zip.ZipFile
|
||||
import java.nio.file.Files
|
||||
|
||||
String mavenGroupId = 'HMCL'
|
||||
String mavenVersion = versionroot + '.' + buildnumber
|
||||
String bundleName = "Hello Minecraft! Launcher"
|
||||
version = mavenVersion
|
||||
import org.tukaani.xz.LZMA2Options
|
||||
import org.tukaani.xz.XZOutputStream
|
||||
import org.glavo.pack200.Pack200
|
||||
|
||||
def dev = null
|
||||
def shortcommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7) ?: null
|
||||
if (shortcommit != null && !shortcommit.isEmpty()) dev = "dev-" + shortcommit
|
||||
def buildnumber = System.getenv("BUILD_NUMBER") ?: dev ?: "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.4"
|
||||
def microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
def microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
def versionType = System.getenv("VERSION_TYPE") ?: "nightly"
|
||||
version = versionroot + '.' + buildnumber
|
||||
|
||||
mainClassName = 'org.jackhuang.hmcl.Main'
|
||||
|
||||
dependencies {
|
||||
compile project(":HMCLCore")
|
||||
compile rootProject.files("lib/JFoenix.jar")
|
||||
implementation project(":HMCLCore")
|
||||
implementation rootProject.files("lib/JFoenix.jar")
|
||||
|
||||
implementation group: 'de.javawi.jstun', name: 'jstun', version: '0.7.4'
|
||||
}
|
||||
|
||||
task generateSources(type: Sync) {
|
||||
from 'src/main/java'
|
||||
into "$buildDir/generated-src"
|
||||
filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [
|
||||
'HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING': mavenVersion
|
||||
])
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
return MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
}
|
||||
|
||||
compileJava.setSource "$buildDir/generated-src"
|
||||
compileJava.dependsOn generateSources
|
||||
|
||||
configurations {
|
||||
coreCompile.extendsFrom compile
|
||||
coreRuntime.extendsFrom runtime
|
||||
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"
|
||||
}
|
||||
|
||||
jar {
|
||||
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
|
||||
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 {
|
||||
if(JavaVersion.current() < JavaVersion.VERSION_11) {
|
||||
javaCompiler = javaToolchains.compilerFor {
|
||||
languageVersion = JavaLanguageVersion.of(11)
|
||||
}
|
||||
}
|
||||
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
classifier = "shadow"
|
||||
|
||||
manifest {
|
||||
attributes 'Created-By' : 'Copyright(c) 2013-2017 huangyuhui.',
|
||||
'Main-Class' : mainClass,
|
||||
'Multi-Release': "true"
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
|
||||
'Main-Class': mainClassName,
|
||||
'Multi-Release': 'true',
|
||||
'Implementation-Version': project.version,
|
||||
'Microsoft-Auth-Id': microsoftAuthId,
|
||||
'Microsoft-Auth-Secret': microsoftAuthSecret,
|
||||
'Build-Channel': versionType,
|
||||
'Class-Path': 'pack200.jar',
|
||||
'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(" ")
|
||||
}
|
||||
}
|
||||
|
||||
task proguard(type: proguard.gradle.ProGuardTask) {
|
||||
dependsOn shadowJar
|
||||
injars shadowJar
|
||||
|
||||
outjars "${buildDir}/libs/${project.name}-${project.version}-proguard.jar"
|
||||
|
||||
dontobfuscate
|
||||
dontoptimize
|
||||
|
||||
keep 'class org.jackhuang.** { *; }'
|
||||
keep 'class com.jfoenix.** { *; }'
|
||||
keep 'class com.google.gson.** { *; }'
|
||||
keep 'class com.github.steveice10.opennbt.** { *; }' // TagRegistry use reflection to create object instance
|
||||
|
||||
keep 'class * extends com.google.gson.TypeAdapter'
|
||||
keep 'class * implements com.google.gson.TypeAdapterFactory'
|
||||
keep 'class * implements com.google.gson.JsonSerializer'
|
||||
keep 'class * implements com.google.gson.JsonDeserializer'
|
||||
|
||||
printusage "${buildDir}/libs/proguard-shrinking-details.txt"
|
||||
|
||||
System.out.println(new File(".").absolutePath)
|
||||
|
||||
dontwarn 'com.nqzero.**'
|
||||
dontwarn 'org.slf4j.**'
|
||||
dontwarn 'org.jackhuang.hmcl.util.Pack200Utils'
|
||||
dontwarn 'com.sun.javafx.**'
|
||||
dontwarn 'com.jfoenix.**'
|
||||
dontwarn 'org.apache.**'
|
||||
|
||||
adaptclassstrings
|
||||
|
||||
// next block taken verbatim from Proguard's documentation examples:
|
||||
|
||||
libraryjars files(configurations.compileClasspath.collect())
|
||||
|
||||
keepattributes 'SourceFile,LineNumberTable'
|
||||
keepattributes '*Annotation*'
|
||||
keepattributes 'Signature'
|
||||
|
||||
var javaHome = System.getProperty('java.home')
|
||||
|
||||
// Automatically handle the Java version of this build.
|
||||
if (System.getProperty('java.version').startsWith('1.')) {
|
||||
// Before Java 9, the runtime classes were packaged in a single jar file.
|
||||
libraryjars "${javaHome}/lib/rt.jar"
|
||||
libraryjars "${javaHome}/lib/ext/jfxrt.jar"
|
||||
} else {
|
||||
// As of Java 9, the runtime classes are packaged in modular jmod files.
|
||||
libraryjars "${javaHome}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.desktop.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.logging.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.sql.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.xml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
|
||||
libraryjars "${javaHome}/jmods/jdk.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/jdk.unsupported.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
|
||||
if (new File("${javaHome}/jmods/javafx.base.jmod").exists()) {
|
||||
libraryjars "${javaHome}/jmods/javafx.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.controls.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.graphics.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.media.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.fxml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.web.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task finalJar(type: Jar) {
|
||||
dependsOn proguard
|
||||
|
||||
manifest = shadowJar.manifest
|
||||
|
||||
from { proguard.outJarFiles.collect { zipTree(it)} }
|
||||
|
||||
into('/') {
|
||||
from { shadowJar.outputs.files.collect { zipTree(it) } }
|
||||
include("META-INF/versions/**")
|
||||
}
|
||||
|
||||
doLast {
|
||||
new File("build/signed").mkdirs()
|
||||
ant.signjar(signedjar: archivePath, jar: archivePath,
|
||||
keystore: "HMCL.keystore", storepass: "123456",
|
||||
alias: "HMCL")
|
||||
|
||||
def messageDigest = MessageDigest.getInstance("SHA1")
|
||||
archivePath.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
|
||||
messageDigest.update(buf, 0, bytesRead)
|
||||
}
|
||||
def sha1Hex = new BigInteger(1, messageDigest.digest()).toString(16).padLeft(40, '0')
|
||||
def fileEx = new File(project.buildDir, "libs/" + archivePath.getName() + ".sha1")
|
||||
if (!fileEx.exists()) fileEx.createNewFile()
|
||||
fileEx.append sha1Hex
|
||||
repack(jar.archivePath) // see repack()
|
||||
attachSignature(jar.archivePath)
|
||||
createChecksum(jar.archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
dependsOn finalJar
|
||||
}
|
||||
|
||||
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 {
|
||||
from(sourceSets.main.resources.srcDirs) {
|
||||
exclude 'icon.icns'
|
||||
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()
|
||||
javaexec {
|
||||
classpath = sourceSets.main.compileClasspath
|
||||
mainClass = "com.sun.javafx.css.parser.Css2Bin"
|
||||
args = [cssFile, bssFile]
|
||||
}
|
||||
}
|
||||
}
|
||||
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 makeExecutable(dependsOn: jar) doLast {
|
||||
ext {
|
||||
jar.classifier = ''
|
||||
makeExecutableinjar = jar.archivePath
|
||||
jar.classifier = ''
|
||||
makeExecutableoutjar = jar.archivePath
|
||||
jar.classifier = ''
|
||||
}
|
||||
def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length() - 4) + ".exe")
|
||||
def fos = new FileOutputStream(loc)
|
||||
def is = new FileInputStream(new File(project.buildDir, '../HMCLauncher.exe'))
|
||||
int read
|
||||
def bytes = new byte[8192]
|
||||
while((read = is.read(bytes)) != -1)
|
||||
fos.write(bytes, 0, read)
|
||||
is.close()
|
||||
is = new FileInputStream(makeExecutableoutjar)
|
||||
while((read = is.read(bytes)) != -1)
|
||||
fos.write(bytes, 0, read)
|
||||
is.close()
|
||||
fos.close()
|
||||
|
||||
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: jar) doLast {
|
||||
ext {
|
||||
jar.classifier = ''
|
||||
makeExecutableinjar = jar.archivePath
|
||||
jar.classifier = ''
|
||||
makeExecutableoutjar = jar.archivePath
|
||||
jar.classifier = ''
|
||||
}
|
||||
def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length()-4)+".pack.gz")
|
||||
def os = new GZIPOutputStream(new FileOutputStream(loc))
|
||||
Pack200.newPacker().pack new JarFile(makeExecutableinjar), os
|
||||
os.close()
|
||||
|
||||
def messageDigest = MessageDigest.getInstance("SHA1")
|
||||
loc.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
|
||||
messageDigest.update(buf, 0, bytesRead)
|
||||
}
|
||||
def sha1Hex = new BigInteger(1, messageDigest.digest()).toString(16).padLeft(40, '0')
|
||||
def fileEx = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length()-4)+".pack.gz.sha1")
|
||||
if (!fileEx.exists()) fileEx.createNewFile()
|
||||
fileEx.append sha1Hex
|
||||
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 proguard(type: proguard.gradle.ProGuardTask, dependsOn: jar) {
|
||||
ext {
|
||||
def re = jar.classifier
|
||||
injar = jar.archivePath
|
||||
jar.classifier = 'optimized'
|
||||
outjar = jar.archivePath
|
||||
jar.classifier = re
|
||||
}
|
||||
|
||||
injars(/*["filter": "!META-INF/**,**"], */injar)
|
||||
String javaHome = System.getProperty("java.home")
|
||||
if (!javaHome.endsWith("jre"))
|
||||
javaHome += File.separator + "jre"
|
||||
libraryjars javaHome + "/lib/rt.jar"
|
||||
libraryjars javaHome + "/lib/jce.jar"
|
||||
libraryjars javaHome + "/lib/jsse.jar"
|
||||
libraryjars javaHome + "/lib/ext/jfxrt.jar"
|
||||
outjars outjar
|
||||
|
||||
configuration 'proguard.pro'
|
||||
task makeExecutables(dependsOn: jar) doLast {
|
||||
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
|
||||
}
|
||||
|
||||
task combineMETA_INF(type: Jar) {
|
||||
ext {
|
||||
def re = jar.classifier
|
||||
injar = jar.archivePath
|
||||
jar.classifier = 'optimized'
|
||||
outjar = jar.archivePath
|
||||
jar.classifier = re
|
||||
}
|
||||
from zipTree(outjar)
|
||||
from zipTree(injar).matching {
|
||||
include "META-INF/"
|
||||
}
|
||||
}
|
||||
|
||||
build.dependsOn makeExecutable
|
||||
build.dependsOn makePackGZ
|
||||
build.dependsOn makePackXz
|
||||
build.dependsOn makePackGz
|
||||
build.dependsOn makeExecutables
|
||||
@@ -0,0 +1,138 @@
|
||||
#
|
||||
# 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
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
S = ';'
|
||||
|
||||
MOD_SEPARATOR = ','
|
||||
|
||||
CURSEFORGE_PATTERN1 = re.compile(
|
||||
r'^/minecraft/(mc-mods|customization|mc-addons|customization/configuration)/(?P<modid>[\w-]+)(/(.*?))?$')
|
||||
CURSEFORGE_PATTERN2 = re.compile(
|
||||
r'^/projects/(?P<modid>[\w-]+)(/(.*?))?$')
|
||||
CURSEFORGE_PATTERN3 = re.compile(
|
||||
r'^/mc-mods/minecraft/(?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]:
|
||||
match = pattern.match(res.path)
|
||||
if match:
|
||||
return match.group('modid')
|
||||
return ''
|
||||
|
||||
|
||||
MCMOD_PATTERN = re.compile(
|
||||
r'^http(s)?://www\.mcmod\.cn/class/(?P<modid>\d+)\.html$')
|
||||
|
||||
|
||||
def parseMcmod(url):
|
||||
match = MCMOD_PATTERN.match(url)
|
||||
if match:
|
||||
return match.group('modid')
|
||||
return ''
|
||||
|
||||
|
||||
MCBBS_HTML_PATTERN = re.compile(r'/thread-(?P<modid>\d+)-(\d+)-(\d+).html')
|
||||
MCBBS_PHP_PATTERN = re.compile(r'')
|
||||
|
||||
|
||||
def parseMcbbs(url):
|
||||
res = urlparse(url)
|
||||
if res.scheme not in ['http', 'https']:
|
||||
return ''
|
||||
if res.netloc != 'www.mcbbs.net':
|
||||
return ''
|
||||
match = MCBBS_HTML_PATTERN.match(res.path)
|
||||
if match:
|
||||
return match.group('modid')
|
||||
|
||||
query = parse_qs(res.query)
|
||||
|
||||
if res.path == '/forum.php':
|
||||
if 'mod' in query and 'tid' in query and query['mod'] == ['viewthread']:
|
||||
return query['tid']
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with codecs.open('data.json', 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 S in chinese_name:
|
||||
print('Error! ' + chinese_name)
|
||||
exit(1)
|
||||
|
||||
if S in sub_name:
|
||||
print('Error! ' + chinese_name)
|
||||
exit(1)
|
||||
|
||||
black_lists = [
|
||||
'Master Chef'
|
||||
]
|
||||
|
||||
curseforge_id = ''
|
||||
mcmod_id = ''
|
||||
mcbbs_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)
|
||||
if 'mcbbs' in links and links['mcbbs']:
|
||||
mcbbs_id = parseMcbbs(links['mcbbs'][0]['url'])
|
||||
if mcbbs_id == '':
|
||||
print('Error mcbbs ' + chinese_name)
|
||||
exit(1)
|
||||
|
||||
mod_id = []
|
||||
if '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}{mcbbs_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
|
||||
|
||||
print('Success!')
|
||||
Vendored
-15
@@ -1,15 +0,0 @@
|
||||
-target 1.8
|
||||
-dontoptimize
|
||||
-dontobfuscate
|
||||
|
||||
# JFoenix
|
||||
-keep class com.jfoenix.** {
|
||||
<fields>;
|
||||
<methods>;
|
||||
}
|
||||
|
||||
# HMCL
|
||||
-keep class org.jackhuang.** {
|
||||
<fields>;
|
||||
<methods>;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
rootProject.name = 'HMCL'
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,68 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class FunctionHelper {
|
||||
|
||||
private FunctionHelper() {
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> void always(Consumer<T> consumer, T... ts) {
|
||||
Arrays.asList(ts).forEach(consumer);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <A, B> void alwaysA(BiConsumer<A, B> consumer, A a, B... bs) {
|
||||
Arrays.asList(bs).forEach(b -> consumer.accept(a, b));
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <A, B> void alwaysB(BiConsumer<A, B> consumer, B b, A... as) {
|
||||
Arrays.asList(as).forEach(a -> consumer.accept(a, b));
|
||||
}
|
||||
|
||||
public static <A, B> BiConsumer<B, A> exchange(BiConsumer<A, B> consumer) {
|
||||
return (b, a) -> consumer.accept(a, b);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Consumer<T> link(Consumer<T>... consumers) {
|
||||
return t -> {
|
||||
for (Consumer<T> consumer : consumers)
|
||||
consumer.accept(t);
|
||||
};
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T extends Event> EventHandler<T> link(EventHandler<T>... handlers) {
|
||||
return t -> {
|
||||
for (EventHandler<T> handler : handlers)
|
||||
handler.handle(t);
|
||||
};
|
||||
}
|
||||
|
||||
public static <A, B> Consumer<A> link1(Function<A, B> function, Consumer<B> consumer) {
|
||||
return a -> consumer.accept(function.apply(a));
|
||||
}
|
||||
|
||||
public static <A, B, C> BiConsumer<A, C> link2(Function<A, B> function, BiConsumer<B, C> consumer) {
|
||||
return (a, c) -> consumer.accept(function.apply(a), c);
|
||||
}
|
||||
|
||||
public static <A, B> Consumer<B> link2(Supplier<A> supplier, BiConsumer<A, B> consumer) {
|
||||
return b -> consumer.accept(supplier.get(), b);
|
||||
}
|
||||
|
||||
public static <A, B> Supplier<B> link1(Supplier<A> supplier, Function<A, B> function) {
|
||||
return () -> function.apply(supplier.get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SkinAnimation {
|
||||
|
||||
protected int weight, left;
|
||||
protected List<SkinTransition> transitions;
|
||||
|
||||
@Deprecated
|
||||
public SkinAnimation() {
|
||||
this.transitions = new LinkedList<>();
|
||||
}
|
||||
|
||||
public SkinAnimation(int weight, SkinTransition... transitions) {
|
||||
this.weight = weight;
|
||||
this.transitions = Arrays.asList(transitions);
|
||||
init();
|
||||
}
|
||||
|
||||
protected void init() {
|
||||
transitions.forEach(t -> {
|
||||
EventHandler<ActionEvent> oldHandler = t.getOnFinished();
|
||||
EventHandler<ActionEvent> newHandler = e -> left--;
|
||||
newHandler = oldHandler == null ? newHandler : FunctionHelper.link(oldHandler, newHandler);
|
||||
t.setOnFinished(newHandler);
|
||||
});
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
return left > 0;
|
||||
}
|
||||
|
||||
public void play() {
|
||||
transitions.forEach(SkinTransition::play);
|
||||
left = transitions.size();
|
||||
}
|
||||
|
||||
public void playFromStart() {
|
||||
transitions.forEach(SkinTransition::playFromStart);
|
||||
left = transitions.size();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
transitions.forEach(SkinTransition::stop);
|
||||
left = 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Random;
|
||||
|
||||
public class SkinAnimationPlayer {
|
||||
|
||||
protected final Random random = new Random();
|
||||
protected LinkedList<SkinAnimation> animations = new LinkedList<>();
|
||||
protected SkinAnimation playing;
|
||||
protected boolean running;
|
||||
protected int weightedSum = 0;
|
||||
protected long lastPlayTime = -1L, interval = 10_000_000_000L;
|
||||
protected AnimationTimer animationTimer = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
if (playing == null || !playing.isPlaying() && now - lastPlayTime > interval) {
|
||||
int nextAni = random.nextInt(weightedSum);
|
||||
SkinAnimation tmp = null;
|
||||
for (SkinAnimation animation : animations) {
|
||||
nextAni -= animation.getWeight();
|
||||
tmp = animation;
|
||||
if (nextAni <= 0)
|
||||
break;
|
||||
}
|
||||
playing = tmp;
|
||||
if (playing == null && animations.size() > 0)
|
||||
playing = animations.getLast();
|
||||
if (playing != null) {
|
||||
playing.playFromStart();
|
||||
lastPlayTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public int getWeightedSum() {
|
||||
return weightedSum;
|
||||
}
|
||||
|
||||
public void setInterval(long interval) {
|
||||
this.interval = interval;
|
||||
if (interval < 1)
|
||||
animationTimer.stop();
|
||||
else
|
||||
start();
|
||||
}
|
||||
|
||||
public long getInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
public long getLastPlayTime() {
|
||||
return lastPlayTime;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
return playing != null;
|
||||
}
|
||||
|
||||
public SkinAnimation getPlaying() {
|
||||
return playing;
|
||||
}
|
||||
|
||||
public void addSkinAnimation(SkinAnimation... animations) {
|
||||
this.animations.addAll(Arrays.asList(animations));
|
||||
this.weightedSum = this.animations.stream().mapToInt(SkinAnimation::getWeight).sum();
|
||||
start();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (!running && weightedSum > 0 && interval > 0) {
|
||||
animationTimer.start();
|
||||
running = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (running)
|
||||
animationTimer.stop();
|
||||
if (playing != null)
|
||||
playing.stop();
|
||||
running = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.scene.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.scene.paint.Material;
|
||||
import javafx.scene.paint.PhongMaterial;
|
||||
import javafx.scene.shape.Shape3D;
|
||||
import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.scene.transform.Translate;
|
||||
|
||||
public class SkinCanvas extends Group {
|
||||
|
||||
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/alex.png"));
|
||||
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/steve.png"));
|
||||
|
||||
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
|
||||
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
|
||||
|
||||
public static final SkinCube STEVEN_LARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, false);
|
||||
public static final SkinCube STEVEN_RARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, false);
|
||||
|
||||
protected Image srcSkin, skin;
|
||||
protected boolean isSlim;
|
||||
|
||||
protected double preW, preH;
|
||||
protected boolean msaa;
|
||||
|
||||
protected SubScene subScene;
|
||||
protected Group root = new Group();
|
||||
|
||||
public final SkinMultipleCubes headOuter = new SkinMultipleCubes(8, 8, 8, 32F / 64F, 0F, 1.125, 0.2);
|
||||
public final SkinMultipleCubes bodyOuter = new SkinMultipleCubes(8, 12, 4, 16F / 64F, 32F / 64F, 1, 0.2);
|
||||
public final SkinMultipleCubes larmOuter = new SkinMultipleCubes(4, 12, 4, 48F / 64F, 48F / 64F, 1.0625, 0.2);
|
||||
public final SkinMultipleCubes rarmOuter = new SkinMultipleCubes(4, 12, 4, 40F / 64F, 32F / 64F, 1.0625, 0.2);
|
||||
public final SkinMultipleCubes llegOuter = new SkinMultipleCubes(4, 12, 4, 0F / 64F, 48F / 64F, 1.0625, 0.2);
|
||||
public final SkinMultipleCubes rlegOuter = new SkinMultipleCubes(4, 12, 4, 0F / 64F, 32F / 64F, 1.0625, 0.2);
|
||||
|
||||
public final SkinCube headInside = new SkinCube(8, 8, 8, 32F / 64F, 16F / 64F, 0F, 0F, 0F, false);
|
||||
public final SkinCube bodyInside = new SkinCube(8, 12, 4, 24F / 64F, 16F / 64F, 16F / 64F, 16F / 64F, 0.03F, false);
|
||||
public final SkinCube larmInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, false);
|
||||
public final SkinCube rarmInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, false);
|
||||
public final SkinCube llegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 16F / 64F, 48F / 64F, 0F, false);
|
||||
public final SkinCube rlegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 0F, 16F / 64F, 0F, false);
|
||||
|
||||
public final SkinGroup head = new SkinGroup(
|
||||
new Rotate(0, 0, headInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, 0, headInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
headOuter, headInside
|
||||
);
|
||||
public final SkinGroup body = new SkinGroup(
|
||||
new Rotate(0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, Rotate.Z_AXIS),
|
||||
bodyOuter, bodyInside
|
||||
);
|
||||
public final SkinGroup larm = new SkinGroup(
|
||||
new Rotate(0, 0, -larmInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, +larmInside.getWidth() / 2, -larmInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
larmOuter, larmInside
|
||||
);
|
||||
public final SkinGroup rarm = new SkinGroup(
|
||||
new Rotate(0, 0, -rarmInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, -rarmInside.getWidth() / 2, -rarmInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
rarmOuter, rarmInside
|
||||
);
|
||||
public final SkinGroup lleg = new SkinGroup(
|
||||
new Rotate(0, 0, -llegInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, 0, -llegInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
llegOuter, llegInside
|
||||
);
|
||||
public final SkinGroup rleg = new SkinGroup(
|
||||
new Rotate(0, 0, -rlegInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, 0, -rlegInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
rlegOuter, rlegInside
|
||||
);
|
||||
|
||||
protected PerspectiveCamera camera = new PerspectiveCamera(true);
|
||||
|
||||
protected Rotate xRotate = new Rotate(0, Rotate.X_AXIS);
|
||||
protected Rotate yRotate = new Rotate(180, Rotate.Y_AXIS);
|
||||
protected Rotate zRotate = new Rotate(0, Rotate.Z_AXIS);
|
||||
protected Translate translate = new Translate(0, 0, -80);
|
||||
protected Scale scale = new Scale(1, 1);
|
||||
|
||||
protected SkinAnimationPlayer animationplayer = new SkinAnimationPlayer();
|
||||
|
||||
public SkinAnimationPlayer getAnimationplayer() {
|
||||
return animationplayer;
|
||||
}
|
||||
|
||||
public Image getSrcSkin() {
|
||||
return srcSkin;
|
||||
}
|
||||
|
||||
public Image getSkin() {
|
||||
return skin;
|
||||
}
|
||||
|
||||
public void updateSkin(Image skin, boolean isSlim) {
|
||||
if (SkinHelper.isNoRequest(skin) && SkinHelper.isSkin(skin)) {
|
||||
this.srcSkin = skin;
|
||||
this.skin = SkinHelper.x32Tox64(skin);
|
||||
int multiple = Math.max((int) (1024 / skin.getWidth()), 1);
|
||||
if (multiple > 1)
|
||||
this.skin = SkinHelper.enlarge(this.skin, multiple);
|
||||
if (this.isSlim != isSlim)
|
||||
updateSkinModel(isSlim);
|
||||
bindMaterial(root);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateSkinModel(boolean isSlim) {
|
||||
this.isSlim = isSlim;
|
||||
FunctionHelper.alwaysB(SkinMultipleCubes::setWidth, isSlim ? 3 : 4, larmOuter, rarmOuter);
|
||||
FunctionHelper.alwaysB(SkinCube::setWidth, isSlim ? 3D : 4D, larmInside, rarmInside);
|
||||
|
||||
FunctionHelper.alwaysB(Node::setTranslateX, -(bodyInside.getWidth() + larmInside.getWidth()) / 2, larm);
|
||||
FunctionHelper.alwaysB(Node::setTranslateX, +(bodyInside.getWidth() + rarmInside.getWidth()) / 2, rarm);
|
||||
if (isSlim) {
|
||||
larmInside.setModel(ALEX_LARM.getModel());
|
||||
rarmInside.setModel(ALEX_RARM.getModel());
|
||||
} else {
|
||||
larmInside.setModel(STEVEN_LARM.getModel());
|
||||
rarmInside.setModel(STEVEN_RARM.getModel());
|
||||
}
|
||||
|
||||
larm.getZRotate().setPivotX(-larmInside.getWidth() / 2);
|
||||
rarm.getZRotate().setPivotX(+rarmInside.getWidth() / 2);
|
||||
}
|
||||
|
||||
public SkinCanvas(double preW, double preH) {
|
||||
this(STEVE, preW, preH, true);
|
||||
}
|
||||
|
||||
public SkinCanvas(Image skin, double preW, double preH, boolean msaa) {
|
||||
this.skin = skin;
|
||||
this.preW = preW;
|
||||
this.preH = preH;
|
||||
this.msaa = msaa;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
protected Material createMaterial() {
|
||||
PhongMaterial material = new PhongMaterial();
|
||||
material.setDiffuseMap(skin);
|
||||
return material;
|
||||
}
|
||||
|
||||
protected void bindMaterial(Group group) {
|
||||
Material material = createMaterial();
|
||||
for (Node node : group.getChildren())
|
||||
if (node instanceof Shape3D)
|
||||
((Shape3D) node).setMaterial(material);
|
||||
else if (node instanceof SkinMultipleCubes)
|
||||
((SkinMultipleCubes) node).updateSkin(skin);
|
||||
else if (node instanceof Group)
|
||||
bindMaterial((Group) node);
|
||||
}
|
||||
|
||||
protected Group createPlayerModel() {
|
||||
head.setTranslateY(-(bodyInside.getHeight() + headInside.getHeight()) / 2);
|
||||
|
||||
larm.setTranslateX(-(bodyInside.getWidth() + larmInside.getWidth()) / 2);
|
||||
rarm.setTranslateX(+(bodyInside.getWidth() + rarmInside.getWidth()) / 2);
|
||||
|
||||
lleg.setTranslateX(-(bodyInside.getWidth() - llegInside.getWidth()) / 2);
|
||||
rleg.setTranslateX(+(bodyInside.getWidth() - rlegInside.getWidth()) / 2);
|
||||
|
||||
lleg.setTranslateY(+(bodyInside.getHeight() + llegInside.getHeight()) / 2);
|
||||
rleg.setTranslateY(+(bodyInside.getHeight() + rlegInside.getHeight()) / 2);
|
||||
|
||||
root.getTransforms().addAll(xRotate);
|
||||
|
||||
root.getChildren().addAll(
|
||||
head,
|
||||
body,
|
||||
larm,
|
||||
rarm,
|
||||
lleg,
|
||||
rleg
|
||||
);
|
||||
updateSkin(skin, false);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected SubScene createSubScene() {
|
||||
Group group = new Group();
|
||||
group.getChildren().add(createPlayerModel());
|
||||
group.getTransforms().add(zRotate);
|
||||
|
||||
camera.getTransforms().addAll(yRotate, translate, scale);
|
||||
|
||||
subScene = new SubScene(group, preW, preH, true,
|
||||
msaa ? SceneAntialiasing.BALANCED : SceneAntialiasing.DISABLED);
|
||||
subScene.setCamera(camera);
|
||||
|
||||
return subScene;
|
||||
}
|
||||
|
||||
protected void init() {
|
||||
getChildren().add(createSubScene());
|
||||
}
|
||||
|
||||
private double lastX, lastY;
|
||||
|
||||
public void enableRotation(double sensitivity) {
|
||||
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
lastX = -1;
|
||||
lastY = -1;
|
||||
});
|
||||
addEventHandler(MouseEvent.MOUSE_DRAGGED, e -> {
|
||||
if (!(lastX == -1 || lastY == -1)) {
|
||||
if (e.isAltDown() || e.isControlDown() || e.isShiftDown()) {
|
||||
if (e.isShiftDown())
|
||||
zRotate.setAngle(zRotate.getAngle() - (e.getSceneY() - lastY) * sensitivity);
|
||||
if (e.isAltDown())
|
||||
yRotate.setAngle(yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity);
|
||||
if (e.isControlDown())
|
||||
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity);
|
||||
} else {
|
||||
double yaw = yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity;
|
||||
yaw %= 360;
|
||||
if (yaw < 0)
|
||||
yaw += 360;
|
||||
|
||||
int flagX = yaw < 90 || yaw > 270 ? 1 : -1;
|
||||
int flagZ = yaw < 180 ? -1 : 1;
|
||||
double kx = Math.abs(90 - yaw % 180) / 90 * flagX, kz = Math.abs(90 - (yaw + 90) % 180) / 90 * flagZ;
|
||||
|
||||
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kx);
|
||||
yRotate.setAngle(yaw);
|
||||
zRotate.setAngle(zRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kz);
|
||||
}
|
||||
}
|
||||
lastX = e.getSceneX();
|
||||
lastY = e.getSceneY();
|
||||
});
|
||||
addEventHandler(ScrollEvent.SCROLL, e -> {
|
||||
double delta = (e.getDeltaY() > 0 ? 1 : e.getDeltaY() == 0 ? 0 : -1) / 10D * sensitivity;
|
||||
scale.setX(Math.min(Math.max(scale.getX() - delta, 0.1), 10));
|
||||
scale.setY(Math.min(Math.max(scale.getY() - delta, 0.1), 10));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.shape.Mesh;
|
||||
import javafx.scene.shape.MeshView;
|
||||
import javafx.scene.shape.TriangleMesh;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
public class SkinCube extends MeshView {
|
||||
|
||||
public static class Model extends TriangleMesh {
|
||||
|
||||
public Model(float width, float height, float depth, float scaleX, float scaleY, float startX, float startY, boolean isSlim) {
|
||||
getPoints().addAll(createPoints(width, height, depth));
|
||||
getTexCoords().addAll(createTexCoords(width, height, depth, scaleX, scaleY, startX, startY, isSlim));
|
||||
getFaces().addAll(createFaces());
|
||||
}
|
||||
|
||||
public static float[] createPoints(float width, float height, float depth) {
|
||||
width /= 2F;
|
||||
height /= 2F;
|
||||
depth /= 2F;
|
||||
|
||||
return new float[]{
|
||||
-width, -height, depth, // P0
|
||||
width, -height, depth, // P1
|
||||
-width, height, depth, // P2
|
||||
width, height, depth, // P3
|
||||
-width, -height, -depth, // P4
|
||||
width, -height, -depth, // P5
|
||||
-width, height, -depth, // P6
|
||||
width, height, -depth // P7
|
||||
};
|
||||
}
|
||||
|
||||
public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
|
||||
float startX, float startY, boolean isSlim) {
|
||||
float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
|
||||
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
|
||||
bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
|
||||
return new float[]{
|
||||
top_x, top_y, // T0 ---
|
||||
top_x + half_width, top_y, // T1 |
|
||||
top_x + half_width * 2, top_y, // T2 ---
|
||||
bottom_x, middle_y, // T3 ---
|
||||
bottom_x + half_depth, middle_y, // T4 |
|
||||
bottom_x + half_depth + half_width, middle_y, // T5 |
|
||||
bottom_x + scaleX - arm4, middle_y, // T6 |
|
||||
bottom_x + scaleX, middle_y, // T7 ---
|
||||
bottom_x, bottom_y, // T8 ---
|
||||
bottom_x + half_depth, bottom_y, // T9 |
|
||||
bottom_x + half_depth + half_width, bottom_y, // T10 |
|
||||
bottom_x + scaleX - arm4, bottom_y, // T11 |
|
||||
bottom_x + scaleX, bottom_y // T12 ---
|
||||
};
|
||||
}
|
||||
|
||||
public static int[] createFaces() {
|
||||
int[] faces = new int[]{
|
||||
// TOP
|
||||
5, 0, 4, 1, 0, 5, //P5,T0, P4,T1, P0,T5
|
||||
5, 0, 0, 5, 1, 4, //P5,T0, P0,T5, P1,T4
|
||||
// RIGHT
|
||||
0, 5, 4, 6, 6, 11, //P0,T4 ,P4,T3, P6,T8
|
||||
0, 5, 6, 11, 2, 10, //P0,T4 ,P6,T8, P2,T9
|
||||
// FRONT
|
||||
1, 4, 0, 5, 2, 10, //P1,T5, P0,T4, P2,T9
|
||||
1, 4, 2, 10, 3, 9, //P1,T5, P2,T9, P3,T10
|
||||
// LEFT
|
||||
5, 3, 1, 4, 3, 9, //P5,T6, P1,T5, P3,T10
|
||||
5, 3, 3, 9, 7, 8, //P5,T6, P3,T10,P7,T11
|
||||
// BACK
|
||||
4, 6, 5, 7, 7, 12, //P4,T6, P5,T7, P7,T12
|
||||
4, 6, 7, 12, 6, 11, //P4,T6, P7,T12,P6,T11
|
||||
// BOTTOM
|
||||
3, 5, 2, 6, 6, 2, //P3,T2, P2,T1, P6,T5
|
||||
3, 5, 6, 2, 7, 1 //P3,T2, P6,T5, P7,T6
|
||||
};
|
||||
|
||||
int[] copy = faces.clone();
|
||||
ArrayUtils.reverse(copy);
|
||||
for (int i = 0; i < copy.length; i += 2) {
|
||||
int tmp = copy[i];
|
||||
copy[i] = copy[i + 1];
|
||||
copy[i + 1] = tmp;
|
||||
}
|
||||
return ArrayUtils.addAll(faces, copy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private double width, height, depth;
|
||||
private boolean isSlim;
|
||||
private Image skin;
|
||||
private Mesh model;
|
||||
|
||||
public SkinCube(float width, float height, float depth, float scaleX, float scaleY, float startX, float startY, float enlarge, boolean isSlim) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.depth = depth;
|
||||
this.isSlim = isSlim;
|
||||
setMesh(model = new Model(width + enlarge, height + enlarge, depth + enlarge, scaleX, scaleY, startX, startY, isSlim));
|
||||
}
|
||||
|
||||
public void setWidth(double width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setHeight(double height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setDepth(double depth) {
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
public double getDepth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public boolean isSlim() {
|
||||
return isSlim;
|
||||
}
|
||||
|
||||
public Mesh getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(Mesh model) {
|
||||
this.model = model;
|
||||
setMesh(model);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.transform.Rotate;
|
||||
|
||||
public class SkinGroup extends Group {
|
||||
|
||||
protected Rotate xRotate, yRotate, zRotate;
|
||||
|
||||
public SkinGroup(Rotate xRotate, Rotate yRotate, Rotate zRotate, Node... nodes) {
|
||||
this.xRotate = xRotate;
|
||||
this.yRotate = yRotate;
|
||||
this.zRotate = zRotate;
|
||||
Group group = new Group();
|
||||
group.getChildren().addAll(nodes);
|
||||
getChildren().add(addRotate(group, xRotate, yRotate, zRotate));
|
||||
}
|
||||
|
||||
protected Group addRotate(Group group, Rotate... rotates) {
|
||||
for (Rotate rotate : rotates)
|
||||
group = addRotate(group, rotate);
|
||||
return group;
|
||||
}
|
||||
|
||||
protected Group addRotate(Group group, Rotate rotate) {
|
||||
Group newGroup = new Group();
|
||||
group.getTransforms().add(rotate);
|
||||
newGroup.getChildren().add(group);
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
public Rotate getXRotate() {
|
||||
return xRotate;
|
||||
}
|
||||
|
||||
public Rotate getYRotate() {
|
||||
return yRotate;
|
||||
}
|
||||
|
||||
public Rotate getZRotate() {
|
||||
return zRotate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
public final class SkinHelper {
|
||||
|
||||
private SkinHelper() {
|
||||
}
|
||||
|
||||
public static class PixelCopyer {
|
||||
|
||||
protected Image srcImage;
|
||||
protected WritableImage newImage;
|
||||
|
||||
public PixelCopyer(Image srcImage, WritableImage newImage) {
|
||||
this.srcImage = srcImage;
|
||||
this.newImage = newImage;
|
||||
}
|
||||
|
||||
public void copy(int srcX, int srcY, int width, int height) {
|
||||
copy(srcX, srcY, srcX, srcY, width, height);
|
||||
}
|
||||
|
||||
public void copy(int srcX, int srcY, int toX, int toY, int width, int height) {
|
||||
copy(srcX, srcY, toX, toY, width, height, false, false);
|
||||
}
|
||||
|
||||
public void copy(int srcX, int srcY, int toX, int toY, int width, int height, boolean reversalX, boolean reversalY) {
|
||||
PixelReader reader = srcImage.getPixelReader();
|
||||
PixelWriter writer = newImage.getPixelWriter();
|
||||
for (int x = 0; x < width; x++)
|
||||
for (int y = 0; y < height; y++)
|
||||
writer.setArgb(toX + x, toY + y,
|
||||
reader.getArgb(srcX + (reversalX ? width - x - 1 : x), srcY + (reversalY ? height - y - 1 : y)));
|
||||
}
|
||||
|
||||
public void copy(float srcX, float srcY, float toX, float toY, float width, float height) {
|
||||
copy(srcX, srcY, toX, toY, width, height, false, false);
|
||||
}
|
||||
|
||||
public void copy(float srcX, float srcY, float toX, float toY, float width, float height, boolean reversalX, boolean reversalY) {
|
||||
PixelReader reader = srcImage.getPixelReader();
|
||||
PixelWriter writer = newImage.getPixelWriter();
|
||||
int srcScaleX = (int) srcImage.getWidth();
|
||||
int srcScaleY = (int) srcImage.getHeight();
|
||||
int newScaleX = (int) newImage.getWidth();
|
||||
int newScaleY = (int) newImage.getHeight();
|
||||
int srcWidth = (int) (width * srcScaleX);
|
||||
int srcHeight = (int) (height * srcScaleY);
|
||||
|
||||
for (int x = 0; x < srcWidth; x++)
|
||||
for (int y = 0; y < srcHeight; y++)
|
||||
writer.setArgb((int) (toX * newScaleX + x), (int) (toY * newScaleY + y),
|
||||
reader.getArgb((int) (srcX * srcScaleX + (reversalX ? srcWidth - x - 1 : x)),
|
||||
(int) (srcY * srcScaleY + (reversalY ? srcHeight - y - 1 : y))));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static boolean isNoRequest(Image image) {
|
||||
return image.getRequestedWidth() == 0 && image.getRequestedHeight() == 0;
|
||||
}
|
||||
|
||||
public static boolean isSkin(Image image) {
|
||||
return image.getWidth() % 64 == 0 && image.getWidth() / 64 > 0 &&
|
||||
(image.getHeight() == image.getWidth() / 2 || image.getHeight() == image.getWidth());
|
||||
}
|
||||
|
||||
public static Image x32Tox64(Image srcSkin) {
|
||||
if (srcSkin.getHeight() == 64)
|
||||
return srcSkin;
|
||||
|
||||
WritableImage newSkin = new WritableImage((int) srcSkin.getWidth(), (int) srcSkin.getHeight() * 2);
|
||||
PixelCopyer copyer = new PixelCopyer(srcSkin, newSkin);
|
||||
// HEAD & HAT
|
||||
copyer.copy(0 / 64F, 0 / 32F, 0 / 64F, 0 / 64F, 64 / 64F, 16 / 32F);
|
||||
// LEFT-LEG
|
||||
x32Tox64(copyer, 0 / 64F, 16 / 32F, 16 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
|
||||
// RIGHT-LEG
|
||||
copyer.copy(0 / 64F, 16 / 32F, 0 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
|
||||
// BODY
|
||||
copyer.copy(16 / 64F, 16 / 32F, 16 / 64F, 16 / 64F, 24 / 64F, 16 / 32F);
|
||||
// LEFT-ARM
|
||||
x32Tox64(copyer, 40 / 64F, 16 / 32F, 32 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
|
||||
// RIGHT-ARM
|
||||
copyer.copy(40 / 64F, 16 / 32F, 40 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
|
||||
|
||||
return newSkin;
|
||||
}
|
||||
|
||||
static void x32Tox64(PixelCopyer copyer, float srcX, float srcY, float toX, float toY, float width, float height, float depth) {
|
||||
// TOP
|
||||
copyer.copy(srcX + depth, srcY, toX + depth, toY, width, depth * 2, true, false);
|
||||
// BOTTOM
|
||||
copyer.copy(srcX + depth + width, srcY, toX + depth + width, toY, width, depth * 2, true, false);
|
||||
// INS
|
||||
copyer.copy(srcX, srcY + depth * 2, toX + width + depth, toY + depth, depth, height, true, false);
|
||||
// OUTS
|
||||
copyer.copy(srcX + width + depth, srcY + depth * 2, toX, toY + depth, depth, height, true, false);
|
||||
// FRONT
|
||||
copyer.copy(srcX + depth, srcY + depth * 2, toX + depth, toY + depth, width, height, true, false);
|
||||
// BACK
|
||||
copyer.copy(srcX + width + depth * 2, srcY + depth * 2, toX + width + depth * 2, toY + depth, width, height, true, false);
|
||||
}
|
||||
|
||||
public static Image enlarge(Image srcSkin, int multiple) {
|
||||
WritableImage newSkin = new WritableImage((int) srcSkin.getWidth() * multiple, (int) srcSkin.getHeight() * multiple);
|
||||
PixelReader reader = srcSkin.getPixelReader();
|
||||
PixelWriter writer = newSkin.getPixelWriter();
|
||||
|
||||
for (int x = 0, lenX = (int) srcSkin.getWidth(); x < lenX; x++)
|
||||
for (int y = 0, lenY = (int) srcSkin.getHeight(); y < lenY; y++)
|
||||
for (int mx = 0; mx < multiple; mx++)
|
||||
for (int my = 0; my < multiple; my++) {
|
||||
int argb = reader.getArgb(x, y);
|
||||
writer.setArgb(x * multiple + mx, y * multiple + my, argb);
|
||||
}
|
||||
|
||||
return newSkin;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Material;
|
||||
import javafx.scene.paint.PhongMaterial;
|
||||
import javafx.scene.shape.Box;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SkinMultipleCubes extends Group {
|
||||
|
||||
public static class Face extends Group {
|
||||
|
||||
public Face(Image image, int startX, int startY, int width, int height, int interval, boolean reverseX, boolean reverseY,
|
||||
Supplier<Box> supplier, BiConsumer<Box, Point2D> consumer) {
|
||||
PixelReader reader = image.getPixelReader();
|
||||
for (int x = 0; x < width; x++)
|
||||
for (int y = 0; y < height; y++) {
|
||||
int argb;
|
||||
if ((argb = reader.getArgb(startX + (reverseX ? width - x - 1 : x) * interval,
|
||||
startY + (reverseY ? height - y - 1 : y) * interval)) != 0) {
|
||||
Box pixel = supplier.get();
|
||||
consumer.accept(pixel, new Point2D(x, y));
|
||||
pixel.setMaterial(createMaterial(Color.rgb(
|
||||
(argb >> 16) & 0xFF, (argb >> 8) & 0xFF, (argb >> 0) & 0xFF)));
|
||||
getChildren().add(pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Material createMaterial(Color color) {
|
||||
return new PhongMaterial(color);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected int width, height, depth;
|
||||
protected float startX, startY;
|
||||
protected double length, thick;
|
||||
|
||||
public SkinMultipleCubes(int width, int height, int depth, float startX, float startY, double length, double thick) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.depth = depth;
|
||||
this.startX = startX;
|
||||
this.startY = startY;
|
||||
this.length = length;
|
||||
this.thick = thick;
|
||||
}
|
||||
|
||||
public void setWidth(int width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setHeight(int height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setDepth(int depth) {
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public void setStartX(float startX) {
|
||||
this.startX = startX;
|
||||
}
|
||||
|
||||
public float getStartX() {
|
||||
return startX;
|
||||
}
|
||||
|
||||
public void setStartY(float startY) {
|
||||
this.startY = startY;
|
||||
}
|
||||
|
||||
public float getStartY() {
|
||||
return startY;
|
||||
}
|
||||
|
||||
public void setLength(double length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public double getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setThick(double thick) {
|
||||
this.thick = thick;
|
||||
}
|
||||
|
||||
public double getThick() {
|
||||
return thick;
|
||||
}
|
||||
|
||||
public void updateSkin(Image skin) {
|
||||
getChildren().clear();
|
||||
int start_x = (int) (startX * skin.getWidth()), start_y = (int) (startY * skin.getHeight()),
|
||||
interval = (int) Math.max(skin.getWidth() / 64, 1),
|
||||
width_interval = width * interval, height_interval = height * interval, depth_interval = depth * interval;
|
||||
// FRONT
|
||||
getChildren().add(new Face(skin, start_x + depth_interval, start_y + depth_interval, width, height, interval, false, false,
|
||||
() -> new Box(length, length, thick), (b, p) -> {
|
||||
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ((depth * length + thick) / 2.0);
|
||||
}));
|
||||
// BACK
|
||||
getChildren().add(new Face(skin, start_x + width_interval + depth_interval * 2, start_y + depth_interval, width, height, interval, true, false,
|
||||
() -> new Box(length, length, thick), (b, p) -> {
|
||||
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ(-(depth * length + thick) / 2.0);
|
||||
}));
|
||||
// LEFT
|
||||
getChildren().add(new Face(skin, start_x + width_interval + depth_interval, start_y + depth_interval, depth, height, interval, false, false,
|
||||
() -> new Box(thick, length, length), (b, p) -> {
|
||||
b.setTranslateX((width * length + thick) / 2.0);
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
|
||||
}));
|
||||
// RIGHT
|
||||
getChildren().add(new Face(skin, start_x, start_y + depth_interval, depth, height, interval, true, false,
|
||||
() -> new Box(thick, length, length), (b, p) -> {
|
||||
b.setTranslateX(-(width * length + thick) / 2.0);
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
|
||||
}));
|
||||
// TOP
|
||||
getChildren().add(new Face(skin, start_x + depth_interval, start_y, width, depth, interval, false, false,
|
||||
() -> new Box(length, thick, length), (b, p) -> {
|
||||
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
|
||||
b.setTranslateY(-(height * length + thick) / 2.0);
|
||||
b.setTranslateZ(-((depth - 1) / 2.0 - p.getY()) * b.getDepth());
|
||||
}));
|
||||
// BOTTOM
|
||||
getChildren().add(new Face(skin, start_x + width_interval + depth_interval, start_y, width, depth, interval, false, false,
|
||||
() -> new Box(length, thick, length), (b, p) -> {
|
||||
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
|
||||
b.setTranslateY((height * length + thick) / 2.0);
|
||||
b.setTranslateZ(-((depth - 1) / 2.0 - p.getY()) * b.getDepth());
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class SkinTransition extends Transition {
|
||||
|
||||
protected Function<Double, Double> expression;
|
||||
protected List<WritableValue<Number>> observables;
|
||||
protected boolean fix;
|
||||
protected int count;
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public SkinTransition(Duration duration, Function<Double, Double> expression, WritableValue<Number>... observables) {
|
||||
setCycleDuration(duration);
|
||||
this.expression = expression;
|
||||
this.observables = Arrays.asList(observables);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
if (frac == 0 || frac == 1)
|
||||
count++;
|
||||
double val = expression.apply(frac);
|
||||
observables.forEach(w -> w.setValue(val));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
count = 0;
|
||||
super.play();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package moe.mickey.minecraft.skin.fx.animation;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import moe.mickey.minecraft.skin.fx.FunctionHelper;
|
||||
import moe.mickey.minecraft.skin.fx.SkinAnimation;
|
||||
import moe.mickey.minecraft.skin.fx.SkinCanvas;
|
||||
import moe.mickey.minecraft.skin.fx.SkinTransition;
|
||||
|
||||
public final class SkinAniRunning extends SkinAnimation {
|
||||
|
||||
private SkinTransition larmTransition, rarmTransition;
|
||||
|
||||
public SkinAniRunning(int weight, int time, double angle, SkinCanvas canvas) {
|
||||
larmTransition = new SkinTransition(Duration.millis(time),
|
||||
v -> v * (larmTransition.getCount() % 4 < 2 ? 1 : -1) * angle,
|
||||
canvas.larm.getXRotate().angleProperty(), canvas.rleg.getXRotate().angleProperty());
|
||||
|
||||
rarmTransition = new SkinTransition(Duration.millis(time),
|
||||
v -> v * (rarmTransition.getCount() % 4 < 2 ? 1 : -1) * -angle,
|
||||
canvas.rarm.getXRotate().angleProperty(), canvas.lleg.getXRotate().angleProperty());
|
||||
|
||||
FunctionHelper.alwaysB(SkinTransition::setAutoReverse, true, larmTransition, rarmTransition);
|
||||
FunctionHelper.alwaysB(SkinTransition::setCycleCount, 16, larmTransition, rarmTransition);
|
||||
FunctionHelper.always(transitions::add, larmTransition, rarmTransition);
|
||||
this.weight = weight;
|
||||
init();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package moe.mickey.minecraft.skin.fx.animation;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import moe.mickey.minecraft.skin.fx.FunctionHelper;
|
||||
import moe.mickey.minecraft.skin.fx.SkinAnimation;
|
||||
import moe.mickey.minecraft.skin.fx.SkinCanvas;
|
||||
import moe.mickey.minecraft.skin.fx.SkinTransition;
|
||||
|
||||
public final class SkinAniWavingArms extends SkinAnimation {
|
||||
|
||||
public SkinAniWavingArms(int weight, int time, double angle, SkinCanvas canvas) {
|
||||
SkinTransition larmTransition = new SkinTransition(Duration.millis(time), v -> v * angle,
|
||||
canvas.larm.getZRotate().angleProperty());
|
||||
|
||||
SkinTransition rarmTransition = new SkinTransition(Duration.millis(time), v -> v * -angle,
|
||||
canvas.rarm.getZRotate().angleProperty());
|
||||
|
||||
FunctionHelper.alwaysB(SkinTransition::setAutoReverse, true, larmTransition, rarmTransition);
|
||||
FunctionHelper.alwaysB(SkinTransition::setCycleCount, 2, larmTransition, rarmTransition);
|
||||
FunctionHelper.always(transitions::add, larmTransition, rarmTransition);
|
||||
this.weight = weight;
|
||||
init();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
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.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.net.*;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
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.i18n.I18n.i18n;
|
||||
|
||||
public final class Launcher extends Application {
|
||||
public static final CookieManager COOKIE_MANAGER = new CookieManager();
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
// runLater to ensure ConfigHolder.init() finished initialization
|
||||
Platform.runLater(() -> {
|
||||
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
|
||||
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
initIcon();
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
super.stop();
|
||||
Controllers.onApplicationStop();
|
||||
}
|
||||
|
||||
private void initIcon() {
|
||||
Toolkit toolkit = Toolkit.getDefaultToolkit();
|
||||
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
||||
|
||||
try {
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
LOG.info("Operating System: " + 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("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
|
||||
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"));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopApplication() {
|
||||
LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
|
||||
|
||||
runInFX(() -> {
|
||||
if (Controllers.getStage() == null)
|
||||
return;
|
||||
Controllers.getStage().close();
|
||||
Schedulers.shutdown();
|
||||
Controllers.shutdown();
|
||||
Platform.exit();
|
||||
});
|
||||
}
|
||||
|
||||
public static void stopWithoutPlatform() {
|
||||
LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
|
||||
|
||||
runInFX(() -> {
|
||||
if (Controllers.getStage() == null)
|
||||
return;
|
||||
Controllers.getStage().close();
|
||||
Schedulers.shutdown();
|
||||
Controllers.shutdown();
|
||||
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
File jarFile = new File(URLDecoder.decode(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8"));
|
||||
String ext = FileUtils.getExtension(jarFile);
|
||||
if ("jar".equals(ext) || "exe".equals(ext))
|
||||
result.add(jarFile);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
LOG.log(Level.WARNING, "Failed to decode jar path", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (result.isEmpty())
|
||||
return null;
|
||||
else
|
||||
return result;
|
||||
}
|
||||
|
||||
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
* 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
|
||||
@@ -13,123 +13,145 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.upgrade.IUpgrader;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.swing.*;
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.ResourceBundle;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class Main extends Application {
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
public final class Main {
|
||||
|
||||
try {
|
||||
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
|
||||
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
primaryStage.setResizable(false);
|
||||
primaryStage.setScene(Controllers.getScene());
|
||||
primaryStage.show();
|
||||
} catch (Throwable e) {
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
private Main() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
System.setProperty("java.net.useSystemProxies", "true");
|
||||
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
System.setProperty("javafx.autoproxy.disable", "true");
|
||||
// Fix title bar not displaying in GTK systems
|
||||
System.setProperty("jdk.gtk.version", "2");
|
||||
|
||||
// Use System look and feel
|
||||
initLookAndFeel();
|
||||
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
checkJavaFX();
|
||||
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
|
||||
private static void initLookAndFeel() {
|
||||
if (System.getProperty("swing.defaultlaf") == null) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
String currentDirectory = new File("").getAbsolutePath();
|
||||
if (currentDirectory.contains("!")) {
|
||||
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
|
||||
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
|
||||
+ "The path is " + currentDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkJavaFX() {
|
||||
try {
|
||||
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
|
||||
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
|
||||
IUpgrader.NOW_UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
|
||||
|
||||
Logging.LOG.info("*** " + TITLE + " ***");
|
||||
|
||||
UPDATE_CHECKER.process(false)
|
||||
.then(Task.of(Schedulers.javafx(), () -> {
|
||||
if (UPDATE_CHECKER.isOutOfDate())
|
||||
Controllers.showUpdate();
|
||||
}))
|
||||
.start();
|
||||
|
||||
launch(args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
SelfDependencyPatcher.patch();
|
||||
} catch (SelfDependencyPatcher.PatchException e) {
|
||||
LOG.log(Level.SEVERE, "unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.log(Level.SEVERE, "unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
} catch (CancellationException e) {
|
||||
LOG.log(Level.SEVERE, "User cancels downloading JavaFX", e);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopApplication() {
|
||||
JFXUtilities.runInFX(() -> {
|
||||
stopWithoutPlatform();
|
||||
Platform.exit();
|
||||
});
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
public static void stopWithoutPlatform() {
|
||||
JFXUtilities.runInFX(() -> {
|
||||
Controllers.getStage().close();
|
||||
|
||||
Logging.LOG.info("Shutting down executor services.");
|
||||
Schedulers.shutdown();
|
||||
});
|
||||
/**
|
||||
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
public static String i18n(String key) {
|
||||
static void fixLetsEncrypt() {
|
||||
try {
|
||||
return RESOURCE_BUNDLE.getString(key);
|
||||
} catch (Exception e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Cannot find key " + key + " in resource bundle", e);
|
||||
return key;
|
||||
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
|
||||
|
||||
try (InputStream ksStream = Files.newInputStream(ksPath)) {
|
||||
defaultKeyStore.load(ksStream, "changeit".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
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]);
|
||||
for (String alias : Collections.list(letsEncryptKeyStore.aliases()))
|
||||
merged.setCertificateEntry(alias, letsEncryptKeyStore.getCertificate(alias));
|
||||
for (String alias : Collections.list(defaultKeyStore.aliases()))
|
||||
merged.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
|
||||
|
||||
TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
instance.init(merged);
|
||||
SSLContext tls = SSLContext.getInstance("TLS");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public static String i18n(String key, Object... formatArgs) {
|
||||
return String.format(i18n(key), formatArgs);
|
||||
}
|
||||
|
||||
public static File getWorkingDirectory(String folder) {
|
||||
String home = System.getProperty("user.home", ".");
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case LINUX:
|
||||
return new File(home, "." + folder + "/");
|
||||
case WINDOWS:
|
||||
String appdata = System.getenv("APPDATA");
|
||||
return new File(Lang.nonNull(appdata, home), "." + folder + "/");
|
||||
case OSX:
|
||||
return new File(home, "Library/Application Support/" + folder);
|
||||
default:
|
||||
return new File(home, folder + "/");
|
||||
}
|
||||
}
|
||||
|
||||
public static final File MINECRAFT_DIRECTORY = getWorkingDirectory("minecraft");
|
||||
|
||||
public static final String VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@";
|
||||
public static final String NAME = "HMCL";
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final File APPDATA = getWorkingDirectory("hmcl");
|
||||
public static final ResourceBundle RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
|
||||
public static final UpdateChecker UPDATE_CHECKER = new UpdateChecker(VersionNumber.asVersion(VERSION));
|
||||
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
|
||||
|
||||
public static final String CONTACT = "http://huangyuhui.duapp.com/hmcl.php";
|
||||
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Stores metadata about this application.
|
||||
*/
|
||||
public final class Metadata {
|
||||
private Metadata() {}
|
||||
|
||||
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.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
|
||||
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + 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 EULA_URL = "https://hmcl.huangyuhui.net/eula";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Build-Channel")).orElse("nightly");
|
||||
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
|
||||
|
||||
private static Path getHMCLDirectory() {
|
||||
String home = System.getProperty("user.home", ".");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
// to fulfill XDG standard.
|
||||
String xdgCache = System.getenv("XDG_CACHE_HOME");
|
||||
if (StringUtils.isNotBlank(xdgCache)) {
|
||||
return Paths.get(xdgCache, "hmcl");
|
||||
}
|
||||
return Paths.get(home, ".cache", "hmcl");
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.event;
|
||||
|
||||
import org.jackhuang.hmcl.util.VersionNumber;
|
||||
|
||||
/**
|
||||
*
|
||||
* Result: Deny if do not upgrade HMCL.
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class OutOfDateEvent extends Event {
|
||||
private final VersionNumber version;
|
||||
|
||||
public OutOfDateEvent(Object source, VersionNumber version) {
|
||||
super(source);
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public VersionNumber getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.event;
|
||||
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
|
||||
/**
|
||||
* This event gets fired when the selected profile changed.
|
||||
* <br>
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class ProfileChangedEvent extends Event {
|
||||
private final Profile profile;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.setting.Settings}
|
||||
* @param profile the new profile.
|
||||
*/
|
||||
public ProfileChangedEvent(Object source, Profile profile) {
|
||||
super(source);
|
||||
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
public Profile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.event;
|
||||
|
||||
/**
|
||||
* This event gets fired when loading profiles.
|
||||
* <br>
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class ProfileLoadingEvent extends Event {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.setting.Settings}
|
||||
*/
|
||||
public ProfileLoadingEvent(Object source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.ProfileTexture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Scheduler;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class AccountHelper {
|
||||
public static final AccountHelper INSTANCE = new AccountHelper();
|
||||
private AccountHelper() {}
|
||||
|
||||
public static final File SKIN_DIR = new File(Main.APPDATA, "skins");
|
||||
|
||||
public static void loadSkins() {
|
||||
loadSkins(Proxy.NO_PROXY);
|
||||
}
|
||||
|
||||
public static void loadSkins(Proxy proxy) {
|
||||
for (Account account : Settings.INSTANCE.getAccounts()) {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
new SkinLoadTask((YggdrasilAccount) account, proxy, false).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Task loadSkinAsync(YggdrasilAccount account) {
|
||||
return loadSkinAsync(account, Settings.INSTANCE.getProxy());
|
||||
}
|
||||
|
||||
public static Task loadSkinAsync(YggdrasilAccount account, Proxy proxy) {
|
||||
return new SkinLoadTask(account, proxy, false);
|
||||
}
|
||||
|
||||
public static Task refreshSkinAsync(YggdrasilAccount account) {
|
||||
return refreshSkinAsync(account, Settings.INSTANCE.getProxy());
|
||||
}
|
||||
|
||||
public static Task refreshSkinAsync(YggdrasilAccount account, Proxy proxy) {
|
||||
return new SkinLoadTask(account, proxy, true);
|
||||
}
|
||||
|
||||
private static File getSkinFile(String name) {
|
||||
return new File(SKIN_DIR, name + ".png");
|
||||
}
|
||||
|
||||
public static Image getSkin(YggdrasilAccount account) {
|
||||
return getSkin(account, 1);
|
||||
}
|
||||
|
||||
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
|
||||
if (account.getSelectedProfile() == null)
|
||||
return getDefaultSkin(account, scaleRatio);
|
||||
String name = account.getSelectedProfile().getName();
|
||||
if (name == null)
|
||||
return getDefaultSkin(account, scaleRatio);
|
||||
File file = getSkinFile(name);
|
||||
if (file.exists()) {
|
||||
Image original = new Image("file:" + file.getAbsolutePath());
|
||||
return new Image("file:" + file.getAbsolutePath(),
|
||||
original.getWidth() * scaleRatio,
|
||||
original.getHeight() * scaleRatio,
|
||||
false, false);
|
||||
}
|
||||
return getDefaultSkin(account, scaleRatio);
|
||||
}
|
||||
|
||||
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio, Proxy proxy) throws Exception {
|
||||
String name = profile.getName();
|
||||
File file = getSkinFile(name);
|
||||
downloadSkin(account, profile, true, proxy);
|
||||
if (!file.exists())
|
||||
return getDefaultSkin(account, scaleRatio);
|
||||
|
||||
String url = "file:" + file.getAbsolutePath();
|
||||
return scale(url, scaleRatio);
|
||||
}
|
||||
|
||||
public static Rectangle2D getViewport(double scaleRatio) {
|
||||
double size = 8.0 * scaleRatio;
|
||||
return new Rectangle2D(size, size, size, size);
|
||||
}
|
||||
|
||||
private static class SkinLoadTask extends Task {
|
||||
private final YggdrasilAccount account;
|
||||
private final Proxy proxy;
|
||||
private final boolean refresh;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
public SkinLoadTask(YggdrasilAccount account, Proxy proxy) {
|
||||
this(account, proxy, false);
|
||||
}
|
||||
|
||||
public SkinLoadTask(YggdrasilAccount account, Proxy proxy, boolean refresh) {
|
||||
this.account = account;
|
||||
this.proxy = proxy;
|
||||
this.refresh = refresh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scheduler getScheduler() {
|
||||
return Schedulers.io();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (account.canLogIn() && (account.getSelectedProfile() == null || refresh))
|
||||
DialogController.logIn(account);
|
||||
|
||||
downloadSkin(account, account.getSelectedProfile(), refresh, proxy);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh, Proxy proxy) throws Exception {
|
||||
if (profile == null) return;
|
||||
String name = profile.getName();
|
||||
if (name == null) return;
|
||||
Optional<ProfileTexture> texture = account.getSkin(profile);
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
File file = getSkinFile(name);
|
||||
if (!refresh && file.exists())
|
||||
return;
|
||||
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();
|
||||
}
|
||||
|
||||
public static Image scale(String url, double scaleRatio) {
|
||||
Image origin = new Image(url);
|
||||
return new Image(url,
|
||||
origin.getWidth() * scaleRatio,
|
||||
origin.getHeight() * scaleRatio,
|
||||
false, false);
|
||||
}
|
||||
|
||||
public static Image getSteveSkin(double scaleRatio) {
|
||||
return scale("/assets/img/steve.png", 4);
|
||||
}
|
||||
|
||||
public static Image getAlexSkin(double scaleRatio) {
|
||||
return scale("/assets/img/alex.png", 4);
|
||||
}
|
||||
|
||||
public static Image getDefaultSkin(Account account, double scaleRatio) {
|
||||
if (account == null)
|
||||
return getSteveSkin(scaleRatio);
|
||||
|
||||
int type = account.getUUID().hashCode() & 1;
|
||||
if (type == 1)
|
||||
return getAlexSkin(scaleRatio);
|
||||
else
|
||||
return getSteveSkin(scaleRatio);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import org.jackhuang.hmcl.download.DefaultCacheRepository;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class HMCLCacheRepository extends DefaultCacheRepository {
|
||||
|
||||
private final StringProperty directory = new SimpleStringProperty();
|
||||
|
||||
public HMCLCacheRepository() {
|
||||
directory.addListener((a, b, t) -> changeDirectory(Paths.get(t)));
|
||||
}
|
||||
|
||||
public String getDirectory() {
|
||||
return directory.get();
|
||||
}
|
||||
|
||||
public StringProperty directoryProperty() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public void setDirectory(String directory) {
|
||||
this.directory.set(directory);
|
||||
}
|
||||
|
||||
public static final HMCLCacheRepository REPOSITORY = new HMCLCacheRepository();
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
|
||||
import java.net.Proxy;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class HMCLDependencyManager extends DefaultDependencyManager {
|
||||
private final Profile profile;
|
||||
|
||||
public HMCLDependencyManager(Profile profile, DownloadProvider downloadProvider) {
|
||||
this(profile, downloadProvider, Proxy.NO_PROXY);
|
||||
}
|
||||
|
||||
public HMCLDependencyManager(Profile profile, DownloadProvider downloadProvider, Proxy proxy) {
|
||||
super(profile.getRepository(), downloadProvider, proxy);
|
||||
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameBuilder gameBuilder() {
|
||||
return new HMCLGameBuilder(profile);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DefaultGameBuilder;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class HMCLGameBuilder extends DefaultGameBuilder {
|
||||
private final Profile profile;
|
||||
|
||||
public HMCLGameBuilder(Profile profile) {
|
||||
super(profile.getDependency());
|
||||
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task downloadGameAsync(String gameVersion, Version version) {
|
||||
return new HMCLGameDownloadTask(profile, gameVersion, version);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class HMCLGameDownloadTask extends Task {
|
||||
private final Profile profile;
|
||||
private final String gameVersion;
|
||||
private final Version version;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
public HMCLGameDownloadTask(Profile profile, String gameVersion, Version version) {
|
||||
this.profile = profile;
|
||||
this.gameVersion = gameVersion;
|
||||
this.version = version;
|
||||
|
||||
setSignificance(TaskSignificance.MINOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws IOException {
|
||||
File jar = profile.getRepository().getVersionJar(version);
|
||||
|
||||
File cache = new File(Settings.INSTANCE.getCommonPath(), "jars/" + gameVersion + ".jar");
|
||||
if (cache.exists())
|
||||
try {
|
||||
FileUtils.copyFile(cache, jar);
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to copy cached Minecraft jar from " + cache + " to " + jar, e);
|
||||
}
|
||||
|
||||
dependencies.add(new FileDownloadTask(
|
||||
NetworkUtils.toURL(profile.getDependency().getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
|
||||
cache,
|
||||
profile.getDependency().getProxy(),
|
||||
version.getDownloadInfo().getSha1()
|
||||
).then(Task.of(v -> FileUtils.copyFile(cache, jar))));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,39 +13,72 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.Main;
|
||||
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 java.util.List;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options) {
|
||||
this(repository, versionId, authInfo, options, null);
|
||||
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options) {
|
||||
this(repository, version, authInfo, options, null);
|
||||
}
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
|
||||
this(repository, versionId, authInfo, options, listener, true);
|
||||
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
|
||||
this(repository, version, authInfo, options, listener, true);
|
||||
}
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
|
||||
super(repository, versionId, authInfo, options, listener, daemon);
|
||||
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
|
||||
super(repository, version, authInfo, options, listener, daemon);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void appendJvmArgs(List<String> result) {
|
||||
super.appendJvmArgs(result);
|
||||
protected Map<String, String> getConfigurations() {
|
||||
Map<String, String> res = super.getConfigurations();
|
||||
res.put("${launcher_name}", Metadata.NAME);
|
||||
res.put("${launcher_version}", Metadata.VERSION);
|
||||
return res;
|
||||
}
|
||||
|
||||
result.add("-Dminecraft.launcher.version=" + Main.VERSION);
|
||||
result.add("-Dminecraft.launcher.brand=" + Main.NAME);
|
||||
private void generateOptionsTxt() {
|
||||
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManagedProcess launch() throws IOException, InterruptedException {
|
||||
generateOptionsTxt();
|
||||
return super.launch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeLaunchScript(File scriptFile) throws IOException {
|
||||
generateOptionsTxt();
|
||||
super.makeLaunchScript(scriptFile);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,36 +13,50 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||
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.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.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.setting.ProxyManager;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
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.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
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.LOG;
|
||||
|
||||
public class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final Profile profile;
|
||||
private final Map<String, VersionSetting> versionSettings = new HashMap<>();
|
||||
private final Set<String> beingModpackVersions = new HashSet<>();
|
||||
|
||||
public boolean checkedModpack = false, checkingModpack = false;
|
||||
// local version settings
|
||||
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
|
||||
private final Set<String> beingModpackVersions = new HashSet<>();
|
||||
|
||||
public HMCLGameRepository(Profile profile, File baseDirectory) {
|
||||
super(baseDirectory);
|
||||
@@ -53,65 +67,59 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return profile;
|
||||
}
|
||||
|
||||
private boolean useSelf(String version, String assetId) {
|
||||
VersionSetting vs = profile.getVersionSetting(version);
|
||||
return new File(getBaseDirectory(), "assets/indexes/" + assetId + ".json").exists() || vs.isNoCommon();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getAssetDirectory(String version, String assetId) {
|
||||
if (useSelf(version, assetId))
|
||||
return super.getAssetDirectory(version, assetId);
|
||||
else
|
||||
return new File(Settings.INSTANCE.getCommonPath(), "assets");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getRunDirectory(String id) {
|
||||
if (beingModpackVersions.contains(id) || isModpack(id))
|
||||
return getVersionRoot(id);
|
||||
else {
|
||||
VersionSetting vs = profile.getVersionSetting(id);
|
||||
switch (vs.getGameDirType()) {
|
||||
case VERSION_FOLDER: return getVersionRoot(id);
|
||||
case ROOT_FOLDER: return super.getRunDirectory(id);
|
||||
case CUSTOM: return new File(vs.getGameDir());
|
||||
default: throw new Error();
|
||||
}
|
||||
public GameDirectoryType getGameDirectoryType(String id) {
|
||||
if (beingModpackVersions.contains(id) || isModpack(id)) {
|
||||
return GameDirectoryType.VERSION_FOLDER;
|
||||
} else {
|
||||
return getVersionSetting(id).getGameDirType();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getLibraryFile(Version version, Library lib) {
|
||||
VersionSetting vs = profile.getVersionSetting(version.getId());
|
||||
File self = super.getLibraryFile(version, lib);
|
||||
if (self.exists() || vs.isNoCommon())
|
||||
return self;
|
||||
else
|
||||
return new File(Settings.INSTANCE.getCommonPath(), "libraries/" + lib.getPath());
|
||||
public File getRunDirectory(String id) {
|
||||
switch (getGameDirectoryType(id)) {
|
||||
case VERSION_FOLDER:
|
||||
return getVersionRoot(id);
|
||||
case ROOT_FOLDER:
|
||||
return super.getRunDirectory(id);
|
||||
case CUSTOM:
|
||||
File dir = new File(getVersionSetting(id).getGameDir());
|
||||
if (!FileUtils.isValidPath(dir)) return getVersionRoot(id);
|
||||
return dir;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<Version> getDisplayVersions() {
|
||||
return getVersions().stream()
|
||||
.filter(v -> !v.isHidden())
|
||||
.sorted(Comparator.comparing((Version v) -> v.getReleaseTime() == null ? new Date(0L) : v.getReleaseTime())
|
||||
.thenComparing(v -> VersionNumber.asVersion(v.getId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshVersionsImpl() {
|
||||
versionSettings.clear();
|
||||
localVersionSettings.clear();
|
||||
super.refreshVersionsImpl();
|
||||
versions.keySet().forEach(this::loadVersionSetting);
|
||||
versions.keySet().forEach(this::loadLocalVersionSetting);
|
||||
versions.keySet().forEach(version -> {
|
||||
if (isModpack(version)) {
|
||||
specializeVersionSetting(version);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
File file = new File(getBaseDirectory(), "launcher_profiles.json");
|
||||
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.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshVersions() {
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this));
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
// https://github.com/huanghongxun/HMCL/issues/938
|
||||
System.gc();
|
||||
}
|
||||
|
||||
public void changeDirectory(File newDirectory) {
|
||||
@@ -119,19 +127,80 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
refreshVersionsAsync().start();
|
||||
}
|
||||
|
||||
private File getVersionSettingFile(String id) {
|
||||
private void clean(File directory) throws IOException {
|
||||
FileUtils.deleteDirectory(new File(directory, "crash-reports"));
|
||||
FileUtils.deleteDirectory(new File(directory, "logs"));
|
||||
}
|
||||
|
||||
public void clean(String id) throws IOException {
|
||||
clean(getBaseDirectory());
|
||||
clean(getRunDirectory(id));
|
||||
}
|
||||
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
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)));
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
oldVersionSetting.setUsesGlobal(false);
|
||||
oldVersionSetting.setGameDirType(GameDirectoryType.VERSION_FOLDER);
|
||||
VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting);
|
||||
saveVersionSetting(dstId);
|
||||
|
||||
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
|
||||
));
|
||||
blackList.add(srcId + ".jar");
|
||||
blackList.add(srcId + ".json");
|
||||
if (!copySaves)
|
||||
blackList.add("saves");
|
||||
|
||||
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
|
||||
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
|
||||
}
|
||||
|
||||
private File getLocalVersionSettingFile(String id) {
|
||||
return new File(getVersionRoot(id), "hmclversion.cfg");
|
||||
}
|
||||
|
||||
private void loadVersionSetting(String id) {
|
||||
File file = getVersionSettingFile(id);
|
||||
private void loadLocalVersionSetting(String id) {
|
||||
File file = getLocalVersionSettingFile(id);
|
||||
if (file.exists())
|
||||
try {
|
||||
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
|
||||
initVersionSetting(id, versionSetting);
|
||||
initLocalVersionSetting(id, versionSetting);
|
||||
} catch (Exception ex) {
|
||||
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
|
||||
initVersionSetting(id, new VersionSetting());
|
||||
initLocalVersionSetting(id, new VersionSetting());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,18 +209,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
* @param id the version id.
|
||||
* @return new version setting, null if given version does not exist.
|
||||
*/
|
||||
public VersionSetting createVersionSetting(String id) {
|
||||
public VersionSetting createLocalVersionSetting(String id) {
|
||||
if (!hasVersion(id))
|
||||
return null;
|
||||
if (versionSettings.containsKey(id))
|
||||
return getVersionSetting(id);
|
||||
if (localVersionSettings.containsKey(id))
|
||||
return getLocalVersionSetting(id);
|
||||
else
|
||||
return initVersionSetting(id, new VersionSetting());
|
||||
return initLocalVersionSetting(id, new VersionSetting());
|
||||
}
|
||||
|
||||
private VersionSetting initVersionSetting(String id, VersionSetting vs) {
|
||||
private VersionSetting initLocalVersionSetting(String id, VersionSetting vs) {
|
||||
localVersionSettings.put(id, vs);
|
||||
vs.addPropertyChangedListener(a -> saveVersionSetting(id));
|
||||
versionSettings.put(id, vs);
|
||||
return vs;
|
||||
}
|
||||
|
||||
@@ -160,29 +229,137 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
*
|
||||
* @param id version id
|
||||
*
|
||||
* @return may return null if the id not exists
|
||||
* @return corresponding version setting, null if the version has no its own version setting.
|
||||
*/
|
||||
public VersionSetting getVersionSetting(String id) {
|
||||
if (!versionSettings.containsKey(id))
|
||||
loadVersionSetting(id);
|
||||
VersionSetting setting = versionSettings.get(id);
|
||||
public VersionSetting getLocalVersionSetting(String id) {
|
||||
if (!localVersionSettings.containsKey(id))
|
||||
loadLocalVersionSetting(id);
|
||||
VersionSetting setting = localVersionSettings.get(id);
|
||||
if (setting != null && isModpack(id))
|
||||
setting.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
|
||||
setting.setGameDirType(GameDirectoryType.VERSION_FOLDER);
|
||||
return setting;
|
||||
}
|
||||
|
||||
public File getVersionIcon(String id) {
|
||||
public VersionSetting getVersionSetting(String id) {
|
||||
VersionSetting vs = getLocalVersionSetting(id);
|
||||
if (vs == null || vs.isUsesGlobal()) {
|
||||
profile.getGlobal().setGlobal(true); // always keep global.isGlobal = true
|
||||
profile.getGlobal().setUsesGlobal(true);
|
||||
return profile.getGlobal();
|
||||
} else
|
||||
return vs;
|
||||
}
|
||||
|
||||
public File getVersionIconFile(String id) {
|
||||
return new File(getVersionRoot(id), "icon.png");
|
||||
}
|
||||
|
||||
public void saveVersionSetting(String id) {
|
||||
if (!versionSettings.containsKey(id))
|
||||
return;
|
||||
Lang.invoke(() -> FileUtils.writeText(getVersionSettingFile(id), GSON.toJson(versionSettings.get(id))));
|
||||
public Image getVersionIconImage(String id) {
|
||||
if (id == null || !isLoaded())
|
||||
return newImage("/assets/img/grass.png");
|
||||
|
||||
Version version = getVersion(id).resolve(this);
|
||||
File iconFile = getVersionIconFile(id);
|
||||
if (iconFile.exists())
|
||||
return new Image("file:" + iconFile.getAbsolutePath());
|
||||
else if (LibraryAnalyzer.isModded(this, version))
|
||||
return newImage("/assets/img/furnace.png");
|
||||
else
|
||||
return newImage("/assets/img/grass.png");
|
||||
}
|
||||
|
||||
public boolean forbidsVersion(String id) {
|
||||
return FORBIDDEN.contains(id);
|
||||
public boolean saveVersionSetting(String id) {
|
||||
if (!localVersionSettings.containsKey(id))
|
||||
return false;
|
||||
File file = getLocalVersionSettingFile(id);
|
||||
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
|
||||
return false;
|
||||
|
||||
try {
|
||||
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make version use self version settings instead of the global one.
|
||||
* @param id the version id.
|
||||
* @return specialized version setting, null if given version does not exist.
|
||||
*/
|
||||
public VersionSetting specializeVersionSetting(String id) {
|
||||
VersionSetting vs = getLocalVersionSetting(id);
|
||||
if (vs == null)
|
||||
vs = createLocalVersionSetting(id);
|
||||
if (vs == null)
|
||||
return null;
|
||||
vs.setUsesGlobal(false);
|
||||
return vs;
|
||||
}
|
||||
|
||||
public void globalizeVersionSetting(String id) {
|
||||
VersionSetting vs = getLocalVersionSetting(id);
|
||||
if (vs != null)
|
||||
vs.setUsesGlobal(true);
|
||||
}
|
||||
|
||||
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir) {
|
||||
VersionSetting vs = getVersionSetting(version);
|
||||
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setVersionName(version)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory((int)(getAllocatedMemory(
|
||||
vs.getMaxMemory() * 1024L * 1024L,
|
||||
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
|
||||
vs.isAutoMemory()
|
||||
) / 1024 / 1024))
|
||||
.setMinMemory(vs.getMinMemory())
|
||||
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
|
||||
.setWidth(vs.getWidth())
|
||||
.setHeight(vs.getHeight())
|
||||
.setFullscreen(vs.isFullscreen())
|
||||
.setServerIp(vs.getServerIp())
|
||||
.setWrapper(vs.getWrapper())
|
||||
.setPreLaunchCommand(vs.getPreLaunchCommand())
|
||||
.setPostExitCommand(vs.getPostExitCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
.setProcessPriority(vs.getProcessPriority())
|
||||
.setUseNativeGLFW(vs.isUseNativeGLFW())
|
||||
.setUseNativeOpenAL(vs.isUseNativeOpenAL());
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxy(ProxyManager.getProxy());
|
||||
if (config().hasProxyAuth()) {
|
||||
builder.setProxyUser(config().getProxyUser());
|
||||
builder.setProxyPass(config().getProxyPass());
|
||||
}
|
||||
}
|
||||
|
||||
File json = getModpackConfiguration(version);
|
||||
if (json.exists()) {
|
||||
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);
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -198,11 +375,60 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
beingModpackVersions.remove(id);
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting()
|
||||
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
|
||||
public void markVersionLaunchedAbnormally(String id) {
|
||||
try {
|
||||
Files.createFile(getVersionRoot(id).toPath().resolve(".abnormal"));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean unmarkVersionLaunchedAbnormally(String id) {
|
||||
File file = new File(getVersionRoot(id), ".abnormal");
|
||||
boolean result = file.isFile();
|
||||
file.delete();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.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()))
|
||||
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) {
|
||||
return auto ? Math.max(minimum, (long) (available * 0.8)) : minimum;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.ZipEngine;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Export the game to a mod pack file.
|
||||
*/
|
||||
public class HMCLModpackExportTask extends TaskResult<ZipEngine> {
|
||||
private final DefaultGameRepository repository;
|
||||
private final String version;
|
||||
private final List<String> whitelist;
|
||||
private final Modpack modpack;
|
||||
private final File output;
|
||||
private final String id;
|
||||
|
||||
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
|
||||
this(repository, version, whitelist, modpack, output, ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param output mod pack file.
|
||||
* @param version to locate version.json
|
||||
*/
|
||||
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output, String id) {
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.whitelist = whitelist;
|
||||
this.modpack = modpack;
|
||||
this.output = output;
|
||||
this.id = id;
|
||||
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed()) output.delete();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
ArrayList<String> blackList = new ArrayList<>(HMCLModpackManager.MODPACK_BLACK_LIST);
|
||||
blackList.add(version + ".jar");
|
||||
blackList.add(version + ".json");
|
||||
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
|
||||
try (ZipEngine zip = new ZipEngine(output)) {
|
||||
zip.putDirectory(repository.getRunDirectory(version), (String pathName, Boolean isDirectory) -> {
|
||||
for (String s : blackList)
|
||||
if (isDirectory) {
|
||||
if (pathName.startsWith(s + "/"))
|
||||
return null;
|
||||
} else if (pathName.equals(s))
|
||||
return null;
|
||||
for (String s : whitelist)
|
||||
if (pathName.equals(s + (isDirectory ? "/" : "")))
|
||||
return "minecraft/" + pathName;
|
||||
return null;
|
||||
});
|
||||
|
||||
Version mv = repository.getVersion(version).resolve(repository);
|
||||
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
|
||||
.orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + version));
|
||||
zip.putTextFile(Constants.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
|
||||
zip.putTextFile(Constants.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
|
||||
}
|
||||
}
|
||||
|
||||
public static final String ID = "zip_engine";
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,44 +13,46 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DependencyManager;
|
||||
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
|
||||
import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.ModpackInstallTask;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class HMCLModpackInstallTask extends Task {
|
||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final File zipFile;
|
||||
private final String name;
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependency;
|
||||
private final Modpack modpack;
|
||||
private final File run;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
private final List<Task> dependents = new LinkedList<>();
|
||||
private final List<Task<?>> dependencies = new LinkedList<>();
|
||||
private final List<Task<?>> dependents = new LinkedList<>();
|
||||
|
||||
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
|
||||
DependencyManager dependency = profile.getDependency();
|
||||
dependency = profile.getDependency();
|
||||
repository = profile.getRepository();
|
||||
this.zipFile = zipFile;
|
||||
this.name = name;
|
||||
this.modpack = modpack;
|
||||
this.run = repository.getRunDirectory(name);
|
||||
|
||||
File run = repository.getRunDirectory(name);
|
||||
File json = repository.getModpackConfiguration(name);
|
||||
if (repository.hasVersion(name) && !json.exists())
|
||||
throw new IllegalArgumentException("Version " + name + " already exists");
|
||||
@@ -64,7 +66,7 @@ public final class HMCLModpackInstallTask extends Task {
|
||||
ModpackConfiguration<Modpack> config = null;
|
||||
try {
|
||||
if (json.exists()) {
|
||||
config = Constants.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
|
||||
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
|
||||
}.getType());
|
||||
|
||||
if (!MODPACK_TYPE.equals(config.getType()))
|
||||
@@ -72,25 +74,35 @@ public final class HMCLModpackInstallTask extends Task {
|
||||
}
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, "minecraft/", it -> !Objects.equals(it, "minecraft/pack.json"), config));
|
||||
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"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getDependencies() {
|
||||
public List<Task<?>> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getDependents() {
|
||||
public List<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
|
||||
Version version = Constants.GSON.fromJson(json, Version.class).setJar(null);
|
||||
dependencies.add(new VersionJsonSaveTask(repository, version));
|
||||
dependencies.add(new MinecraftInstanceTask<>(zipFile, "minecraft/", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
|
||||
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion);
|
||||
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
|
||||
// reinstall libraries
|
||||
// libraries of Forge and OptiFine should be obtained by installation.
|
||||
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||
if (LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(mark.getLibraryId()))
|
||||
continue;
|
||||
libraryTask = libraryTask.thenComposeAsync(version -> dependency.installLibraryAsync(modpack.getGameVersion(), version, mark.getLibraryId(), mark.getLibraryVersion()));
|
||||
}
|
||||
|
||||
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "HMCL";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,87 +13,58 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* 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.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.util.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
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.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.FileSystem;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class HMCLModpackManager {
|
||||
|
||||
public static final List<String> MODPACK_BLACK_LIST = Arrays.asList(
|
||||
"usernamecache.json", "usercache.json", // Minecraft
|
||||
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
|
||||
"pack.json", "launcher.jar", "hmclmc.log", // HMCL
|
||||
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
|
||||
"minetweaker.log", // Mods
|
||||
"logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
|
||||
"downloads", // Curse
|
||||
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods
|
||||
);
|
||||
public static final List<String> MODPACK_SUGGESTED_BLACK_LIST = Arrays.asList(
|
||||
"fonts", // BetterFonts
|
||||
"saves", "servers.dat", "options.txt", // Minecraft
|
||||
"blueprints" /* BuildCraft */,
|
||||
"optionsof.txt" /* OptiFine */,
|
||||
"journeymap" /* JourneyMap */,
|
||||
"optionsshaders.txt",
|
||||
"mods/VoxelMods");
|
||||
|
||||
public static ModAdviser.ModSuggestion suggestMod(String fileName, boolean isDirectory) {
|
||||
if (match(MODPACK_BLACK_LIST, fileName, isDirectory))
|
||||
return ModAdviser.ModSuggestion.HIDDEN;
|
||||
if (match(MODPACK_SUGGESTED_BLACK_LIST, fileName, isDirectory))
|
||||
return ModAdviser.ModSuggestion.NORMAL;
|
||||
else
|
||||
return ModAdviser.ModSuggestion.SUGGESTED;
|
||||
};
|
||||
|
||||
private static boolean match(List<String> l, String fileName, boolean isDirectory) {
|
||||
for (String s : l)
|
||||
if (isDirectory) {
|
||||
if (fileName.startsWith(s + "/"))
|
||||
return true;
|
||||
} else if (fileName.equals(s))
|
||||
return true;
|
||||
return false;
|
||||
private HMCLModpackManager() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the manifest in a HMCL modpack.
|
||||
*
|
||||
* @param file a HMCL modpack file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the manifest.json is missing or malformed.
|
||||
* @param fs a HMCL modpack file.
|
||||
* @param encoding encoding of modpack zip file.
|
||||
* @return the manifest of HMCL modpack.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the manifest.json is missing or malformed.
|
||||
*/
|
||||
public static Modpack readHMCLModpackManifest(File file) throws IOException, JsonParseException {
|
||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
|
||||
Modpack manifest = Constants.GSON.fromJson(manifestJson, Modpack.class);
|
||||
if (manifest == null)
|
||||
throw new JsonParseException("`modpack.json` not found. " + file + " is not a valid HMCL modpack.");
|
||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
|
||||
Version game = Constants.GSON.fromJson(gameJson, Version.class);
|
||||
if (game == null)
|
||||
throw new JsonParseException("`minecraft/pack.json` not found. " + file + " iot a valid HMCL modpack.");
|
||||
public static Modpack readHMCLModpackManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = FileUtils.readText(fs.getPath("modpack.json"));
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
|
||||
String gameJson = FileUtils.readText(fs.getPath("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 + ".");
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack");
|
||||
else
|
||||
return manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||
else
|
||||
return manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,7 +13,7 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,11 +13,11 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public enum LoadingState {
|
||||
DEPENDENCIES("launch.state.dependencies"),
|
||||
@@ -33,6 +33,6 @@ public enum LoadingState {
|
||||
}
|
||||
|
||||
public String getLocalizedMessage() {
|
||||
return Main.i18n(key);
|
||||
return i18n(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
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)) {
|
||||
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) {
|
||||
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log");
|
||||
}
|
||||
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) {
|
||||
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log");
|
||||
}
|
||||
zipper.putTextFile(Logging.getLogs(), "hmcl.log");
|
||||
zipper.putTextFile(logs, "minecraft.log");
|
||||
zipper.putTextFile(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);
|
||||
}
|
||||
}, Schedulers.io());
|
||||
}
|
||||
}
|
||||
+12
-18
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,26 +13,20 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.task;
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
/**
|
||||
* A task that has a result.
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public abstract class TaskResult<V> extends Task {
|
||||
import java.nio.file.Path;
|
||||
|
||||
private V result;
|
||||
public class ManuallyCreatedModpackException extends Exception {
|
||||
private final Path path;
|
||||
|
||||
public V getResult() {
|
||||
return result;
|
||||
public ManuallyCreatedModpackException(Path path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public void setResult(V result) {
|
||||
this.result = result;
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public abstract String getId();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,58 +13,129 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||
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.mcbbs.McbbsModpackManifest;
|
||||
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.server.ServerModpackManifest;
|
||||
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.FinalizedCallback;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
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.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
|
||||
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.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.toIterable;
|
||||
|
||||
public final class ModpackHelper {
|
||||
private ModpackHelper() {}
|
||||
|
||||
public static Modpack readModpackManifest(File file) throws UnsupportedModpackException {
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(file);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid CurseForge modpack.
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
||||
try (ZipFileSystem zfs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(zfs, charset);
|
||||
} catch (Exception ignored) {
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid CurseForge modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return HMCLModpackManager.readHMCLModpackManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid HMCL modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(zfs, file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid MultiMC modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return ServerModpackManifest.readManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid Server modpack.
|
||||
}
|
||||
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
return HMCLModpackManager.readHMCLModpackManifest(file);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid HMCL modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid MultiMC 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 <T> ModpackConfiguration<T> readModpackConfiguration(File file) throws IOException {
|
||||
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 Constants.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<T>>() {
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
|
||||
}.getType());
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed modpack configuration");
|
||||
@@ -82,49 +153,119 @@ public final class ModpackHelper {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
|
||||
public static Task getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
|
||||
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
FinalizedCallback finalizeTask = (variables, isDependentsSucceeded) -> {
|
||||
profile.getRepository().refreshVersions();
|
||||
VersionSetting vs = profile.specializeVersionSetting(name);
|
||||
profile.getRepository().undoMark(name);
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
repository.refreshVersions();
|
||||
VersionSetting vs = repository.specializeVersionSetting(name);
|
||||
repository.undoMark(name);
|
||||
if (vs != null)
|
||||
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
|
||||
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
|
||||
};
|
||||
|
||||
if (modpack.getManifest() instanceof CurseManifest)
|
||||
return new CurseInstallTask(profile.getDependency(), zipFile, ((CurseManifest) modpack.getManifest()), name)
|
||||
.finalized(finalizeTask);
|
||||
else if (modpack.getManifest() instanceof HMCLModpackManifest)
|
||||
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
|
||||
.finalized(finalizeTask);
|
||||
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
|
||||
.finalized(finalizeTask)
|
||||
.with(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
|
||||
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
|
||||
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
||||
if (ex instanceof CurseCompletionException && !(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)
|
||||
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
public static Task getUpdateTask(Profile profile, File zipFile, String name, ModpackConfiguration configuration) throws UnsupportedModpackException, MismatchedModpackTypeException, IOException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile);
|
||||
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 = () -> {
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
repository.refreshVersions();
|
||||
VersionSetting vs = repository.specializeVersionSetting(name);
|
||||
repository.undoMark(name);
|
||||
if (vs != null)
|
||||
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
|
||||
};
|
||||
|
||||
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
||||
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
|
||||
success.run();
|
||||
// This is tolerable and we will not delete the game
|
||||
}
|
||||
};
|
||||
|
||||
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 McbbsModpackManifest)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
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))
|
||||
.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, 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 CurseInstallTask(profile.getDependency(), zipFile, (CurseManifest) modpack.getManifest(), name);
|
||||
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 MultiMCModpackInstallTask(profile.getDependency(), zipFile, (MultiMCInstanceConfiguration) modpack.getManifest(), name);
|
||||
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 HMCLModpackInstallTask(profile, zipFile, modpack, name);
|
||||
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();
|
||||
}
|
||||
@@ -132,7 +273,7 @@ public final class ModpackHelper {
|
||||
|
||||
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||
vs.setUsesGlobal(false);
|
||||
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
|
||||
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
|
||||
|
||||
if (c.isOverrideJavaLocation()) {
|
||||
vs.setJavaDir(Lang.nonNull(c.getJavaPath(), ""));
|
||||
@@ -167,5 +308,18 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
ModpackHelper.toVersionSetting(manifest, vs);
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<Void> createMcbbsPostInstallTask(Profile profile, McbbsModpackManifest manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
if (manifest.getLaunchInfo().getMinMemory() > vs.getMaxMemory())
|
||||
vs.setMaxMemory(manifest.getLaunchInfo().getMinMemory());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.mod.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Scheduler;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class MultiMCInstallVersionSettingTask extends Task {
|
||||
private final Profile profile;
|
||||
private final MultiMCInstanceConfiguration manifest;
|
||||
private final String version;
|
||||
|
||||
public MultiMCInstallVersionSettingTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
this.profile = profile;
|
||||
this.manifest = manifest;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scheduler getScheduler() {
|
||||
return Schedulers.javafx();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.specializeVersionSetting(version));
|
||||
ModpackHelper.toVersionSetting(manifest, vs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 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.Logging;
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
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) {
|
||||
Logging.LOG.log(Level.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 {
|
||||
Logging.LOG.warning("Error: " + parameters);
|
||||
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
|
||||
}
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||
}
|
||||
thread(() -> {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
stop();
|
||||
} catch (InterruptedException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "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.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Id")).orElse(""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse(""));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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 javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.scene.image.Image;
|
||||
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.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
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.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public final class TexturesLoader {
|
||||
|
||||
private TexturesLoader() {
|
||||
}
|
||||
|
||||
// ==== Texture Loading ====
|
||||
public static class LoadedTexture {
|
||||
private final BufferedImage image;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
|
||||
this.image = requireNonNull(image);
|
||||
this.metadata = requireNonNull(metadata);
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS);
|
||||
private static final Path TEXTURES_DIR = Metadata.MINECRAFT_DIRECTORY.resolve("assets").resolve("skins");
|
||||
|
||||
private static Path getTexturePath(Texture texture) {
|
||||
String url = texture.getUrl();
|
||||
int slash = url.lastIndexOf('/');
|
||||
int dot = url.lastIndexOf('.');
|
||||
if (dot < slash) {
|
||||
dot = url.length();
|
||||
}
|
||||
String hash = url.substring(slash + 1, dot);
|
||||
String prefix = hash.length() > 2 ? hash.substring(0, 2) : "xx";
|
||||
return TEXTURES_DIR.resolve(prefix).resolve(hash);
|
||||
}
|
||||
|
||||
public static LoadedTexture loadTexture(Texture texture) throws IOException {
|
||||
if (StringUtils.isBlank(texture.getUrl())) {
|
||||
throw new IOException("Texture url is empty");
|
||||
}
|
||||
|
||||
Path file = getTexturePath(texture);
|
||||
if (!Files.isRegularFile(file)) {
|
||||
// download it
|
||||
try {
|
||||
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
|
||||
LOG.info("Texture downloaded: " + texture.getUrl());
|
||||
} 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);
|
||||
} else {
|
||||
throw new IOException("Failed to download texture " + texture.getUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BufferedImage img;
|
||||
try (InputStream in = Files.newInputStream(file)) {
|
||||
img = ImageIO.read(in);
|
||||
}
|
||||
if (img == null)
|
||||
throw new IOException("Texture is malformed");
|
||||
|
||||
Map<String, String> metadata = texture.getMetadata();
|
||||
if (metadata == null) {
|
||||
metadata = emptyMap();
|
||||
}
|
||||
return new LoadedTexture(img, metadata);
|
||||
}
|
||||
// ====
|
||||
|
||||
// ==== Skins ====
|
||||
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
|
||||
|
||||
static {
|
||||
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
|
||||
}
|
||||
|
||||
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(TextureModel model) {
|
||||
return DEFAULT_SKINS.get(model);
|
||||
}
|
||||
|
||||
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(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);
|
||||
return Optional.empty();
|
||||
}
|
||||
})
|
||||
.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;
|
||||
}
|
||||
}, POOL);
|
||||
} else {
|
||||
return CompletableFuture.completedFuture(uuidFallback);
|
||||
}
|
||||
}, 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;
|
||||
}
|
||||
}, POOL);
|
||||
} else {
|
||||
return CompletableFuture.completedFuture(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();
|
||||
|
||||
int scale = 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;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
|
||||
return BindingMapping.of(skinBinding(service, uuid))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(FXUtils::toFXImage);
|
||||
}
|
||||
|
||||
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(FXUtils::toFXImage);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
|
||||
}
|
||||
}
|
||||
// ====
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,21 +13,48 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.OfflineAccountFactory;
|
||||
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.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
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.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import java.io.IOException;
|
||||
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 static java.util.stream.Collectors.toList;
|
||||
import static javafx.collections.FXCollections.observableArrayList;
|
||||
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.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -35,44 +62,326 @@ import java.util.Optional;
|
||||
public final class Accounts {
|
||||
private Accounts() {}
|
||||
|
||||
public static final String OFFLINE_ACCOUNT_KEY = "offline";
|
||||
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
|
||||
|
||||
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = Lang.mapOf(
|
||||
new Pair<>(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
|
||||
new Pair<>(YGGDRASIL_ACCOUNT_KEY, YggdrasilAccountFactory.INSTANCE)
|
||||
);
|
||||
|
||||
public static String getAccountType(Account account) {
|
||||
if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY;
|
||||
else if (account instanceof YggdrasilAccount) return YGGDRASIL_ACCOUNT_KEY;
|
||||
else return YGGDRASIL_ACCOUNT_KEY;
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void setCurrentCharacter(Account account, String character) {
|
||||
account.getProperties().put("character", character);
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, 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);
|
||||
|
||||
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
|
||||
}
|
||||
|
||||
public static boolean hasCurrentCharacter(Account account) {
|
||||
return Lang.get(account.getProperties(), "character", String.class, null) != null;
|
||||
public static String getLoginType(AccountFactory<?> factory) {
|
||||
return Optional.ofNullable(factory2type.get(factory))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory"));
|
||||
}
|
||||
|
||||
public static String getCurrentCharacter(Account account) {
|
||||
return Lang.get(account.getProperties(), "character", String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Account " + account + " has not set current character."));
|
||||
public static AccountFactory<?> getAccountFactory(String loginType) {
|
||||
return Optional.ofNullable(type2factory.get(loginType))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unrecognized login type"));
|
||||
}
|
||||
// ====
|
||||
|
||||
public static AccountFactory<?> getAccountFactory(Account account) {
|
||||
if (account instanceof OfflineAccount)
|
||||
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);
|
||||
}
|
||||
|
||||
public static Optional<String> getCurrentCharacter(Map<Object, Object> storage) {
|
||||
Optional<Map> properties = Lang.get(storage, "properties", Map.class);
|
||||
if (!properties.isPresent()) return Optional.empty();
|
||||
return Lang.get(properties.get(), "character", String.class);
|
||||
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
|
||||
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* True if {@link #init()} hasn't been called.
|
||||
*/
|
||||
private static boolean initialized = false;
|
||||
|
||||
static {
|
||||
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
||||
}
|
||||
|
||||
static String getAccountId(Account account) {
|
||||
return getAccountId(account.getUsername(), getCurrentCharacter(account));
|
||||
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;
|
||||
}
|
||||
|
||||
static String getAccountId(String username, String character) {
|
||||
return username + ":" + character;
|
||||
private static void updateAccountStorages() {
|
||||
// don't update the underlying storage before data loading is completed
|
||||
// otherwise it might cause data loss
|
||||
if (!initialized)
|
||||
return;
|
||||
// update storage
|
||||
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when it's ready to load accounts from {@link ConfigHolder#config()}.
|
||||
*/
|
||||
static void init() {
|
||||
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 (Boolean.TRUE.equals(storage.get("selected"))) {
|
||||
selectedAccount.set(account);
|
||||
}
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
|
||||
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
|
||||
|
||||
Account selected = selectedAccount.get();
|
||||
if (selected != null) {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
selected.logIn();
|
||||
} catch (AuthenticationException e) {
|
||||
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!config().getAuthlibInjectorServers().isEmpty()) {
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
}
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
continue;
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
server.fetchMetadataResponse();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + server, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static ObservableList<Account> getAccounts() {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
public static ReadOnlyListProperty<Account> accountsProperty() {
|
||||
return accountsWrapper.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public static Account getSelectedAccount() {
|
||||
return selectedAccount.get();
|
||||
}
|
||||
|
||||
public static void setSelectedAccount(Account selectedAccount) {
|
||||
Accounts.selectedAccount.set(selectedAccount);
|
||||
}
|
||||
|
||||
public static ObjectProperty<Account> selectedAccountProperty() {
|
||||
return selectedAccount;
|
||||
}
|
||||
|
||||
// ==== authlib-injector ====
|
||||
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
|
||||
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
|
||||
if (authlibinjectorLocation == null) {
|
||||
return new AuthlibInjectorDownloader(
|
||||
Metadata.HMCL_DIRECTORY.resolve("authlib-injector.jar"),
|
||||
DownloadProviders::getDownloadProvider) {
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
|
||||
if (local.isPresent()) {
|
||||
return local;
|
||||
}
|
||||
// search authlib-injector.jar in current directory, it's used as a fallback
|
||||
return parseArtifact(Paths.get("authlib-injector.jar"));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
|
||||
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
|
||||
}
|
||||
}
|
||||
|
||||
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
|
||||
return config().getAuthlibInjectorServers().stream()
|
||||
.filter(server -> url.equals(server.getUrl()))
|
||||
.findFirst()
|
||||
.orElseGet(() -> {
|
||||
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
|
||||
config().getAuthlibInjectorServers().add(server);
|
||||
return server;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
|
||||
* This method performs a check and removes the dangling accounts.
|
||||
*/
|
||||
private static void removeDanglingAuthlibInjectorAccounts() {
|
||||
accounts.stream()
|
||||
.filter(AuthlibInjectorAccount.class::isInstance)
|
||||
.map(AuthlibInjectorAccount.class::cast)
|
||||
.filter(it -> !config().getAuthlibInjectorServers().contains(it.getServer()))
|
||||
.collect(toList())
|
||||
.forEach(accounts::remove);
|
||||
}
|
||||
// ====
|
||||
|
||||
// ==== Login type name i18n ===
|
||||
private static 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"));
|
||||
|
||||
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
|
||||
return i18n(Optional.ofNullable(unlocalizedLoginTypeNames.get(factory))
|
||||
.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) {
|
||||
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.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.setting;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
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.Validation;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
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 static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class AuthlibInjectorServers implements Validation {
|
||||
|
||||
public static final String CONFIG_FILENAME = "authlib-injectors.json";
|
||||
|
||||
private final List<String> urls;
|
||||
|
||||
public 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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
configInstance = new AuthlibInjectorServers(Collections.emptyList());
|
||||
|
||||
if (Files.exists(configLocation)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,245 +13,570 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.util.JavaVersion;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.hildan.fxgson.creators.ObservableListCreator;
|
||||
import org.hildan.fxgson.creators.ObservableMapCreator;
|
||||
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.util.gson.EnumOrdinalDeserializer;
|
||||
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.io.File;
|
||||
import java.net.Proxy;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public final class Config {
|
||||
public final class Config implements Cloneable, Observable {
|
||||
|
||||
public static final int CURRENT_UI_VERSION = 0;
|
||||
|
||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
||||
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
|
||||
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
|
||||
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
|
||||
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
@Nullable
|
||||
public static Config fromJson(String json) throws JsonParseException {
|
||||
Config loaded = CONFIG_GSON.fromJson(json, Config.class);
|
||||
if (loaded == null) {
|
||||
return null;
|
||||
}
|
||||
Config instance = new Config();
|
||||
PropertyUtils.copyProperties(loaded, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@SerializedName("last")
|
||||
private String selectedProfile = "";
|
||||
private StringProperty selectedProfile = new SimpleStringProperty("");
|
||||
|
||||
@SerializedName("backgroundType")
|
||||
private int backgroundImageType = 0;
|
||||
private ObjectProperty<EnumBackgroundImage> backgroundImageType = new SimpleObjectProperty<>(EnumBackgroundImage.DEFAULT);
|
||||
|
||||
@SerializedName("bgpath")
|
||||
private String backgroundImage = null;
|
||||
private StringProperty backgroundImage = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("bgurl")
|
||||
private StringProperty backgroundImageUrl = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("commonDirType")
|
||||
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
|
||||
|
||||
@SerializedName("commonpath")
|
||||
private String commonDirectory = Main.MINECRAFT_DIRECTORY.getAbsolutePath();
|
||||
private StringProperty commonDirectory = new SimpleStringProperty(Metadata.MINECRAFT_DIRECTORY.toString());
|
||||
|
||||
@SerializedName("hasProxy")
|
||||
private BooleanProperty hasProxy = new SimpleBooleanProperty();
|
||||
|
||||
@SerializedName("hasProxyAuth")
|
||||
private BooleanProperty hasProxyAuth = new SimpleBooleanProperty();
|
||||
|
||||
@SerializedName("proxyType")
|
||||
private int proxyType = 0;
|
||||
private ObjectProperty<Proxy.Type> proxyType = new SimpleObjectProperty<>(Proxy.Type.HTTP);
|
||||
|
||||
@SerializedName("proxyHost")
|
||||
private String proxyHost = null;
|
||||
private StringProperty proxyHost = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("proxyPort")
|
||||
private String proxyPort = null;
|
||||
private IntegerProperty proxyPort = new SimpleIntegerProperty();
|
||||
|
||||
@SerializedName("proxyUserName")
|
||||
private String proxyUser = null;
|
||||
private StringProperty proxyUser = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("proxyPassword")
|
||||
private String proxyPass = null;
|
||||
private StringProperty proxyPass = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("width")
|
||||
private DoubleProperty width = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("height")
|
||||
private DoubleProperty height = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("theme")
|
||||
private String theme = null;
|
||||
|
||||
@SerializedName("java")
|
||||
private List<JavaVersion> java = null;
|
||||
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
|
||||
|
||||
@SerializedName("localization")
|
||||
private String localization;
|
||||
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
|
||||
|
||||
@SerializedName("downloadtype")
|
||||
private int downloadType = 0;
|
||||
@SerializedName("autoDownloadThreads")
|
||||
private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(false);
|
||||
|
||||
@SerializedName("downloadThreads")
|
||||
private IntegerProperty downloadThreads = new SimpleIntegerProperty(64);
|
||||
|
||||
@SerializedName("downloadType")
|
||||
private StringProperty downloadType = new SimpleStringProperty("mcbbs");
|
||||
|
||||
@SerializedName("autoChooseDownloadType")
|
||||
private BooleanProperty autoChooseDownloadType = new SimpleBooleanProperty(true);
|
||||
|
||||
@SerializedName("versionListSource")
|
||||
private StringProperty versionListSource = new SimpleStringProperty("balanced");
|
||||
|
||||
@SerializedName("configurations")
|
||||
private Map<String, Profile> configurations = new TreeMap<>();
|
||||
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
|
||||
|
||||
@SerializedName("accounts")
|
||||
private List<Map<Object, Object>> accounts = new LinkedList<>();
|
||||
|
||||
@SerializedName("selectedAccount")
|
||||
private String selectedAccount = "";
|
||||
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
||||
|
||||
@SerializedName("fontFamily")
|
||||
private String fontFamily = "Consolas";
|
||||
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
|
||||
|
||||
@SerializedName("fontSize")
|
||||
private double fontSize = 12;
|
||||
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
|
||||
|
||||
@SerializedName("launcherFontFamily")
|
||||
private StringProperty launcherFontFamily = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("logLines")
|
||||
private int logLines = 100;
|
||||
private IntegerProperty logLines = new SimpleIntegerProperty(100);
|
||||
|
||||
@SerializedName("titleTransparent")
|
||||
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
|
||||
|
||||
@SerializedName("authlibInjectorServers")
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
|
||||
|
||||
@SerializedName("_version")
|
||||
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
/**
|
||||
* The version of UI that the user have last used.
|
||||
* If there is a major change in UI, {@link Config#CURRENT_UI_VERSION} should be increased.
|
||||
* When {@link #CURRENT_UI_VERSION} is higher than the property, the user guide should be shown,
|
||||
* then this property is set to the same value as {@link #CURRENT_UI_VERSION}.
|
||||
* In particular, the property is default to 0, so that whoever open the application for the first time will see the guide.
|
||||
*/
|
||||
@SerializedName("uiVersion")
|
||||
private IntegerProperty uiVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
/**
|
||||
* The preferred login type to use when the user wants to add an account.
|
||||
*/
|
||||
@SerializedName("preferredLoginType")
|
||||
private StringProperty preferredLoginType = new SimpleStringProperty();
|
||||
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public Config() {
|
||||
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_GSON.toJson(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config clone() {
|
||||
return fromJson(this.toJson());
|
||||
}
|
||||
|
||||
// Getters & Setters & Properties
|
||||
public String getSelectedProfile() {
|
||||
return selectedProfile;
|
||||
return selectedProfile.get();
|
||||
}
|
||||
|
||||
public void setSelectedProfile(String selectedProfile) {
|
||||
this.selectedProfile = selectedProfile;
|
||||
Settings.INSTANCE.save();
|
||||
this.selectedProfile.set(selectedProfile);
|
||||
}
|
||||
|
||||
public String getBackgroundImage() {
|
||||
return backgroundImage;
|
||||
public StringProperty selectedProfileProperty() {
|
||||
return selectedProfile;
|
||||
}
|
||||
|
||||
public void setBackgroundImage(String backgroundImage) {
|
||||
this.backgroundImage = backgroundImage;
|
||||
Settings.INSTANCE.save();
|
||||
public EnumBackgroundImage getBackgroundImageType() {
|
||||
return backgroundImageType.get();
|
||||
}
|
||||
|
||||
public int getBackgroundImageType() {
|
||||
public void setBackgroundImageType(EnumBackgroundImage backgroundImageType) {
|
||||
this.backgroundImageType.set(backgroundImageType);
|
||||
}
|
||||
|
||||
public ObjectProperty<EnumBackgroundImage> backgroundImageTypeProperty() {
|
||||
return backgroundImageType;
|
||||
}
|
||||
|
||||
public void setBackgroundImageType(int backgroundImageType) {
|
||||
this.backgroundImageType = backgroundImageType;
|
||||
Settings.INSTANCE.save();
|
||||
public String getBackgroundImage() {
|
||||
return backgroundImage.get();
|
||||
}
|
||||
|
||||
public void setBackgroundImage(String backgroundImage) {
|
||||
this.backgroundImage.set(backgroundImage);
|
||||
}
|
||||
|
||||
public StringProperty backgroundImageProperty() {
|
||||
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();
|
||||
}
|
||||
|
||||
public ObjectProperty<EnumCommonDirectory> commonDirTypeProperty() {
|
||||
return commonDirType;
|
||||
}
|
||||
|
||||
public void setCommonDirType(EnumCommonDirectory commonDirType) {
|
||||
this.commonDirType.set(commonDirType);
|
||||
}
|
||||
|
||||
public String getCommonDirectory() {
|
||||
return commonDirectory;
|
||||
return commonDirectory.get();
|
||||
}
|
||||
|
||||
public void setCommonDirectory(String commonDirectory) {
|
||||
this.commonDirectory = commonDirectory;
|
||||
Settings.INSTANCE.save();
|
||||
this.commonDirectory.set(commonDirectory);
|
||||
}
|
||||
|
||||
public int getProxyType() {
|
||||
public StringProperty commonDirectoryProperty() {
|
||||
return commonDirectory;
|
||||
}
|
||||
|
||||
public boolean hasProxy() {
|
||||
return hasProxy.get();
|
||||
}
|
||||
|
||||
public void setHasProxy(boolean hasProxy) {
|
||||
this.hasProxy.set(hasProxy);
|
||||
}
|
||||
|
||||
public BooleanProperty hasProxyProperty() {
|
||||
return hasProxy;
|
||||
}
|
||||
|
||||
public boolean hasProxyAuth() {
|
||||
return hasProxyAuth.get();
|
||||
}
|
||||
|
||||
public void setHasProxyAuth(boolean hasProxyAuth) {
|
||||
this.hasProxyAuth.set(hasProxyAuth);
|
||||
}
|
||||
|
||||
public BooleanProperty hasProxyAuthProperty() {
|
||||
return hasProxyAuth;
|
||||
}
|
||||
|
||||
public Proxy.Type getProxyType() {
|
||||
return proxyType.get();
|
||||
}
|
||||
|
||||
public void setProxyType(Proxy.Type proxyType) {
|
||||
this.proxyType.set(proxyType);
|
||||
}
|
||||
|
||||
public ObjectProperty<Proxy.Type> proxyTypeProperty() {
|
||||
return proxyType;
|
||||
}
|
||||
|
||||
public void setProxyType(int proxyType) {
|
||||
this.proxyType = proxyType;
|
||||
Settings.INSTANCE.save();
|
||||
}
|
||||
|
||||
public String getProxyHost() {
|
||||
return proxyHost;
|
||||
return proxyHost.get();
|
||||
}
|
||||
|
||||
public void setProxyHost(String proxyHost) {
|
||||
this.proxyHost = proxyHost;
|
||||
Settings.INSTANCE.save();
|
||||
this.proxyHost.set(proxyHost);
|
||||
}
|
||||
|
||||
public String getProxyPort() {
|
||||
public StringProperty proxyHostProperty() {
|
||||
return proxyHost;
|
||||
}
|
||||
|
||||
public int getProxyPort() {
|
||||
return proxyPort.get();
|
||||
}
|
||||
|
||||
public void setProxyPort(int proxyPort) {
|
||||
this.proxyPort.set(proxyPort);
|
||||
}
|
||||
|
||||
public IntegerProperty proxyPortProperty() {
|
||||
return proxyPort;
|
||||
}
|
||||
|
||||
public void setProxyPort(String proxyPort) {
|
||||
this.proxyPort = proxyPort;
|
||||
Settings.INSTANCE.save();
|
||||
}
|
||||
|
||||
public String getProxyUser() {
|
||||
return proxyUser;
|
||||
return proxyUser.get();
|
||||
}
|
||||
|
||||
public void setProxyUser(String proxyUser) {
|
||||
this.proxyUser = proxyUser;
|
||||
Settings.INSTANCE.save();
|
||||
this.proxyUser.set(proxyUser);
|
||||
}
|
||||
|
||||
public StringProperty proxyUserProperty() {
|
||||
return proxyUser;
|
||||
}
|
||||
|
||||
public String getProxyPass() {
|
||||
return proxyPass;
|
||||
return proxyPass.get();
|
||||
}
|
||||
|
||||
public void setProxyPass(String proxyPass) {
|
||||
this.proxyPass = proxyPass;
|
||||
Settings.INSTANCE.save();
|
||||
this.proxyPass.set(proxyPass);
|
||||
}
|
||||
|
||||
public String getTheme() {
|
||||
public StringProperty proxyPassProperty() {
|
||||
return proxyPass;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width.get();
|
||||
}
|
||||
|
||||
public DoubleProperty widthProperty() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(double width) {
|
||||
this.width.set(width);
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height.get();
|
||||
}
|
||||
|
||||
public DoubleProperty heightProperty() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(double height) {
|
||||
this.height.set(height);
|
||||
}
|
||||
|
||||
public Theme getTheme() {
|
||||
return theme.get();
|
||||
}
|
||||
|
||||
public void setTheme(Theme theme) {
|
||||
this.theme.set(theme);
|
||||
}
|
||||
|
||||
public ObjectProperty<Theme> themeProperty() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
public void setTheme(String theme) {
|
||||
this.theme = theme;
|
||||
Settings.INSTANCE.save();
|
||||
public SupportedLocale getLocalization() {
|
||||
return localization.get();
|
||||
}
|
||||
|
||||
public List<JavaVersion> getJava() {
|
||||
return java;
|
||||
public void setLocalization(SupportedLocale localization) {
|
||||
this.localization.set(localization);
|
||||
}
|
||||
|
||||
public void setJava(List<JavaVersion> java) {
|
||||
this.java = java;
|
||||
Settings.INSTANCE.save();
|
||||
}
|
||||
|
||||
public String getLocalization() {
|
||||
public ObjectProperty<SupportedLocale> localizationProperty() {
|
||||
return localization;
|
||||
}
|
||||
|
||||
public void setLocalization(String localization) {
|
||||
this.localization = localization;
|
||||
Settings.INSTANCE.save();
|
||||
public boolean getAutoDownloadThreads() {
|
||||
return autoDownloadThreads.get();
|
||||
}
|
||||
|
||||
public int getDownloadType() {
|
||||
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();
|
||||
}
|
||||
|
||||
public void setDownloadType(String downloadType) {
|
||||
this.downloadType.set(downloadType);
|
||||
}
|
||||
|
||||
public StringProperty downloadTypeProperty() {
|
||||
return downloadType;
|
||||
}
|
||||
|
||||
public void setDownloadType(int downloadType) {
|
||||
this.downloadType = downloadType;
|
||||
Settings.INSTANCE.save();
|
||||
public boolean isAutoChooseDownloadType() {
|
||||
return autoChooseDownloadType.get();
|
||||
}
|
||||
|
||||
public Map<String, Profile> getConfigurations() {
|
||||
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 ObservableMap<String, Profile> getConfigurations() {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
public void setConfigurations(Map<String, Profile> configurations) {
|
||||
this.configurations = configurations;
|
||||
Settings.INSTANCE.save();
|
||||
}
|
||||
|
||||
public List<Map<Object, Object>> getAccounts() {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
public void setAccounts(List<Map<Object, Object>> accounts) {
|
||||
this.accounts = accounts;
|
||||
Settings.INSTANCE.save();
|
||||
}
|
||||
|
||||
public String getSelectedAccount() {
|
||||
return selectedAccount;
|
||||
}
|
||||
|
||||
public void setSelectedAccount(String selectedAccount) {
|
||||
this.selectedAccount = selectedAccount;
|
||||
Settings.INSTANCE.save();
|
||||
public ObservableList<Map<Object, Object>> getAccountStorages() {
|
||||
return accountStorages;
|
||||
}
|
||||
|
||||
public String getFontFamily() {
|
||||
return fontFamily;
|
||||
return fontFamily.get();
|
||||
}
|
||||
|
||||
public void setFontFamily(String fontFamily) {
|
||||
this.fontFamily = fontFamily;
|
||||
Settings.INSTANCE.save();
|
||||
this.fontFamily.set(fontFamily);
|
||||
}
|
||||
|
||||
public StringProperty fontFamilyProperty() {
|
||||
return fontFamily;
|
||||
}
|
||||
|
||||
public double getFontSize() {
|
||||
return fontSize;
|
||||
return fontSize.get();
|
||||
}
|
||||
|
||||
public void setFontSize(double fontSize) {
|
||||
this.fontSize = fontSize;
|
||||
Settings.INSTANCE.save();
|
||||
this.fontSize.set(fontSize);
|
||||
}
|
||||
|
||||
public DoubleProperty fontSizeProperty() {
|
||||
return fontSize;
|
||||
}
|
||||
|
||||
public String getLauncherFontFamily() {
|
||||
return launcherFontFamily.get();
|
||||
}
|
||||
|
||||
public StringProperty launcherFontFamilyProperty() {
|
||||
return launcherFontFamily;
|
||||
}
|
||||
|
||||
public void setLauncherFontFamily(String launcherFontFamily) {
|
||||
this.launcherFontFamily.set(launcherFontFamily);
|
||||
}
|
||||
|
||||
public int getLogLines() {
|
||||
return logLines;
|
||||
return logLines.get();
|
||||
}
|
||||
|
||||
public void setLogLines(int logLines) {
|
||||
this.logLines = logLines;
|
||||
this.logLines.set(logLines);
|
||||
}
|
||||
|
||||
public IntegerProperty logLinesProperty() {
|
||||
return logLines;
|
||||
}
|
||||
|
||||
public ObservableList<AuthlibInjectorServer> getAuthlibInjectorServers() {
|
||||
return authlibInjectorServers;
|
||||
}
|
||||
|
||||
public int getConfigVersion() {
|
||||
return configVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty configVersionProperty() {
|
||||
return configVersion;
|
||||
}
|
||||
|
||||
public void setConfigVersion(int configVersion) {
|
||||
this.configVersion.set(configVersion);
|
||||
}
|
||||
|
||||
public int getUiVersion() {
|
||||
return uiVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty uiVersionProperty() {
|
||||
return uiVersion;
|
||||
}
|
||||
|
||||
public void setUiVersion(int uiVersion) {
|
||||
this.uiVersion.set(uiVersion);
|
||||
}
|
||||
|
||||
public String getPreferredLoginType() {
|
||||
return preferredLoginType.get();
|
||||
}
|
||||
|
||||
public void setPreferredLoginType(String preferredLoginType) {
|
||||
this.preferredLoginType.set(preferredLoginType);
|
||||
}
|
||||
|
||||
public StringProperty preferredLoginTypeProperty() {
|
||||
return preferredLoginType;
|
||||
}
|
||||
|
||||
public boolean isTitleTransparent() {
|
||||
return titleTransparent.get();
|
||||
}
|
||||
|
||||
public BooleanProperty titleTransparentProperty() {
|
||||
return titleTransparent;
|
||||
}
|
||||
|
||||
public void setTitleTransparent(boolean titleTransparent) {
|
||||
this.titleTransparent.set(titleTransparent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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.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.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public final class 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;
|
||||
|
||||
public static Config config() {
|
||||
if (configInstance == null) {
|
||||
throw new IllegalStateException("Configuration hasn't been loaded");
|
||||
}
|
||||
return configInstance;
|
||||
}
|
||||
|
||||
public static GlobalConfig globalConfig() {
|
||||
if (globalConfigInstance == null) {
|
||||
throw new IllegalStateException("Configuration hasn't been loaded");
|
||||
}
|
||||
return globalConfigInstance;
|
||||
}
|
||||
|
||||
public static boolean isNewlyCreated() {
|
||||
return newlyCreated;
|
||||
}
|
||||
|
||||
public synchronized static void init() throws IOException {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("Configuration is already loaded");
|
||||
}
|
||||
|
||||
configLocation = locateConfig();
|
||||
|
||||
LOG.log(Level.INFO, "Config location: " + configLocation);
|
||||
|
||||
configInstance = loadConfig();
|
||||
configInstance.addListener(source -> markConfigDirty());
|
||||
|
||||
globalConfigInstance = loadGlobalConfig();
|
||||
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
|
||||
|
||||
Settings.init();
|
||||
|
||||
if (newlyCreated) {
|
||||
saveConfigSync();
|
||||
|
||||
// hide the config file on windows
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
Files.setAttribute(configLocation, "dos:hidden", true);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to set hidden attribute of " + configLocation, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Files.isWritable(configLocation)) {
|
||||
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("");
|
||||
try {
|
||||
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
|
||||
.toURI()).toAbsolutePath();
|
||||
if (Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
|
||||
jarPath = jarPath.getParent();
|
||||
exePath = jarPath;
|
||||
|
||||
Path config = jarPath.resolve(CONFIG_FILENAME);
|
||||
if (Files.isRegularFile(config))
|
||||
return config;
|
||||
|
||||
Path dotConfig = jarPath.resolve(CONFIG_FILENAME_LINUX);
|
||||
if (Files.isRegularFile(dotConfig))
|
||||
return dotConfig;
|
||||
}
|
||||
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
|
||||
Path config = Paths.get(CONFIG_FILENAME);
|
||||
if (Files.isRegularFile(config))
|
||||
return config;
|
||||
|
||||
Path dotConfig = Paths.get(CONFIG_FILENAME_LINUX);
|
||||
if (Files.isRegularFile(dotConfig))
|
||||
return dotConfig;
|
||||
|
||||
// create new
|
||||
return exePath.resolve(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? CONFIG_FILENAME : CONFIG_FILENAME_LINUX);
|
||||
}
|
||||
|
||||
private static Config loadConfig() throws IOException {
|
||||
if (Files.exists(configLocation)) {
|
||||
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);
|
||||
return deserialized;
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Malformed config.", e);
|
||||
}
|
||||
}
|
||||
|
||||
LOG.info("Creating an empty config");
|
||||
newlyCreated = true;
|
||||
return new Config();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
private static void writeToConfig(String content) throws IOException {
|
||||
LOG.info("Saving config");
|
||||
synchronized (configLocation) {
|
||||
Files.write(configLocation, content.getBytes(UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
static void markConfigDirty() {
|
||||
configWriter.accept(configInstance.toJson());
|
||||
}
|
||||
|
||||
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.log(Level.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.log(Level.SEVERE, "Failed to save config", e);
|
||||
}
|
||||
});
|
||||
|
||||
private static void writeToGlobalConfig(String content) throws IOException {
|
||||
LOG.info("Saving global config");
|
||||
synchronized (GLOBAL_CONFIG_PATH) {
|
||||
Files.write(GLOBAL_CONFIG_PATH, content.getBytes(UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
static void markGlobalConfigDirty() {
|
||||
globalConfigWriter.accept(globalConfigInstance.toJson());
|
||||
}
|
||||
|
||||
private static void saveGlobalConfigSync() throws IOException {
|
||||
writeToConfig(globalConfigInstance.toJson());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.setting;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
final class ConfigUpgrader {
|
||||
private static final int VERSION = 0;
|
||||
|
||||
private ConfigUpgrader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
* @return true if config version is upgraded
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
upgradeV2(deserialized, rawJson);
|
||||
upgradeV3(deserialized, rawJson);
|
||||
|
||||
return upgraded;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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()));
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,24 +13,157 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.MojangDownloadProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
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 java.io.FileNotFoundException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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() {}
|
||||
|
||||
public static List<DownloadProvider> DOWNLOAD_PROVIDERS = Arrays.asList(MojangDownloadProvider.INSTANCE, BMCLAPIDownloadProvider.INSTANCE);
|
||||
private static DownloadProvider currentDownloadProvider;
|
||||
|
||||
public static DownloadProvider getDownloadProvider(int index) {
|
||||
return Lang.get(DOWNLOAD_PROVIDERS, index).orElse(MojangDownloadProvider.INSTANCE);
|
||||
public static final Map<String, DownloadProvider> providersById;
|
||||
public static final Map<String, DownloadProvider> rawProviders;
|
||||
private static final AdaptedDownloadProvider fileDownloadProvider = new AdaptedDownloadProvider();
|
||||
|
||||
private static final MojangDownloadProvider MOJANG;
|
||||
private static final BMCLAPIDownloadProvider BMCLAPI;
|
||||
private static final BMCLAPIDownloadProvider MCBBS;
|
||||
|
||||
public static final String DEFAULT_PROVIDER_ID = "balanced";
|
||||
public static final String DEFAULT_RAW_PROVIDER_ID = "mcbbs";
|
||||
|
||||
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);
|
||||
MCBBS = new BMCLAPIDownloadProvider("https://download.mcbbs.net");
|
||||
rawProviders = mapOf(
|
||||
pair("mojang", MOJANG),
|
||||
pair("bmclapi", BMCLAPI),
|
||||
pair("mcbbs", MCBBS)
|
||||
);
|
||||
|
||||
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
|
||||
fileProvider.setDownloadProviderCandidates(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
|
||||
BalancedDownloadProvider balanced = new BalancedDownloadProvider(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
|
||||
|
||||
providersById = mapOf(
|
||||
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
|
||||
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
|
||||
pair("mirror", new AutoDownloadProvider(MCBBS, fileProvider)));
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
FetchTask.setDownloadExecutorConcurrency(
|
||||
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
|
||||
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
|
||||
}
|
||||
|
||||
static void init() {
|
||||
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));
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
|
||||
if (!rawProviders.containsKey(downloadType)) {
|
||||
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
|
||||
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
|
||||
fileDownloadProvider.setDownloadProviderCandidates(
|
||||
Stream.concat(
|
||||
Stream.of(primary),
|
||||
rawProviders.values().stream().filter(x -> x != primary)
|
||||
).collect(Collectors.toList())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static String getPrimaryDownloadProviderId() {
|
||||
String downloadType = config().getDownloadType();
|
||||
if (providersById.containsKey(downloadType))
|
||||
return downloadType;
|
||||
else
|
||||
return DEFAULT_PROVIDER_ID;
|
||||
}
|
||||
|
||||
public static DownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
|
||||
return Optional.ofNullable(providersById.get(primaryId))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current primary preferred 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
* 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
|
||||
@@ -13,18 +13,14 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
public enum EnumBackgroundImage {
|
||||
DEFAULT,
|
||||
CUSTOM;
|
||||
|
||||
public static EnumBackgroundImage indexOf(int index) {
|
||||
if (index >= values().length || index < 0)
|
||||
return DEFAULT;
|
||||
else
|
||||
return values()[index];
|
||||
}
|
||||
CUSTOM,
|
||||
CLASSIC,
|
||||
NETWORK,
|
||||
TRANSLUCENT
|
||||
}
|
||||
+13
-12
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,16 +13,17 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class Fonts {
|
||||
private Fonts() {}
|
||||
|
||||
public static final List<String> FONTS = Font.getFamilies();
|
||||
public enum EnumCommonDirectory {
|
||||
/**
|
||||
* %appdata%/.minecraft or ~/.minecraft
|
||||
*/
|
||||
DEFAULT,
|
||||
/**
|
||||
* user customized common directory.
|
||||
*/
|
||||
CUSTOM
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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.*;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.hildan.fxgson.creators.ObservableListCreator;
|
||||
import org.hildan.fxgson.creators.ObservableMapCreator;
|
||||
import org.hildan.fxgson.creators.ObservableSetCreator;
|
||||
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
|
||||
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.Proxy;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(GlobalConfig.Serializer.class)
|
||||
public class GlobalConfig implements Cloneable, Observable {
|
||||
|
||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
||||
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
|
||||
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
|
||||
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
|
||||
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
@Nullable
|
||||
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
||||
GlobalConfig loaded = 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 IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private StringProperty multiplayerToken = new SimpleStringProperty();
|
||||
|
||||
private BooleanProperty multiplayerRelay = new SimpleBooleanProperty();
|
||||
|
||||
private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
private final Map<String, Object> unknownFields = new HashMap<>();
|
||||
|
||||
private 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_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 boolean isMultiplayerRelay() {
|
||||
return multiplayerRelay.get();
|
||||
}
|
||||
|
||||
public BooleanProperty multiplayerRelayProperty() {
|
||||
return multiplayerRelay;
|
||||
}
|
||||
|
||||
public void setMultiplayerRelay(boolean multiplayerRelay) {
|
||||
this.multiplayerRelay.set(multiplayerRelay);
|
||||
}
|
||||
|
||||
public int getMultiplayerAgreementVersion() {
|
||||
return multiplayerAgreementVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty multiplayerAgreementVersionProperty() {
|
||||
return multiplayerAgreementVersion;
|
||||
}
|
||||
|
||||
public void setMultiplayerAgreementVersion(int multiplayerAgreementVersion) {
|
||||
this.multiplayerAgreementVersion.set(multiplayerAgreementVersion);
|
||||
}
|
||||
|
||||
public String getMultiplayerToken() {
|
||||
return multiplayerToken.get();
|
||||
}
|
||||
|
||||
public StringProperty multiplayerTokenProperty() {
|
||||
return multiplayerToken;
|
||||
}
|
||||
|
||||
public void setMultiplayerToken(String multiplayerToken) {
|
||||
this.multiplayerToken.set(multiplayerToken);
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||
"agreementVersion",
|
||||
"multiplayerToken",
|
||||
"multiplayerAgreementVersion"
|
||||
));
|
||||
|
||||
@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("multiplayerToken", context.serialize(src.getMultiplayerToken()));
|
||||
jsonObject.add("multiplayerRelay", context.serialize(src.isMultiplayerRelay()));
|
||||
jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion()));
|
||||
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.setMultiplayerToken(Optional.ofNullable(obj.get("multiplayerToken")).map(JsonElement::getAsString).orElse(null));
|
||||
config.setMultiplayerRelay(Optional.ofNullable(obj.get("multiplayerRelay")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
config.setMultiplayerAgreementVersion(Optional.ofNullable(obj.get("multiplayerAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
|
||||
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,164 @@
|
||||
/*
|
||||
* 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.annotations.SerializedName;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
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.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public final class HMCLAccounts {
|
||||
|
||||
private static final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
|
||||
|
||||
private HMCLAccounts() {
|
||||
}
|
||||
|
||||
public static HMCLAccount getAccount() {
|
||||
return account.get();
|
||||
}
|
||||
|
||||
public static ObjectProperty<HMCLAccount> accountProperty() {
|
||||
return account;
|
||||
}
|
||||
|
||||
public static void setAccount(HMCLAccount account) {
|
||||
HMCLAccounts.account.set(account);
|
||||
}
|
||||
|
||||
public static Task<Void> login() {
|
||||
String nonce = UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
String scope = "openid offline_access";
|
||||
|
||||
return Task.supplyAsync(() -> {
|
||||
OAuth.Session session = Accounts.OAUTH_CALLBACK.startServer();
|
||||
Accounts.OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
|
||||
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
mapOf(
|
||||
pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()),
|
||||
pair("response_type", "id_token code"),
|
||||
pair("response_mode", "form_post"),
|
||||
pair("scope", scope),
|
||||
pair("redirect_uri", session.getRedirectURI()),
|
||||
pair("nonce", nonce)
|
||||
)));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
String responseText = HttpRequest.POST("https://login.microsoftonline.com/common/oauth2/v2.0/token")
|
||||
.form(mapOf(pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.OAUTH_CALLBACK.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", scope)))
|
||||
.getString();
|
||||
OAuth.AuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
OAuth.AuthorizationResponse.class);
|
||||
|
||||
HMCLAccountProfile profile = HttpRequest.GET("https://hmcl.huangyuhui.net/api/user")
|
||||
.header("Token-Type", response.tokenType)
|
||||
.header("Access-Token", response.accessToken)
|
||||
.header("Authorization-Provider", "microsoft")
|
||||
.authorization("Bearer", session.getIdToken())
|
||||
.getJson(HMCLAccountProfile.class);
|
||||
|
||||
return new HMCLAccount("microsoft", profile.nickname, profile.email, profile.role, session.getIdToken(), response.tokenType, response.accessToken, response.refreshToken);
|
||||
}).thenAcceptAsync(Schedulers.javafx(), account -> {
|
||||
setAccount(account);
|
||||
});
|
||||
}
|
||||
|
||||
public static class HMCLAccount implements HttpRequest.Authorization {
|
||||
private final String provider;
|
||||
private final String nickname;
|
||||
private final String email;
|
||||
private final String role;
|
||||
private final String idToken;
|
||||
private final String tokenType;
|
||||
private final String accessToken;
|
||||
private final String refreshToken;
|
||||
|
||||
public HMCLAccount(String provider, String nickname, String email, String role, String idToken, String tokenType, String accessToken, String refreshToken) {
|
||||
this.provider = provider;
|
||||
this.nickname = nickname;
|
||||
this.email = email;
|
||||
this.role = role;
|
||||
this.idToken = idToken;
|
||||
this.tokenType = tokenType;
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public String getIdToken() {
|
||||
return idToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTokenType() {
|
||||
return tokenType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
}
|
||||
|
||||
private static class HMCLAccountProfile {
|
||||
@SerializedName("ID")
|
||||
String id;
|
||||
@SerializedName("Provider")
|
||||
String provider;
|
||||
@SerializedName("Email")
|
||||
String email;
|
||||
@SerializedName("NickName")
|
||||
String nickname;
|
||||
@SerializedName("Role")
|
||||
String role;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,7 +13,7 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2013 huangyuhui
|
||||
* 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
|
||||
@@ -13,78 +13,108 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* 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.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.HMCLDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.ImmediateObjectProperty;
|
||||
import org.jackhuang.hmcl.util.ImmediateStringProperty;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class Profile {
|
||||
|
||||
@JsonAdapter(Profile.Serializer.class)
|
||||
public final class Profile implements Observable {
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
private final HMCLGameRepository repository;
|
||||
private final ModManager modManager;
|
||||
|
||||
private final ImmediateObjectProperty<File> gameDirProperty;
|
||||
private final StringProperty selectedVersion = new SimpleStringProperty();
|
||||
|
||||
public ImmediateObjectProperty<File> gameDirProperty() {
|
||||
return gameDirProperty;
|
||||
public StringProperty selectedVersionProperty() {
|
||||
return selectedVersion;
|
||||
}
|
||||
|
||||
public String getSelectedVersion() {
|
||||
return selectedVersion.get();
|
||||
}
|
||||
|
||||
public void setSelectedVersion(String selectedVersion) {
|
||||
this.selectedVersion.set(selectedVersion);
|
||||
}
|
||||
|
||||
private final ObjectProperty<File> gameDir;
|
||||
|
||||
public ObjectProperty<File> gameDirProperty() {
|
||||
return gameDir;
|
||||
}
|
||||
|
||||
public File getGameDir() {
|
||||
return gameDirProperty.get();
|
||||
return gameDir.get();
|
||||
}
|
||||
|
||||
public void setGameDir(File gameDir) {
|
||||
gameDirProperty.set(gameDir);
|
||||
this.gameDir.set(gameDir);
|
||||
}
|
||||
|
||||
private final ImmediateObjectProperty<VersionSetting> globalProperty = new ImmediateObjectProperty<>(this, "global", new VersionSetting());
|
||||
private final ReadOnlyObjectWrapper<VersionSetting> global = new ReadOnlyObjectWrapper<>(this, "global");
|
||||
|
||||
public ImmediateObjectProperty<VersionSetting> globalProperty() {
|
||||
return globalProperty;
|
||||
public ReadOnlyObjectProperty<VersionSetting> globalProperty() {
|
||||
return global.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public VersionSetting getGlobal() {
|
||||
return globalProperty.get();
|
||||
return global.get();
|
||||
}
|
||||
|
||||
public void setGlobal(VersionSetting global) {
|
||||
if (global == null)
|
||||
global = new VersionSetting();
|
||||
globalProperty.set(global);
|
||||
}
|
||||
private final SimpleStringProperty name;
|
||||
|
||||
private final ImmediateStringProperty nameProperty;
|
||||
|
||||
public ImmediateStringProperty nameProperty() {
|
||||
return nameProperty;
|
||||
public StringProperty nameProperty() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return nameProperty.get();
|
||||
return name.get();
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
nameProperty.set(name);
|
||||
this.name.set(name);
|
||||
}
|
||||
|
||||
public Profile() {
|
||||
this("Default");
|
||||
private BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
|
||||
|
||||
public BooleanProperty useRelativePathProperty() {
|
||||
return useRelativePath;
|
||||
}
|
||||
|
||||
public boolean isUseRelativePath() {
|
||||
return useRelativePath.get();
|
||||
}
|
||||
|
||||
public void setUseRelativePath(boolean useRelativePath) {
|
||||
this.useRelativePath.set(useRelativePath);
|
||||
}
|
||||
|
||||
public Profile(String name) {
|
||||
@@ -92,73 +122,111 @@ public final class Profile {
|
||||
}
|
||||
|
||||
public Profile(String name, File initialGameDir) {
|
||||
nameProperty = new ImmediateStringProperty(this, "name", name);
|
||||
gameDirProperty = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir);
|
||||
repository = new HMCLGameRepository(this, initialGameDir);
|
||||
modManager = new ModManager(repository);
|
||||
this(name, initialGameDir, new VersionSetting());
|
||||
}
|
||||
|
||||
gameDirProperty.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
|
||||
public Profile(String name, File initialGameDir, VersionSetting global) {
|
||||
this(name, initialGameDir, global, null, false);
|
||||
}
|
||||
|
||||
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
|
||||
this.name = new SimpleStringProperty(this, "name", name);
|
||||
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
|
||||
repository = new HMCLGameRepository(this, initialGameDir);
|
||||
this.global.set(global == null ? new VersionSetting() : global);
|
||||
this.selectedVersion.set(selectedVersion);
|
||||
this.useRelativePath.set(useRelativePath);
|
||||
|
||||
gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
|
||||
this.selectedVersion.addListener(o -> checkSelectedVersion());
|
||||
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
|
||||
|
||||
addPropertyChangedListener(onInvalidating(this::invalidate));
|
||||
}
|
||||
|
||||
private void checkSelectedVersion() {
|
||||
runInFX(() -> {
|
||||
if (!repository.isLoaded()) return;
|
||||
String newValue = selectedVersion.get();
|
||||
if (!repository.hasVersion(newValue)) {
|
||||
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
|
||||
if (version.isPresent())
|
||||
selectedVersion.setValue(version.get());
|
||||
else if (newValue != null)
|
||||
selectedVersion.setValue(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public HMCLGameRepository getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public ModManager getModManager() {
|
||||
return modManager;
|
||||
public DefaultDependencyManager getDependency() {
|
||||
return getDependency(DownloadProviders.getDownloadProvider());
|
||||
}
|
||||
|
||||
public HMCLDependencyManager getDependency() {
|
||||
return new HMCLDependencyManager(this, Settings.INSTANCE.getDownloadProvider(), Settings.INSTANCE.getProxy());
|
||||
public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) {
|
||||
return new DefaultDependencyManager(repository, downloadProvider, HMCLCacheRepository.REPOSITORY);
|
||||
}
|
||||
|
||||
public VersionSetting getVersionSetting(String id) {
|
||||
VersionSetting vs = repository.getVersionSetting(id);
|
||||
if (vs == null || vs.isUsesGlobal()) {
|
||||
getGlobal().setGlobal(true); // always keep global.isGlobal = true
|
||||
return getGlobal();
|
||||
} else
|
||||
return vs;
|
||||
return repository.getVersionSetting(id);
|
||||
}
|
||||
|
||||
public boolean isVersionGlobal(String id) {
|
||||
VersionSetting vs = repository.getVersionSetting(id);
|
||||
return vs == null || vs.isUsesGlobal();
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("gameDir", getGameDir())
|
||||
.append("name", getName())
|
||||
.append("useRelativePath", isUseRelativePath())
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make version use self version settings instead of the global one.
|
||||
* @param id the version id.
|
||||
* @return specialized version setting, null if given version does not exist.
|
||||
*/
|
||||
public VersionSetting specializeVersionSetting(String id) {
|
||||
VersionSetting vs = repository.getVersionSetting(id);
|
||||
if (vs == null)
|
||||
vs = repository.createVersionSetting(id);
|
||||
if (vs == null)
|
||||
return null;
|
||||
vs.setUsesGlobal(false);
|
||||
return vs;
|
||||
private void addPropertyChangedListener(InvalidationListener listener) {
|
||||
name.addListener(listener);
|
||||
global.addListener(listener);
|
||||
gameDir.addListener(listener);
|
||||
useRelativePath.addListener(listener);
|
||||
global.get().addPropertyChangedListener(listener);
|
||||
selectedVersion.addListener(listener);
|
||||
}
|
||||
|
||||
public void globalizeVersionSetting(String id) {
|
||||
VersionSetting vs = repository.getVersionSetting(id);
|
||||
if (vs != null)
|
||||
vs.setUsesGlobal(true);
|
||||
private ObservableHelper observableHelper = new ObservableHelper(this);
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
observableHelper.addListener(listener);
|
||||
}
|
||||
|
||||
public void addPropertyChangedListener(InvalidationListener listener) {
|
||||
nameProperty.addListener(listener);
|
||||
globalProperty.addListener(listener);
|
||||
gameDirProperty.addListener(listener);
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
observableHelper.removeListener(listener);
|
||||
}
|
||||
|
||||
private void invalidate() {
|
||||
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> {
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null)
|
||||
@@ -167,19 +235,23 @@ public final class Profile {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("global", context.serialize(src.getGlobal()));
|
||||
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
|
||||
jsonObject.addProperty("useRelativePath", src.isUseRelativePath());
|
||||
jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion());
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
|
||||
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
|
||||
|
||||
Profile profile = new Profile("Default", new File(gameDir));
|
||||
profile.setGlobal(context.deserialize(obj.get("global"), VersionSetting.class));
|
||||
return profile;
|
||||
return new Profile("Default",
|
||||
new File(gameDir),
|
||||
context.deserialize(obj.get("global"), VersionSetting.class),
|
||||
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""),
|
||||
Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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.setting;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
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.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static javafx.collections.FXCollections.observableArrayList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Profiles {
|
||||
|
||||
public static final String DEFAULT_PROFILE = "Default";
|
||||
public static final String HOME_PROFILE = "Home";
|
||||
|
||||
private Profiles() {
|
||||
}
|
||||
|
||||
public static String getProfileDisplayName(Profile profile) {
|
||||
switch (profile.getName()) {
|
||||
case Profiles.DEFAULT_PROFILE:
|
||||
return i18n("profile.default");
|
||||
case Profiles.HOME_PROFILE:
|
||||
return i18n("profile.home");
|
||||
default:
|
||||
return profile.getName();
|
||||
}
|
||||
}
|
||||
|
||||
private static final ObservableList<Profile> profiles = observableArrayList(profile -> new Observable[] { profile });
|
||||
private static final ReadOnlyListWrapper<Profile> profilesWrapper = new ReadOnlyListWrapper<>(profiles);
|
||||
|
||||
private static ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
|
||||
{
|
||||
profiles.addListener(onInvalidating(this::invalidated));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
if (!initialized)
|
||||
return;
|
||||
|
||||
Profile profile = get();
|
||||
|
||||
if (profiles.isEmpty()) {
|
||||
if (profile != null) {
|
||||
set(null);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!profiles.contains(profile)) {
|
||||
set(profiles.get(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
config().setSelectedProfile(profile == null ? "" : profile.getName());
|
||||
if (profile != null) {
|
||||
if (profile.getRepository().isLoaded())
|
||||
selectedVersion.bind(profile.selectedVersionProperty());
|
||||
else {
|
||||
selectedVersion.unbind();
|
||||
selectedVersion.set(null);
|
||||
// bind when repository was reloaded.
|
||||
profile.getRepository().refreshVersionsAsync().start();
|
||||
}
|
||||
} else {
|
||||
selectedVersion.unbind();
|
||||
selectedVersion.set(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static void checkProfiles() {
|
||||
if (profiles.isEmpty()) {
|
||||
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true);
|
||||
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY.toFile());
|
||||
Platform.runLater(() -> profiles.addAll(current, home));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if {@link #init()} hasn't been called.
|
||||
*/
|
||||
private static boolean initialized = false;
|
||||
|
||||
static {
|
||||
profiles.addListener(onInvalidating(Profiles::updateProfileStorages));
|
||||
profiles.addListener(onInvalidating(Profiles::checkProfiles));
|
||||
|
||||
selectedProfile.addListener((a, b, newValue) -> {
|
||||
if (newValue != null)
|
||||
newValue.getRepository().refreshVersionsAsync().start();
|
||||
});
|
||||
}
|
||||
|
||||
private static void updateProfileStorages() {
|
||||
// don't update the underlying storage before data loading is completed
|
||||
// otherwise it might cause data loss
|
||||
if (!initialized)
|
||||
return;
|
||||
// update storage
|
||||
config().getConfigurations().clear();
|
||||
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when it's ready to load profiles from {@link ConfigHolder#config()}.
|
||||
*/
|
||||
static void init() {
|
||||
if (initialized)
|
||||
throw new IllegalStateException("Already initialized");
|
||||
|
||||
HashSet<String> names = new HashSet<>();
|
||||
config().getConfigurations().forEach((name, profile) -> {
|
||||
if (!names.add(name)) return;
|
||||
profiles.add(profile);
|
||||
profile.setName(name);
|
||||
});
|
||||
checkProfiles();
|
||||
|
||||
// Platform.runLater is necessary or profiles will be empty
|
||||
// since checkProfiles adds 2 base profile later.
|
||||
Platform.runLater(() -> {
|
||||
initialized = true;
|
||||
|
||||
selectedProfile.set(
|
||||
profiles.stream()
|
||||
.filter(it -> it.getName().equals(config().getSelectedProfile()))
|
||||
.findFirst()
|
||||
.orElse(profiles.get(0)));
|
||||
});
|
||||
|
||||
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
|
||||
runInFX(() -> {
|
||||
Profile profile = selectedProfile.get();
|
||||
if (profile != null && profile.getRepository() == event.getSource()) {
|
||||
selectedVersion.bind(profile.selectedVersionProperty());
|
||||
for (Consumer<Profile> listener : versionsListeners)
|
||||
listener.accept(profile);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static ObservableList<Profile> getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
public static ReadOnlyListProperty<Profile> profilesProperty() {
|
||||
return profilesWrapper.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public static Profile getSelectedProfile() {
|
||||
return selectedProfile.get();
|
||||
}
|
||||
|
||||
public static void setSelectedProfile(Profile profile) {
|
||||
selectedProfile.set(profile);
|
||||
}
|
||||
|
||||
public static ObjectProperty<Profile> selectedProfileProperty() {
|
||||
return selectedProfile;
|
||||
}
|
||||
|
||||
private static final ReadOnlyStringWrapper selectedVersion = new ReadOnlyStringWrapper();
|
||||
|
||||
public static ReadOnlyStringProperty selectedVersionProperty() {
|
||||
return selectedVersion.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
// Guaranteed that the repository is loaded.
|
||||
public static String getSelectedVersion() {
|
||||
return selectedVersion.get();
|
||||
}
|
||||
|
||||
private static final List<Consumer<Profile>> versionsListeners = new LinkedList<>();
|
||||
|
||||
public static void registerVersionsListener(Consumer<Profile> listener) {
|
||||
Profile profile = getSelectedProfile();
|
||||
if (profile != null && profile.getRepository().isLoaded())
|
||||
listener.accept(profile);
|
||||
versionsListeners.add(listener);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class Proxies {
|
||||
private Proxies() {}
|
||||
|
||||
public static final List<Proxy.Type> PROXIES = Arrays.asList(Proxy.Type.DIRECT, Proxy.Type.HTTP, Proxy.Type.SOCKS);
|
||||
|
||||
public static Proxy.Type getProxyType(int index) {
|
||||
return Lang.get(PROXIES, index).orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.setting;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.net.Authenticator;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.PasswordAuthentication;
|
||||
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;
|
||||
|
||||
public final class ProxyManager {
|
||||
private ProxyManager() {
|
||||
}
|
||||
|
||||
private static ObjectBinding<Proxy> proxyProperty;
|
||||
|
||||
public static Proxy getProxy() {
|
||||
return proxyProperty.get();
|
||||
}
|
||||
|
||||
public static ObservableObjectValue<Proxy> proxyProperty() {
|
||||
return proxyProperty;
|
||||
}
|
||||
|
||||
static void init() {
|
||||
proxyProperty = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
String host = config().getProxyHost();
|
||||
int port = config().getProxyPort();
|
||||
if (!config().hasProxy() || StringUtils.isBlank(host) || config().getProxyType() == Proxy.Type.DIRECT) {
|
||||
return Proxy.NO_PROXY;
|
||||
} else {
|
||||
if (port < 0 || port > 0xFFFF) {
|
||||
LOG.warning("Illegal proxy port: " + port);
|
||||
return Proxy.NO_PROXY;
|
||||
}
|
||||
return new Proxy(config().getProxyType(), new InetSocketAddress(host, port));
|
||||
}
|
||||
},
|
||||
config().proxyTypeProperty(),
|
||||
config().proxyHostProperty(),
|
||||
config().proxyPortProperty(),
|
||||
config().hasProxyProperty());
|
||||
|
||||
proxyProperty.addListener(any -> updateSystemProxy());
|
||||
updateSystemProxy();
|
||||
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
if (config().hasProxyAuth()) {
|
||||
String username = config().getProxyUser();
|
||||
String password = config().getProxyPass();
|
||||
if (username != null && password != null) {
|
||||
return new PasswordAuthentication(username, password.toCharArray());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void updateSystemProxy() {
|
||||
Proxy proxy = proxyProperty.get();
|
||||
if (proxy.type() == Proxy.Type.DIRECT) {
|
||||
System.clearProperty("http.proxyHost");
|
||||
System.clearProperty("http.proxyPort");
|
||||
System.clearProperty("https.proxyHost");
|
||||
System.clearProperty("https.proxyPort");
|
||||
System.clearProperty("socksProxyHost");
|
||||
System.clearProperty("socksProxyPort");
|
||||
} else {
|
||||
InetSocketAddress address = (InetSocketAddress) proxy.address();
|
||||
String host = address.getHostString();
|
||||
String port = String.valueOf(address.getPort());
|
||||
if (proxy.type() == Type.HTTP) {
|
||||
System.clearProperty("socksProxyHost");
|
||||
System.clearProperty("socksProxyPort");
|
||||
System.setProperty("http.proxyHost", host);
|
||||
System.setProperty("http.proxyPort", port);
|
||||
System.setProperty("https.proxyHost", host);
|
||||
System.setProperty("https.proxyPort", port);
|
||||
} else if (proxy.type() == Type.SOCKS) {
|
||||
System.clearProperty("http.proxyHost");
|
||||
System.clearProperty("http.proxyPort");
|
||||
System.clearProperty("https.proxyHost");
|
||||
System.clearProperty("https.proxyPort");
|
||||
System.setProperty("socksProxyHost", host);
|
||||
System.setProperty("socksProxyPort", port);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,506 +13,74 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.MojangDownloadProvider;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.ProfileChangedEvent;
|
||||
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.Authenticator;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.PasswordAuthentication;
|
||||
import java.net.Proxy;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Settings {
|
||||
public static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(Profile.class, Profile.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||
.setPrettyPrinting().create();
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
public static final String DEFAULT_PROFILE = "Default";
|
||||
public static final String HOME_PROFILE = "Home";
|
||||
public final class Settings {
|
||||
|
||||
public static final File SETTINGS_FILE = new File("hmcl.json").getAbsoluteFile();
|
||||
private static Settings instance;
|
||||
|
||||
public static final Settings INSTANCE = new Settings();
|
||||
|
||||
private Settings() {}
|
||||
|
||||
private final Config SETTINGS = initSettings();
|
||||
|
||||
private Map<String, Account> accounts = new HashMap<>();
|
||||
|
||||
{
|
||||
for (Map<Object, Object> settings : SETTINGS.getAccounts()) {
|
||||
Optional<String> characterName = Accounts.getCurrentCharacter(settings);
|
||||
AccountFactory factory = Accounts.ACCOUNT_FACTORY.get(Lang.get(settings, "type", String.class, ""));
|
||||
if (factory == null || !characterName.isPresent()) {
|
||||
// unrecognized account type, so remove it.
|
||||
SETTINGS.getAccounts().remove(settings);
|
||||
continue;
|
||||
}
|
||||
|
||||
Account account;
|
||||
try {
|
||||
account = factory.fromStorage(settings);
|
||||
} catch (Exception e) {
|
||||
SETTINGS.getAccounts().remove(settings);
|
||||
// storage is malformed, delete.
|
||||
continue;
|
||||
}
|
||||
|
||||
accounts.put(Accounts.getAccountId(account), account);
|
||||
public static Settings instance() {
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException("Settings hasn't been initialized");
|
||||
}
|
||||
|
||||
save();
|
||||
|
||||
checkProfileMap();
|
||||
|
||||
for (Map.Entry<String, Profile> entry2 : getProfileMap().entrySet()) {
|
||||
entry2.getValue().setName(entry2.getKey());
|
||||
entry2.getValue().nameProperty().setChangedListener(this::profileNameChanged);
|
||||
entry2.getValue().addPropertyChangedListener(e -> {
|
||||
save();
|
||||
});
|
||||
}
|
||||
|
||||
Lang.ignoringException(() -> {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::save));
|
||||
});
|
||||
|
||||
loadProxy();
|
||||
}
|
||||
|
||||
private Config initSettings() {
|
||||
Config c = new Config();
|
||||
if (SETTINGS_FILE.exists())
|
||||
try {
|
||||
String str = FileUtils.readText(SETTINGS_FILE);
|
||||
if (StringUtils.isBlank(str))
|
||||
Logging.LOG.finer("Settings file is empty, use the default settings.");
|
||||
else {
|
||||
Config d = GSON.fromJson(str, Config.class);
|
||||
if (d != null)
|
||||
c = d;
|
||||
}
|
||||
Logging.LOG.finest("Initialized settings.");
|
||||
} catch (Exception e) {
|
||||
Logging.LOG.log(Level.WARNING, "Something happened wrongly when load settings.", e);
|
||||
}
|
||||
else {
|
||||
Logging.LOG.config("No settings file here, may be first loading.");
|
||||
if (!c.getConfigurations().containsKey(HOME_PROFILE))
|
||||
c.getConfigurations().put(HOME_PROFILE, new Profile(HOME_PROFILE, Main.MINECRAFT_DIRECTORY));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
public void save() {
|
||||
try {
|
||||
SETTINGS.getAccounts().clear();
|
||||
for (Account account : accounts.values()) {
|
||||
Map<Object, Object> storage = account.toStorage();
|
||||
storage.put("type", Accounts.getAccountType(account));
|
||||
SETTINGS.getAccounts().add(storage);
|
||||
}
|
||||
|
||||
FileUtils.writeText(SETTINGS_FILE, GSON.toJson(SETTINGS));
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to save config", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.getCommonDirectory()) {
|
||||
@Override
|
||||
public void invalidated() {
|
||||
super.invalidated();
|
||||
|
||||
SETTINGS.setCommonDirectory(get());
|
||||
}
|
||||
};
|
||||
|
||||
public String getCommonPath() {
|
||||
return commonPath.get();
|
||||
}
|
||||
|
||||
public StringProperty commonPathProperty() {
|
||||
return commonPath;
|
||||
}
|
||||
|
||||
public void setCommonPath(String commonPath) {
|
||||
this.commonPath.set(commonPath);
|
||||
}
|
||||
|
||||
private Locales.SupportedLocale locale = Locales.getLocaleByName(SETTINGS.getLocalization());
|
||||
|
||||
public Locales.SupportedLocale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public void setLocale(Locales.SupportedLocale locale) {
|
||||
this.locale = locale;
|
||||
SETTINGS.setLocalization(Locales.getNameByLocale(locale));
|
||||
}
|
||||
|
||||
private Proxy proxy = Proxy.NO_PROXY;
|
||||
|
||||
public Proxy getProxy() {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
private Proxy.Type proxyType = Proxies.getProxyType(SETTINGS.getProxyType());
|
||||
|
||||
public Proxy.Type getProxyType() {
|
||||
return proxyType;
|
||||
}
|
||||
|
||||
public void setProxyType(Proxy.Type proxyType) {
|
||||
this.proxyType = proxyType;
|
||||
SETTINGS.setProxyType(Proxies.PROXIES.indexOf(proxyType));
|
||||
loadProxy();
|
||||
}
|
||||
|
||||
public String getProxyHost() {
|
||||
return SETTINGS.getProxyHost();
|
||||
}
|
||||
|
||||
public void setProxyHost(String proxyHost) {
|
||||
SETTINGS.setProxyHost(proxyHost);
|
||||
}
|
||||
|
||||
public String getProxyPort() {
|
||||
return SETTINGS.getProxyPort();
|
||||
}
|
||||
|
||||
public void setProxyPort(String proxyPort) {
|
||||
SETTINGS.setProxyPort(proxyPort);
|
||||
}
|
||||
|
||||
public String getProxyUser() {
|
||||
return SETTINGS.getProxyUser();
|
||||
}
|
||||
|
||||
public void setProxyUser(String proxyUser) {
|
||||
SETTINGS.setProxyUser(proxyUser);
|
||||
}
|
||||
|
||||
public String getProxyPass() {
|
||||
return SETTINGS.getProxyPass();
|
||||
}
|
||||
|
||||
public void setProxyPass(String proxyPass) {
|
||||
SETTINGS.setProxyPass(proxyPass);
|
||||
}
|
||||
|
||||
private void loadProxy() {
|
||||
String host = getProxyHost();
|
||||
Integer port = Lang.toIntOrNull(getProxyPort());
|
||||
if (StringUtils.isBlank(host) || port == null)
|
||||
proxy = Proxy.NO_PROXY;
|
||||
else {
|
||||
System.setProperty("http.proxyHost", getProxyHost());
|
||||
System.setProperty("http.proxyPort", getProxyPort());
|
||||
if (getProxyType() == Proxy.Type.DIRECT)
|
||||
proxy = Proxy.NO_PROXY;
|
||||
else
|
||||
proxy = new Proxy(proxyType, new InetSocketAddress(host, port));
|
||||
|
||||
String user = getProxyUser();
|
||||
String pass = getProxyPass();
|
||||
if (StringUtils.isNotBlank(user) && StringUtils.isNotBlank(pass)) {
|
||||
System.setProperty("http.proxyUser", user);
|
||||
System.setProperty("http.proxyPassword", pass);
|
||||
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
public PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(user, pass.toCharArray());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Font getFont() {
|
||||
return Font.font(SETTINGS.getFontFamily(), SETTINGS.getFontSize());
|
||||
}
|
||||
|
||||
public void setFont(Font font) {
|
||||
SETTINGS.setFontFamily(font.getFamily());
|
||||
SETTINGS.setFontSize(font.getSize());
|
||||
}
|
||||
|
||||
public int getLogLines() {
|
||||
return Math.max(SETTINGS.getLogLines(), 100);
|
||||
}
|
||||
|
||||
public void setLogLines(int logLines) {
|
||||
SETTINGS.setLogLines(logLines);
|
||||
}
|
||||
|
||||
public DownloadProvider getDownloadProvider() {
|
||||
switch (SETTINGS.getDownloadType()) {
|
||||
case 0:
|
||||
return MojangDownloadProvider.INSTANCE;
|
||||
case 1:
|
||||
return BMCLAPIDownloadProvider.INSTANCE;
|
||||
default:
|
||||
return MojangDownloadProvider.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
public void setDownloadProvider(DownloadProvider downloadProvider) {
|
||||
if (downloadProvider == MojangDownloadProvider.INSTANCE)
|
||||
SETTINGS.setDownloadType(0);
|
||||
else if (downloadProvider == BMCLAPIDownloadProvider.INSTANCE)
|
||||
SETTINGS.setDownloadType(1);
|
||||
else
|
||||
throw new IllegalArgumentException("Unknown download provider: " + downloadProvider);
|
||||
}
|
||||
|
||||
/****************************************
|
||||
* ACCOUNTS *
|
||||
****************************************/
|
||||
|
||||
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.getSelectedAccount())) {
|
||||
@Override
|
||||
public Account get() {
|
||||
Account a = super.get();
|
||||
if (a == null || !accounts.containsKey(Accounts.getAccountId(a))) {
|
||||
Account acc = accounts.values().stream().findAny().orElse(null);
|
||||
set(acc);
|
||||
return acc;
|
||||
} else return a;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(Account newValue) {
|
||||
if (newValue == null || accounts.containsKey(Accounts.getAccountId(newValue))) {
|
||||
super.set(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated() {
|
||||
super.invalidated();
|
||||
|
||||
SETTINGS.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
|
||||
}
|
||||
};
|
||||
|
||||
public Account getSelectedAccount() {
|
||||
return selectedAccount.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Account> selectedAccountProperty() {
|
||||
return selectedAccount;
|
||||
}
|
||||
|
||||
public void setSelectedAccount(Account selectedAccount) {
|
||||
this.selectedAccount.set(selectedAccount);
|
||||
}
|
||||
|
||||
public void addAccount(Account account) {
|
||||
accounts.put(Accounts.getAccountId(account), account);
|
||||
}
|
||||
|
||||
public Account getAccount(String name, String character) {
|
||||
return accounts.get(Accounts.getAccountId(name, character));
|
||||
}
|
||||
|
||||
public Collection<Account> getAccounts() {
|
||||
return Collections.unmodifiableCollection(accounts.values());
|
||||
}
|
||||
|
||||
public void deleteAccount(String name, String character) {
|
||||
accounts.remove(Accounts.getAccountId(name, character));
|
||||
|
||||
selectedAccount.get();
|
||||
}
|
||||
|
||||
public void deleteAccount(Account account) {
|
||||
accounts.remove(Accounts.getAccountId(account));
|
||||
|
||||
selectedAccount.get();
|
||||
}
|
||||
|
||||
/****************************************
|
||||
* BACKGROUND *
|
||||
****************************************/
|
||||
|
||||
private final ImmediateStringProperty backgroundImage = new ImmediateStringProperty(this, "backgroundImage", SETTINGS.getBackgroundImage()) {
|
||||
@Override
|
||||
public void invalidated() {
|
||||
super.invalidated();
|
||||
|
||||
SETTINGS.setBackgroundImage(get());
|
||||
}
|
||||
};
|
||||
|
||||
public String getBackgroundImage() {
|
||||
return backgroundImage.get();
|
||||
}
|
||||
|
||||
public ImmediateStringProperty backgroundImageProperty() {
|
||||
return backgroundImage;
|
||||
}
|
||||
|
||||
public void setBackgroundImage(String backgroundImage) {
|
||||
this.backgroundImage.set(backgroundImage);
|
||||
}
|
||||
|
||||
private final ImmediateObjectProperty<EnumBackgroundImage> backgroundImageType = new ImmediateObjectProperty<EnumBackgroundImage>(this, "backgroundImageType", EnumBackgroundImage.indexOf(SETTINGS.getBackgroundImageType())) {
|
||||
@Override
|
||||
public void invalidated() {
|
||||
super.invalidated();
|
||||
|
||||
SETTINGS.setBackgroundImageType(get().ordinal());
|
||||
}
|
||||
};
|
||||
|
||||
public EnumBackgroundImage getBackgroundImageType() {
|
||||
return backgroundImageType.get();
|
||||
}
|
||||
|
||||
public ImmediateObjectProperty<EnumBackgroundImage> backgroundImageTypeProperty() {
|
||||
return backgroundImageType;
|
||||
}
|
||||
|
||||
public void setBackgroundImageType(EnumBackgroundImage backgroundImageType) {
|
||||
this.backgroundImageType.set(backgroundImageType);
|
||||
}
|
||||
|
||||
/****************************************
|
||||
* THEME *
|
||||
****************************************/
|
||||
|
||||
private final ImmediateObjectProperty<Theme> theme = new ImmediateObjectProperty<Theme>(this, "theme", Theme.getTheme(SETTINGS.getTheme()).orElse(Theme.BLUE)) {
|
||||
@Override
|
||||
public void invalidated() {
|
||||
super.invalidated();
|
||||
|
||||
SETTINGS.setTheme(get().name().toLowerCase());
|
||||
}
|
||||
};
|
||||
|
||||
public Theme getTheme() {
|
||||
return theme.get();
|
||||
}
|
||||
|
||||
public void setTheme(Theme theme) {
|
||||
this.theme.set(theme);
|
||||
}
|
||||
|
||||
public ImmediateObjectProperty<Theme> themeProperty() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
/****************************************
|
||||
* PROFILES *
|
||||
****************************************/
|
||||
|
||||
private Profile selectedProfile;
|
||||
|
||||
public Profile getSelectedProfile() {
|
||||
checkProfileMap();
|
||||
|
||||
if (!hasProfile(SETTINGS.getSelectedProfile())) {
|
||||
SETTINGS.setSelectedProfile(getProfileMap().keySet().stream().findFirst().get());
|
||||
Schedulers.computation().schedule(this::onProfileChanged);
|
||||
}
|
||||
return getProfile(SETTINGS.getSelectedProfile());
|
||||
}
|
||||
|
||||
public void setSelectedProfile(Profile selectedProfile) {
|
||||
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.getSelectedProfile())) {
|
||||
SETTINGS.setSelectedProfile(selectedProfile.getName());
|
||||
Schedulers.computation().schedule(this::onProfileChanged);
|
||||
}
|
||||
}
|
||||
|
||||
public Profile getProfile(String name) {
|
||||
checkProfileMap();
|
||||
|
||||
Optional<Profile> p = name == null ? getProfileMap().values().stream().findFirst() : Optional.ofNullable(getProfileMap().get(name));
|
||||
return p.orElse(null);
|
||||
}
|
||||
|
||||
public boolean hasProfile(String name) {
|
||||
return getProfileMap().containsKey(Lang.nonNull(name, DEFAULT_PROFILE));
|
||||
}
|
||||
|
||||
public Map<String, Profile> getProfileMap() {
|
||||
return SETTINGS.getConfigurations();
|
||||
}
|
||||
|
||||
public Collection<Profile> getProfiles() {
|
||||
return getProfileMap().values().stream().filter(t -> StringUtils.isNotBlank(t.getName())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void putProfile(Profile ver) {
|
||||
if (StringUtils.isBlank(ver.getName()))
|
||||
throw new IllegalArgumentException("Profile's name is empty");
|
||||
|
||||
getProfileMap().put(ver.getName(), ver);
|
||||
|
||||
ver.nameProperty().setChangedListener(this::profileNameChanged);
|
||||
}
|
||||
|
||||
public void deleteProfile(Profile profile) {
|
||||
deleteProfile(profile.getName());
|
||||
}
|
||||
|
||||
public void deleteProfile(String profileName) {
|
||||
getProfileMap().remove(profileName);
|
||||
checkProfileMap();
|
||||
Schedulers.computation().schedule(this::onProfileLoading);
|
||||
}
|
||||
|
||||
private void checkProfileMap() {
|
||||
if (getProfileMap().isEmpty())
|
||||
getProfileMap().put(DEFAULT_PROFILE, new Profile(DEFAULT_PROFILE));
|
||||
}
|
||||
|
||||
private void onProfileChanged() {
|
||||
getSelectedProfile().getRepository().refreshVersionsAsync().start();
|
||||
EventBus.EVENT_BUS.fireEvent(new ProfileChangedEvent(SETTINGS, getSelectedProfile()));
|
||||
}
|
||||
|
||||
private void profileNameChanged(ObservableValue<? extends String> observableValue, String oldValue, String newValue) {
|
||||
getProfileMap().put(newValue, getProfileMap().remove(oldValue));
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start profiles loading process.
|
||||
* Invoked by loading GUI phase.
|
||||
* Should be called from {@link ConfigHolder#init()}.
|
||||
*/
|
||||
public void onProfileLoading() {
|
||||
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(SETTINGS));
|
||||
onProfileChanged();
|
||||
static void init() {
|
||||
instance = new Settings();
|
||||
}
|
||||
|
||||
private Settings() {
|
||||
config().localizationProperty().addListener(unused -> updateSystemLocale());
|
||||
updateSystemLocale();
|
||||
|
||||
DownloadProviders.init();
|
||||
ProxyManager.init();
|
||||
Accounts.init();
|
||||
Profiles.init();
|
||||
AuthlibInjectorServers.init();
|
||||
|
||||
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
|
||||
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (FileUtils.canCreateDirectory(getCommonDirectory())) {
|
||||
return getCommonDirectory();
|
||||
} else {
|
||||
return getDefaultCommonDirectory();
|
||||
}
|
||||
}, config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
||||
}
|
||||
|
||||
public static String getDefaultCommonDirectory() {
|
||||
return Metadata.MINECRAFT_DIRECTORY.toString();
|
||||
}
|
||||
|
||||
public String getCommonDirectory() {
|
||||
switch (config().getCommonDirType()) {
|
||||
case DEFAULT:
|
||||
return getDefaultCommonDirectory();
|
||||
case CUSTOM:
|
||||
return config().getCommonDirectory();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateSystemLocale() {
|
||||
Locale.setDefault(config().getLocalization().getLocale());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
* 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
|
||||
@@ -13,33 +13,167 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
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 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.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public enum Theme {
|
||||
BLUE,
|
||||
DARK_BLUE,
|
||||
GREEN,
|
||||
ORANGE,
|
||||
PURPLE,
|
||||
RED;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
@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("#3D6DA3"), // blue
|
||||
Color.web("#283593"), // dark blue
|
||||
Color.web("#43A047"), // green
|
||||
Color.web("#E67E22"), // orange
|
||||
Color.web("#9C27B0"), // purple
|
||||
Color.web("#B71C1C") // red
|
||||
};
|
||||
|
||||
private final Color paint;
|
||||
private final String color;
|
||||
private final String name;
|
||||
|
||||
Theme(String name, String color) {
|
||||
this.name = name;
|
||||
this.color = Objects.requireNonNull(color);
|
||||
this.paint = Color.web(color);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public boolean isCustom() {
|
||||
return name.startsWith("#");
|
||||
}
|
||||
|
||||
public boolean isLight() {
|
||||
return Color.web(color).grayscale().getRed() >= 0.5;
|
||||
}
|
||||
|
||||
public Color getForegroundColor() {
|
||||
return isLight() ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
|
||||
public String[] getStylesheets(String overrideFontFamily) {
|
||||
Color textFill = getForegroundColor();
|
||||
|
||||
String fontFamily;
|
||||
if (System.getProperty("hmcl.font.override") != null) {
|
||||
fontFamily = System.getProperty("hmcl.font.override");
|
||||
} else if (overrideFontFamily != null) {
|
||||
fontFamily = overrideFontFamily;
|
||||
} else {
|
||||
fontFamily = null;
|
||||
}
|
||||
|
||||
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-red%", Integer.toString((int)Math.ceil(paint.getRed() * 256)))
|
||||
.replace("%base-green%", Integer.toString((int)Math.ceil(paint.getGreen() * 256)))
|
||||
.replace("%base-blue%", Integer.toString((int)Math.ceil(paint.getBlue() * 256)))
|
||||
.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(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").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() {
|
||||
return new String[]{
|
||||
Theme.class.getResource("/css/jfoenix-fonts.css").toExternalForm(),
|
||||
Theme.class.getResource("/css/jfoenix-design.css").toExternalForm(),
|
||||
Theme.class.getResource("/assets/css/" + name().toLowerCase() + ".css").toExternalForm(),
|
||||
Theme.class.getResource("/assets/css/root.css").toExternalForm()
|
||||
css,
|
||||
"/assets/css/root.css"
|
||||
};
|
||||
}
|
||||
|
||||
public static Theme custom(String color) {
|
||||
if (!color.startsWith("#"))
|
||||
throw new IllegalArgumentException();
|
||||
return new Theme(color, color);
|
||||
}
|
||||
|
||||
public static Optional<Theme> getTheme(String name) {
|
||||
for (Theme theme : values())
|
||||
if (theme.name().equalsIgnoreCase(name))
|
||||
return Optional.of(theme);
|
||||
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("#"))
|
||||
try {
|
||||
Color.web(name);
|
||||
return Optional.of(custom(name));
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> foregroundFillBinding() {
|
||||
return BindingMapping.of(config().themeProperty())
|
||||
.map(Theme::getForegroundColor);
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> blackFillBinding() {
|
||||
return Bindings.createObjectBinding(() -> BLACK);
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> whiteFillBinding() {
|
||||
return Bindings.createObjectBinding(() -> 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());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Theme read(JsonReader in) throws IOException {
|
||||
return getTheme(in.nextString()).orElse(Theme.BLUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2013 huangyuhui <huanghongxun2008@126.com>
|
||||
* 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
|
||||
@@ -13,28 +13,42 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* 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 org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
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.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.jfoenix.concurrency.JFXUtilities.runInFX;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class VersionSetting {
|
||||
|
||||
public transient String id;
|
||||
@JsonAdapter(VersionSetting.Serializer.class)
|
||||
public final class VersionSetting implements Cloneable {
|
||||
|
||||
private boolean global = false;
|
||||
|
||||
@@ -46,9 +60,9 @@ public final class VersionSetting {
|
||||
this.global = global;
|
||||
}
|
||||
|
||||
private final ImmediateBooleanProperty usesGlobalProperty = new ImmediateBooleanProperty(this, "usesGlobal", false);
|
||||
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", false);
|
||||
|
||||
public ImmediateBooleanProperty usesGlobalProperty() {
|
||||
public BooleanProperty usesGlobalProperty() {
|
||||
return usesGlobalProperty;
|
||||
}
|
||||
|
||||
@@ -57,7 +71,7 @@ public final class VersionSetting {
|
||||
* 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() {
|
||||
@@ -70,14 +84,14 @@ public final class VersionSetting {
|
||||
|
||||
// java
|
||||
|
||||
private final ImmediateStringProperty javaProperty = new ImmediateStringProperty(this, "java", "");
|
||||
private final StringProperty javaProperty = new SimpleStringProperty(this, "java", "");
|
||||
|
||||
public ImmediateStringProperty javaProperty() {
|
||||
public StringProperty javaProperty() {
|
||||
return javaProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Java version or null if user customizes java directory.
|
||||
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
|
||||
*/
|
||||
public String getJava() {
|
||||
return javaProperty.get();
|
||||
@@ -87,9 +101,78 @@ public final class VersionSetting {
|
||||
javaProperty.set(java);
|
||||
}
|
||||
|
||||
private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", "");
|
||||
public boolean isUsesCustomJavaDir() {
|
||||
return "Custom".equals(getJava());
|
||||
}
|
||||
|
||||
public ImmediateStringProperty javaDirProperty() {
|
||||
public void setUsesCustomJavaDir() {
|
||||
setJava("Custom");
|
||||
setDefaultJavaPath(null);
|
||||
}
|
||||
|
||||
public boolean isJavaAutoSelected() {
|
||||
return "Auto".equals(getJava());
|
||||
}
|
||||
|
||||
public void setJavaAutoSelected() {
|
||||
setJava("Auto");
|
||||
setDefaultJavaPath(null);
|
||||
}
|
||||
|
||||
private final StringProperty defaultJavaPathProperty = new SimpleStringProperty(this, "defaultJavaPath", "");
|
||||
|
||||
/**
|
||||
* Path to Java executable, or null if user customizes java directory.
|
||||
* It's used to determine which JRE to use when multiple JREs match the selected Java version.
|
||||
*/
|
||||
public String getDefaultJavaPath() {
|
||||
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() {
|
||||
return javaDirProperty;
|
||||
}
|
||||
|
||||
@@ -104,9 +187,9 @@ public final class VersionSetting {
|
||||
javaDirProperty.set(javaDir);
|
||||
}
|
||||
|
||||
private final ImmediateStringProperty wrapperProperty = new ImmediateStringProperty(this, "wrapper", "");
|
||||
private final StringProperty wrapperProperty = new SimpleStringProperty(this, "wrapper", "");
|
||||
|
||||
public ImmediateStringProperty wrapperProperty() {
|
||||
public StringProperty wrapperProperty() {
|
||||
return wrapperProperty;
|
||||
}
|
||||
|
||||
@@ -121,9 +204,9 @@ public final class VersionSetting {
|
||||
wrapperProperty.set(wrapper);
|
||||
}
|
||||
|
||||
private final ImmediateStringProperty permSizeProperty = new ImmediateStringProperty(this, "permSize", "");
|
||||
private final StringProperty permSizeProperty = new SimpleStringProperty(this, "permSize", "");
|
||||
|
||||
public ImmediateStringProperty permSizeProperty() {
|
||||
public StringProperty permSizeProperty() {
|
||||
return permSizeProperty;
|
||||
}
|
||||
|
||||
@@ -138,9 +221,9 @@ public final class VersionSetting {
|
||||
permSizeProperty.set(permSize);
|
||||
}
|
||||
|
||||
private final ImmediateIntegerProperty maxMemoryProperty = new ImmediateIntegerProperty(this, "maxMemory", (int) OperatingSystem.SUGGESTED_MEMORY);
|
||||
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
|
||||
|
||||
public ImmediateIntegerProperty maxMemoryProperty() {
|
||||
public IntegerProperty maxMemoryProperty() {
|
||||
return maxMemoryProperty;
|
||||
}
|
||||
|
||||
@@ -158,9 +241,9 @@ public final class VersionSetting {
|
||||
/**
|
||||
* The minimum memory that JVM can allocate for heap.
|
||||
*/
|
||||
private final ImmediateObjectProperty<Integer> minMemoryProperty = new ImmediateObjectProperty<>(this, "minMemory", null);
|
||||
private final ObjectProperty<Integer> minMemoryProperty = new SimpleObjectProperty<>(this, "minMemory", null);
|
||||
|
||||
public ImmediateObjectProperty<Integer> minMemoryProperty() {
|
||||
public ObjectProperty<Integer> minMemoryProperty() {
|
||||
return minMemoryProperty;
|
||||
}
|
||||
|
||||
@@ -172,9 +255,23 @@ public final class VersionSetting {
|
||||
minMemoryProperty.set(minMemory);
|
||||
}
|
||||
|
||||
private final ImmediateStringProperty preLaunchCommandProperty = new ImmediateStringProperty(this, "precalledCommand", "");
|
||||
private final BooleanProperty autoMemory = new SimpleBooleanProperty(this, "autoMemory", true);
|
||||
|
||||
public ImmediateStringProperty preLaunchCommandProperty() {
|
||||
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() {
|
||||
return preLaunchCommandProperty;
|
||||
}
|
||||
|
||||
@@ -190,11 +287,29 @@ public final class VersionSetting {
|
||||
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 ImmediateStringProperty javaArgsProperty = new ImmediateStringProperty(this, "javaArgs", "");
|
||||
private final StringProperty javaArgsProperty = new SimpleStringProperty(this, "javaArgs", "");
|
||||
|
||||
public ImmediateStringProperty javaArgsProperty() {
|
||||
public StringProperty javaArgsProperty() {
|
||||
return javaArgsProperty;
|
||||
}
|
||||
|
||||
@@ -209,9 +324,9 @@ public final class VersionSetting {
|
||||
javaArgsProperty.set(javaArgs);
|
||||
}
|
||||
|
||||
private final ImmediateStringProperty minecraftArgsProperty = new ImmediateStringProperty(this, "minecraftArgs", "");
|
||||
private final StringProperty minecraftArgsProperty = new SimpleStringProperty(this, "minecraftArgs", "");
|
||||
|
||||
public ImmediateStringProperty minecraftArgsProperty() {
|
||||
public StringProperty minecraftArgsProperty() {
|
||||
return minecraftArgsProperty;
|
||||
}
|
||||
|
||||
@@ -226,9 +341,9 @@ public final class VersionSetting {
|
||||
minecraftArgsProperty.set(minecraftArgs);
|
||||
}
|
||||
|
||||
private final ImmediateBooleanProperty noJVMArgsProperty = new ImmediateBooleanProperty(this, "noJVMArgs", false);
|
||||
private final BooleanProperty noJVMArgsProperty = new SimpleBooleanProperty(this, "noJVMArgs", false);
|
||||
|
||||
public ImmediateBooleanProperty noJVMArgsProperty() {
|
||||
public BooleanProperty noJVMArgsProperty() {
|
||||
return noJVMArgsProperty;
|
||||
}
|
||||
|
||||
@@ -243,9 +358,26 @@ public final class VersionSetting {
|
||||
noJVMArgsProperty.set(noJVMArgs);
|
||||
}
|
||||
|
||||
private final ImmediateBooleanProperty notCheckGameProperty = new ImmediateBooleanProperty(this, "notCheckGame", false);
|
||||
private final BooleanProperty notCheckJVMProperty = new SimpleBooleanProperty(this, "notCheckJVM", false);
|
||||
|
||||
public ImmediateBooleanProperty notCheckGameProperty() {
|
||||
public BooleanProperty notCheckJVMProperty() {
|
||||
return notCheckJVMProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if HMCL does not check JVM validity.
|
||||
*/
|
||||
public boolean isNotCheckJVM() {
|
||||
return notCheckJVMProperty.get();
|
||||
}
|
||||
|
||||
public void setNotCheckJVM(boolean notCheckJVM) {
|
||||
notCheckJVMProperty.set(notCheckJVM);
|
||||
}
|
||||
|
||||
private final BooleanProperty notCheckGameProperty = new SimpleBooleanProperty(this, "notCheckGame", false);
|
||||
|
||||
public BooleanProperty notCheckGameProperty() {
|
||||
return notCheckGameProperty;
|
||||
}
|
||||
|
||||
@@ -260,26 +392,9 @@ public final class VersionSetting {
|
||||
notCheckGameProperty.set(notCheckGame);
|
||||
}
|
||||
|
||||
private final ImmediateBooleanProperty noCommonProperty = new ImmediateBooleanProperty(this, "noCommon", false);
|
||||
private final BooleanProperty showLogsProperty = new SimpleBooleanProperty(this, "showLogs", false);
|
||||
|
||||
public ImmediateBooleanProperty noCommonProperty() {
|
||||
return noCommonProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if HMCL does not find/download libraries in/to common path.
|
||||
*/
|
||||
public boolean isNoCommon() {
|
||||
return noCommonProperty.get();
|
||||
}
|
||||
|
||||
public void setNoCommon(boolean noCommon) {
|
||||
noCommonProperty.set(noCommon);
|
||||
}
|
||||
|
||||
private final ImmediateBooleanProperty showLogsProperty = new ImmediateBooleanProperty(this, "showLogs", false);
|
||||
|
||||
public ImmediateBooleanProperty showLogsProperty() {
|
||||
public BooleanProperty showLogsProperty() {
|
||||
return showLogsProperty;
|
||||
}
|
||||
|
||||
@@ -296,15 +411,15 @@ public final class VersionSetting {
|
||||
|
||||
// Minecraft settings.
|
||||
|
||||
private final ImmediateStringProperty serverIpProperty = new ImmediateStringProperty(this, "serverIp", "");
|
||||
private final StringProperty serverIpProperty = new SimpleStringProperty(this, "serverIp", "");
|
||||
|
||||
public ImmediateStringProperty serverIpProperty() {
|
||||
public StringProperty serverIpProperty() {
|
||||
return serverIpProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* The server ip that will be entered after Minecraft successfully loaded immediately.
|
||||
*
|
||||
* The server ip that will be entered after Minecraft successfully loaded ly.
|
||||
* <p>
|
||||
* Format: ip:port or without port.
|
||||
*/
|
||||
public String getServerIp() {
|
||||
@@ -316,9 +431,9 @@ public final class VersionSetting {
|
||||
}
|
||||
|
||||
|
||||
private final ImmediateBooleanProperty fullscreenProperty = new ImmediateBooleanProperty(this, "fullscreen", false);
|
||||
private final BooleanProperty fullscreenProperty = new SimpleBooleanProperty(this, "fullscreen", false);
|
||||
|
||||
public ImmediateBooleanProperty fullscreenProperty() {
|
||||
public BooleanProperty fullscreenProperty() {
|
||||
return fullscreenProperty;
|
||||
}
|
||||
|
||||
@@ -333,15 +448,15 @@ public final class VersionSetting {
|
||||
fullscreenProperty.set(fullscreen);
|
||||
}
|
||||
|
||||
private final ImmediateIntegerProperty widthProperty = new ImmediateIntegerProperty(this, "width", 854);
|
||||
private final IntegerProperty widthProperty = new SimpleIntegerProperty(this, "width", 854);
|
||||
|
||||
public ImmediateIntegerProperty widthProperty() {
|
||||
public IntegerProperty widthProperty() {
|
||||
return widthProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -355,15 +470,15 @@ public final class VersionSetting {
|
||||
}
|
||||
|
||||
|
||||
private final ImmediateIntegerProperty heightProperty = new ImmediateIntegerProperty(this, "height", 480);
|
||||
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
|
||||
|
||||
public ImmediateIntegerProperty heightProperty() {
|
||||
public IntegerProperty heightProperty() {
|
||||
return heightProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -380,26 +495,26 @@ public final class VersionSetting {
|
||||
* 0 - .minecraft<br/>
|
||||
* 1 - .minecraft/versions/<version>/<br/>
|
||||
*/
|
||||
private final ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty = new ImmediateObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
|
||||
private final ObjectProperty<GameDirectoryType> gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", GameDirectoryType.ROOT_FOLDER);
|
||||
|
||||
public ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty() {
|
||||
public ObjectProperty<GameDirectoryType> gameDirTypeProperty() {
|
||||
return gameDirTypeProperty;
|
||||
}
|
||||
|
||||
public EnumGameDirectory getGameDirType() {
|
||||
public GameDirectoryType getGameDirType() {
|
||||
return gameDirTypeProperty.get();
|
||||
}
|
||||
|
||||
public void setGameDirType(EnumGameDirectory gameDirType) {
|
||||
public void setGameDirType(GameDirectoryType gameDirType) {
|
||||
gameDirTypeProperty.set(gameDirType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Your custom gameDir
|
||||
*/
|
||||
private final ImmediateStringProperty gameDirProperty = new ImmediateStringProperty(this, "gameDir", "");
|
||||
private final StringProperty gameDirProperty = new SimpleStringProperty(this, "gameDir", "");
|
||||
|
||||
public ImmediateStringProperty gameDirProperty() {
|
||||
public StringProperty gameDirProperty() {
|
||||
return gameDirProperty;
|
||||
}
|
||||
|
||||
@@ -411,6 +526,48 @@ public final class VersionSetting {
|
||||
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 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);
|
||||
}
|
||||
|
||||
// launcher settings
|
||||
|
||||
/**
|
||||
@@ -418,9 +575,9 @@ public final class VersionSetting {
|
||||
* 1 - Hide the launcher when the game starts.<br/>
|
||||
* 2 - Keep the launcher open.<br/>
|
||||
*/
|
||||
private final ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty = new ImmediateObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE);
|
||||
private final ObjectProperty<LauncherVisibility> launcherVisibilityProperty = new SimpleObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE);
|
||||
|
||||
public ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty() {
|
||||
public ObjectProperty<LauncherVisibility> launcherVisibilityProperty() {
|
||||
return launcherVisibilityProperty;
|
||||
}
|
||||
|
||||
@@ -432,25 +589,55 @@ public final class VersionSetting {
|
||||
launcherVisibilityProperty.set(launcherVisibility);
|
||||
}
|
||||
|
||||
public JavaVersion getJavaVersion() 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 ("Custom".equals(getJava())) {
|
||||
try {
|
||||
return JavaVersion.fromExecutable(new File(getJavaDir()));
|
||||
} catch (IOException e) {
|
||||
return null; // Custom Java Directory not found,
|
||||
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version) {
|
||||
return getJavaVersion(gameVersion, version, true);
|
||||
}
|
||||
|
||||
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version, boolean checkJava) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
if (StringUtils.isBlank(getJava())) {
|
||||
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
|
||||
}
|
||||
} else if (StringUtils.isNotBlank(getJava())) {
|
||||
JavaVersion c = JavaVersion.getJREs().get(getJava());
|
||||
if (c == null) {
|
||||
setJava("Default");
|
||||
return JavaVersion.fromCurrentEnvironment();
|
||||
} else
|
||||
return c;
|
||||
} else throw new Error();
|
||||
}).thenSupplyAsync(() -> {
|
||||
try {
|
||||
if ("Default".equals(getJava())) {
|
||||
return JavaVersion.fromCurrentEnvironment();
|
||||
} else if (isJavaAutoSelected()) {
|
||||
return JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version);
|
||||
} else if (isUsesCustomJavaDir()) {
|
||||
try {
|
||||
if (checkJava)
|
||||
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
||||
else
|
||||
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN));
|
||||
} 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()) {
|
||||
runInFX(() -> {
|
||||
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();
|
||||
} catch (InterruptedException e) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setJavaVersion(JavaVersion java) {
|
||||
setJava(java.getVersion());
|
||||
setDefaultJavaPath(java.getBinary().toString());
|
||||
}
|
||||
|
||||
public void addPropertyChangedListener(InvalidationListener listener) {
|
||||
@@ -461,12 +648,14 @@ public final class VersionSetting {
|
||||
permSizeProperty.addListener(listener);
|
||||
maxMemoryProperty.addListener(listener);
|
||||
minMemoryProperty.addListener(listener);
|
||||
autoMemory.addListener(listener);
|
||||
preLaunchCommandProperty.addListener(listener);
|
||||
postExitCommand.addListener(listener);
|
||||
javaArgsProperty.addListener(listener);
|
||||
minecraftArgsProperty.addListener(listener);
|
||||
noJVMArgsProperty.addListener(listener);
|
||||
notCheckGameProperty.addListener(listener);
|
||||
noCommonProperty.addListener(listener);
|
||||
notCheckJVMProperty.addListener(listener);
|
||||
showLogsProperty.addListener(listener);
|
||||
serverIpProperty.addListener(listener);
|
||||
fullscreenProperty.addListener(listener);
|
||||
@@ -474,41 +663,50 @@ public final class VersionSetting {
|
||||
heightProperty.addListener(listener);
|
||||
gameDirTypeProperty.addListener(listener);
|
||||
gameDirProperty.addListener(listener);
|
||||
processPriorityProperty.addListener(listener);
|
||||
useNativeGLFW.addListener(listener);
|
||||
useNativeOpenAL.addListener(listener);
|
||||
launcherVisibilityProperty.addListener(listener);
|
||||
defaultJavaPathProperty.addListener(listener);
|
||||
nativesDirProperty.addListener(listener);
|
||||
nativesDirTypeProperty.addListener(listener);
|
||||
}
|
||||
|
||||
public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException {
|
||||
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion()).orElse(JavaVersion.fromCurrentEnvironment());
|
||||
return new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionName(Main.TITLE)
|
||||
.setProfileName(Main.TITLE)
|
||||
.setMinecraftArgs(getMinecraftArgs())
|
||||
.setJavaArgs(getJavaArgs())
|
||||
.setMaxMemory(getMaxMemory())
|
||||
.setMinMemory(getMinMemory())
|
||||
.setMetaspace(Lang.toIntOrNull(getPermSize()))
|
||||
.setWidth(getWidth())
|
||||
.setHeight(getHeight())
|
||||
.setFullscreen(isFullscreen())
|
||||
.setServerIp(getServerIp())
|
||||
.setWrapper(getWrapper())
|
||||
.setProxyHost(Settings.INSTANCE.getProxyHost())
|
||||
.setProxyPort(Settings.INSTANCE.getProxyPort())
|
||||
.setProxyUser(Settings.INSTANCE.getProxyUser())
|
||||
.setProxyPass(Settings.INSTANCE.getProxyPass())
|
||||
.setPrecalledCommand(getPreLaunchCommand())
|
||||
.setNoGeneratedJVMArgs(isNoJVMArgs())
|
||||
.create();
|
||||
@Override
|
||||
public VersionSetting clone() {
|
||||
VersionSetting versionSetting = new VersionSetting();
|
||||
versionSetting.setUsesGlobal(isUsesGlobal());
|
||||
versionSetting.setJava(getJava());
|
||||
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.setNoJVMArgs(isNoJVMArgs());
|
||||
versionSetting.setNotCheckGame(isNotCheckGame());
|
||||
versionSetting.setNotCheckJVM(isNotCheckJVM());
|
||||
versionSetting.setShowLogs(isShowLogs());
|
||||
versionSetting.setServerIp(getServerIp());
|
||||
versionSetting.setFullscreen(isFullscreen());
|
||||
versionSetting.setWidth(getWidth());
|
||||
versionSetting.setHeight(getHeight());
|
||||
versionSetting.setGameDirType(getGameDirType());
|
||||
versionSetting.setGameDir(getGameDir());
|
||||
versionSetting.setProcessPriority(getProcessPriority());
|
||||
versionSetting.setUseNativeGLFW(isUseNativeGLFW());
|
||||
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
|
||||
versionSetting.setLauncherVisibility(getLauncherVisibility());
|
||||
versionSetting.setNativesDir(getNativesDir());
|
||||
return versionSetting;
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null) return JsonNull.INSTANCE;
|
||||
@@ -519,29 +717,37 @@ public final class VersionSetting {
|
||||
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
|
||||
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("noCommon", src.isNoCommon());
|
||||
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
|
||||
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());
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject))
|
||||
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject))
|
||||
return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
@@ -555,6 +761,7 @@ public final class VersionSetting {
|
||||
vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).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));
|
||||
@@ -564,13 +771,19 @@ public final class VersionSetting {
|
||||
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.setNoCommon(Optional.ofNullable(obj.get("noCommon")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).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(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(LauncherVisibility.HIDE.ordinal())]);
|
||||
vs.setProcessPriority(ProcessPriority.values()[Optional.ofNullable(obj.get("processPriority")).map(JsonElement::getAsInt).orElse(ProcessPriority.NORMAL.ordinal())]);
|
||||
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(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(GameDirectoryType.ROOT_FOLDER.ordinal())]);
|
||||
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
|
||||
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
|
||||
return vs;
|
||||
}
|
||||
@@ -580,7 +793,9 @@ public final class VersionSetting {
|
||||
}
|
||||
|
||||
private int parseJsonPrimitive(JsonPrimitive primitive, int defaultValue) {
|
||||
if (primitive.isNumber())
|
||||
if (primitive == null)
|
||||
return defaultValue;
|
||||
else if (primitive.isNumber())
|
||||
return primitive.getAsInt();
|
||||
else
|
||||
return Lang.parseInt(primitive.getAsString(), defaultValue);
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.AccountHelper;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
|
||||
public final class AccountItem extends StackPane {
|
||||
|
||||
private final Account account;
|
||||
|
||||
@FXML
|
||||
private Pane icon;
|
||||
@FXML private VBox content;
|
||||
@FXML private StackPane header;
|
||||
@FXML private StackPane body;
|
||||
@FXML private JFXButton btnDelete;
|
||||
@FXML private JFXButton btnRefresh;
|
||||
@FXML private Label lblUser;
|
||||
@FXML private Label lblType;
|
||||
@FXML private Label lblEmail;
|
||||
@FXML private Label lblCurrentAccount;
|
||||
@FXML private JFXRadioButton chkSelected;
|
||||
@FXML private JFXProgressBar pgsSkin;
|
||||
@FXML private ImageView portraitView;
|
||||
@FXML private HBox buttonPane;
|
||||
|
||||
public AccountItem(int i, Account account, ToggleGroup toggleGroup) {
|
||||
this.account = account;
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/account-item.fxml");
|
||||
|
||||
FXUtils.limitWidth(this, 160);
|
||||
FXUtils.limitHeight(this, 156);
|
||||
|
||||
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -0.5, 1.0));
|
||||
|
||||
chkSelected.setToggleGroup(toggleGroup);
|
||||
btnDelete.setGraphic(SVG.delete("black", 15, 15));
|
||||
btnRefresh.setGraphic(SVG.refresh("black", 15, 15));
|
||||
|
||||
// create content
|
||||
String headerColor = getDefaultColor(i % 12);
|
||||
header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor);
|
||||
|
||||
// create image view
|
||||
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 32.0, header.boundsInParentProperty(), icon.heightProperty()));
|
||||
|
||||
chkSelected.getProperties().put("account", account);
|
||||
setSelected(Settings.INSTANCE.getSelectedAccount() == account);
|
||||
|
||||
lblUser.setText(Accounts.getCurrentCharacter(account));
|
||||
lblType.setText(AccountsPage.accountType(account));
|
||||
lblEmail.setText(account.getUsername());
|
||||
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
btnRefresh.setOnMouseClicked(e -> {
|
||||
pgsSkin.setVisible(true);
|
||||
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
|
||||
.subscribe(Schedulers.javafx(), this::loadSkin);
|
||||
});
|
||||
AccountHelper.loadSkinAsync((YggdrasilAccount) account)
|
||||
.subscribe(Schedulers.javafx(), this::loadSkin);
|
||||
} else
|
||||
loadSkin();
|
||||
|
||||
if (account instanceof OfflineAccount) { // Offline Account cannot be refreshed,
|
||||
buttonPane.getChildren().remove(btnRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSkin() {
|
||||
pgsSkin.setVisible(false);
|
||||
portraitView.setViewport(AccountHelper.getViewport(4));
|
||||
if (account instanceof YggdrasilAccount)
|
||||
portraitView.setImage(AccountHelper.getSkin((YggdrasilAccount) account, 4));
|
||||
else
|
||||
portraitView.setImage(AccountHelper.getDefaultSkin(account, 4));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
}
|
||||
|
||||
private String getDefaultColor(int i) {
|
||||
switch (i) {
|
||||
case 0: return "#8F3F7E";
|
||||
case 1: return "#B5305F";
|
||||
case 2: return "#CE584A";
|
||||
case 3: return "#DB8D5C";
|
||||
case 4: return "#DA854E";
|
||||
case 5: return "#E9AB44";
|
||||
case 6: return "#FEE435";
|
||||
case 7: return "#99C286";
|
||||
case 8: return "#01A05E";
|
||||
case 9: return "#4A8895";
|
||||
case 10: return "#16669B";
|
||||
case 11: return "#2F65A5";
|
||||
case 12: return "#4E6A9C";
|
||||
default: return "#FFFFFF";
|
||||
}
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
lblCurrentAccount.setVisible(selected);
|
||||
chkSelected.setSelected(selected);
|
||||
}
|
||||
|
||||
public void setOnDeleteButtonMouseClicked(EventHandler<? super MouseEvent> eventHandler) {
|
||||
btnDelete.setOnMouseClicked(eventHandler);
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.InvalidCredentialsException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.game.AccountHelper;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedItem;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
public final class AccountsPage extends StackPane implements DecoratorPage {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("account"));
|
||||
|
||||
@FXML
|
||||
private ScrollPane scrollPane;
|
||||
@FXML private JFXMasonryPane masonryPane;
|
||||
@FXML private JFXDialog dialog;
|
||||
@FXML private JFXTextField txtUsername;
|
||||
@FXML private JFXPasswordField txtPassword;
|
||||
@FXML private Label lblCreationWarning;
|
||||
@FXML private JFXComboBox<String> cboType;
|
||||
@FXML private JFXProgressBar progressBar;
|
||||
|
||||
{
|
||||
FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
|
||||
|
||||
getChildren().remove(dialog);
|
||||
dialog.setDialogContainer(this);
|
||||
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
FXUtils.setValidateWhileTextChanged(txtUsername);
|
||||
FXUtils.setValidateWhileTextChanged(txtPassword);
|
||||
|
||||
cboType.getItems().setAll(Main.i18n("account.methods.offline"), Main.i18n("account.methods.yggdrasil"));
|
||||
cboType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> {
|
||||
txtPassword.setVisible(newValue.intValue() != 0);
|
||||
});
|
||||
cboType.getSelectionModel().select(0);
|
||||
|
||||
txtPassword.setOnAction(e -> onCreationAccept());
|
||||
txtUsername.setOnAction(e -> onCreationAccept());
|
||||
txtUsername.getValidators().add(new Validator(Main.i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
|
||||
|
||||
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), account -> {
|
||||
for (Node node : masonryPane.getChildren())
|
||||
if (node instanceof AccountItem)
|
||||
((AccountItem) node).setSelected(account == ((AccountItem) node).getAccount());
|
||||
});
|
||||
|
||||
loadAccounts();
|
||||
|
||||
if (Settings.INSTANCE.getAccounts().isEmpty())
|
||||
addNewAccount();
|
||||
}
|
||||
|
||||
public void loadAccounts() {
|
||||
List<Node> children = new LinkedList<>();
|
||||
int i = 0;
|
||||
ToggleGroup group = new ToggleGroup();
|
||||
for (Account account : Settings.INSTANCE.getAccounts()) {
|
||||
children.add(buildNode(++i, account, group));
|
||||
}
|
||||
group.selectedToggleProperty().addListener((a, b, newValue) -> {
|
||||
if (newValue != null)
|
||||
Settings.INSTANCE.setSelectedAccount((Account) newValue.getProperties().get("account"));
|
||||
});
|
||||
FXUtils.resetChildren(masonryPane, children);
|
||||
Platform.runLater(() -> {
|
||||
masonryPane.requestLayout();
|
||||
scrollPane.requestLayout();
|
||||
});
|
||||
}
|
||||
|
||||
private Node buildNode(int i, Account account, ToggleGroup group) {
|
||||
AccountItem item = new AccountItem(i, account, group);
|
||||
item.setOnDeleteButtonMouseClicked(e -> {
|
||||
Settings.INSTANCE.deleteAccount(account);
|
||||
Platform.runLater(this::loadAccounts);
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void addNewAccount() {
|
||||
txtUsername.setText("");
|
||||
txtPassword.setText("");
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onCreationAccept() {
|
||||
int type = cboType.getSelectionModel().getSelectedIndex();
|
||||
String username = txtUsername.getText();
|
||||
String password = txtPassword.getText();
|
||||
progressBar.setVisible(true);
|
||||
lblCreationWarning.setText("");
|
||||
Task.ofResult("create_account", () -> {
|
||||
try {
|
||||
Account account;
|
||||
switch (type) {
|
||||
case 0: account = OfflineAccountFactory.INSTANCE.fromUsername(username); break;
|
||||
case 1: account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password); break;
|
||||
default: throw new Error();
|
||||
}
|
||||
|
||||
AuthInfo info = account.logIn(new CharacterSelector(), Settings.INSTANCE.getProxy());
|
||||
Accounts.setCurrentCharacter(account, info.getUsername());
|
||||
return account;
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
}).subscribe(Schedulers.javafx(), variables -> {
|
||||
Object account = variables.get("create_account");
|
||||
if (account instanceof Account) {
|
||||
Settings.INSTANCE.addAccount((Account) account);
|
||||
dialog.close();
|
||||
loadAccounts();
|
||||
} else if (account instanceof InvalidCredentialsException) {
|
||||
lblCreationWarning.setText(Main.i18n("account.failed.wrong_password"));
|
||||
} else if (account instanceof NoCharacterException) {
|
||||
lblCreationWarning.setText(Main.i18n("account.failed.no_charactor"));
|
||||
} else if (account instanceof ServerDisconnectException) {
|
||||
lblCreationWarning.setText(Main.i18n("account.failed.connect_authentication_server"));
|
||||
} else if (account instanceof InvalidTokenException) {
|
||||
lblCreationWarning.setText(Main.i18n("account.failed.invalid_token"));
|
||||
} else if (account instanceof InvalidPasswordException) {
|
||||
lblCreationWarning.setText(Main.i18n("account.failed.invalid_password"));
|
||||
} else if (account instanceof NoSelectedCharacterException) {
|
||||
dialog.close();
|
||||
} else if (account instanceof Exception) {
|
||||
lblCreationWarning.setText(account.getClass() + ": " + ((Exception) account).getLocalizedMessage());
|
||||
}
|
||||
progressBar.setVisible(false);
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onCreationCancel() {
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public static String accountType(Account account) {
|
||||
if (account instanceof OfflineAccount) return Main.i18n("account.methods.offline");
|
||||
else if (account instanceof YggdrasilAccount) return Main.i18n("account.methods.yggdrasil");
|
||||
else throw new Error(Main.i18n("account.methods.no_method") + ": " + account);
|
||||
}
|
||||
|
||||
class CharacterSelector extends BorderPane implements MultiCharacterSelector {
|
||||
private AdvancedListBox listBox = new AdvancedListBox();
|
||||
private JFXButton cancel = new JFXButton();
|
||||
|
||||
private CountDownLatch latch = new CountDownLatch(1);
|
||||
private GameProfile selectedProfile = null;
|
||||
|
||||
{
|
||||
setStyle("-fx-padding: 8px;");
|
||||
|
||||
cancel.setText(Main.i18n("button.cancel"));
|
||||
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
|
||||
cancel.setOnMouseClicked(e -> latch.countDown());
|
||||
|
||||
listBox.startCategory(Main.i18n("account.choose"));
|
||||
|
||||
setCenter(listBox);
|
||||
|
||||
HBox hbox = new HBox();
|
||||
hbox.setAlignment(Pos.CENTER_RIGHT);
|
||||
hbox.getChildren().add(cancel);
|
||||
setBottom(hbox);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
|
||||
if (!(account instanceof YggdrasilAccount))
|
||||
return MultiCharacterSelector.DEFAULT.select(account, names);
|
||||
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
|
||||
|
||||
for (GameProfile profile : names) {
|
||||
Image image;
|
||||
try {
|
||||
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4, Settings.INSTANCE.getProxy());
|
||||
} catch (Exception e) {
|
||||
image = FXUtils.DEFAULT_ICON;
|
||||
}
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
if (image == FXUtils.DEFAULT_ICON)
|
||||
portraitView.setImage(FXUtils.DEFAULT_ICON);
|
||||
else {
|
||||
portraitView.setImage(image);
|
||||
portraitView.setViewport(AccountHelper.getViewport(4));
|
||||
}
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
});
|
||||
listBox.add(accountItem);
|
||||
}
|
||||
|
||||
JFXUtilities.runInFX(() -> Controllers.dialog(this));
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
|
||||
JFXUtilities.runInFX(Controllers::closeDialog);
|
||||
|
||||
if (selectedProfile == null)
|
||||
throw new NoSelectedCharacterException(account);
|
||||
|
||||
return selectedProfile;
|
||||
} catch (InterruptedException ignore) {
|
||||
throw new NoSelectedCharacterException(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
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");
|
||||
Object taskBar = getTaskBarMethod.invoke(null);
|
||||
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
|
||||
setIconImageMethod.invoke(taskBar, image);
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,37 +13,89 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXDialog;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
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.Accounts;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.JavaVersion;
|
||||
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.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.multiplayer.MultiplayerPage;
|
||||
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.Lazy;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
|
||||
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.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Controllers {
|
||||
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
|
||||
|
||||
private static Scene scene;
|
||||
private static Stage stage;
|
||||
private static MainPage mainPage = new MainPage();
|
||||
private static SettingsPage settingsPage = null;
|
||||
private static VersionPage versionPage = null;
|
||||
private static LeftPaneController leftPaneController;
|
||||
private static Decorator decorator;
|
||||
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, it -> "zip".equals(FileUtils.getExtension(it)), 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.accountsProperty());
|
||||
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
|
||||
return accountListPage;
|
||||
});
|
||||
private static Lazy<MultiplayerPage> multiplayerPage = new Lazy<>(MultiplayerPage::new);
|
||||
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
|
||||
|
||||
private Controllers() {
|
||||
}
|
||||
|
||||
public static Scene getScene() {
|
||||
return scene;
|
||||
@@ -54,62 +106,112 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static SettingsPage getSettingsPage() {
|
||||
if (settingsPage == null)
|
||||
settingsPage = new SettingsPage();
|
||||
return settingsPage;
|
||||
public static VersionPage getVersionPage() {
|
||||
return versionPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static VersionPage getVersionPage() {
|
||||
if (versionPage == null)
|
||||
versionPage = new VersionPage();
|
||||
return versionPage;
|
||||
public static GameListPage getGameListPage() {
|
||||
return gameListPage.get();
|
||||
}
|
||||
|
||||
public static Decorator getDecorator() {
|
||||
// FXThread
|
||||
public static RootPage getRootPage() {
|
||||
return rootPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static MultiplayerPage getMultiplayerPage() {
|
||||
return multiplayerPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static LauncherSettingsPage getSettingsPage() {
|
||||
return settingsPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static AccountListPage getAccountListPage() {
|
||||
return accountListPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static DownloadPage getDownloadPage() {
|
||||
return downloadPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static DecoratorController getDecorator() {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
public static MainPage getMainPage() {
|
||||
return mainPage;
|
||||
}
|
||||
|
||||
public static LeftPaneController getLeftPaneController() {
|
||||
return leftPaneController;
|
||||
public static void onApplicationStop() {
|
||||
config().setHeight(stageHeight.get());
|
||||
config().setWidth(stageWidth.get());
|
||||
stageHeight = null;
|
||||
stageWidth = null;
|
||||
}
|
||||
|
||||
public static void initialize(Stage stage) {
|
||||
Logging.LOG.info("Start initializing application");
|
||||
|
||||
Controllers.stage = stage;
|
||||
|
||||
decorator = new Decorator(stage, mainPage, Main.TITLE, false, true);
|
||||
decorator.showPage(null);
|
||||
leftPaneController = new LeftPaneController(decorator.getLeftPane());
|
||||
stage.setHeight(config().getHeight());
|
||||
stageHeight.bind(stage.heightProperty());
|
||||
stage.setWidth(config().getWidth());
|
||||
stageWidth.bind(stage.widthProperty());
|
||||
|
||||
Settings.INSTANCE.onProfileLoading();
|
||||
Task.of(JavaVersion::initialize).start();
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
decorator.setCustomMaximize(false);
|
||||
decorator = new DecoratorController(stage, getRootPage());
|
||||
|
||||
scene = new Scene(decorator, 804, 521);
|
||||
scene.getStylesheets().addAll(Settings.INSTANCE.getTheme().getStylesheets());
|
||||
stage.setMinWidth(804);
|
||||
stage.setMaxWidth(804);
|
||||
stage.setMinHeight(521);
|
||||
stage.setMaxHeight(521);
|
||||
if (config().getCommonDirType() == EnumCommonDirectory.CUSTOM &&
|
||||
!FileUtils.canCreateDirectory(config().getCommonDirectory())) {
|
||||
config().setCommonDirType(EnumCommonDirectory.DEFAULT);
|
||||
dialog(i18n("launcher.cache_directory.invalid"));
|
||||
}
|
||||
|
||||
stage.getIcons().add(new Image("/assets/img/icon.png"));
|
||||
stage.setTitle(Main.TITLE);
|
||||
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
|
||||
|
||||
scene = new Scene(decorator.getDecorator());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
stage.setMinHeight(450 + 2 + 40 + 16); // bg height + border width*2 + toolbar height + shadow width*2
|
||||
stage.setMinWidth(800 + 2 + 16); // bg width + border width*2 + shadow width*2
|
||||
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
|
||||
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
||||
scene.getStylesheets().setAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
|
||||
stage.getIcons().add(newImage("/assets/img/icon.png"));
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
|
||||
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.setOnAction(e -> FXUtils.openLink(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 -> {
|
||||
System.exit(1);
|
||||
});
|
||||
agreementPane.setActions(agreementLink, yesButton, noButton);
|
||||
Controllers.dialog(agreementPane);
|
||||
}
|
||||
}
|
||||
|
||||
public static Region getDialogContent() {
|
||||
return decorator.getDialog().getContent();
|
||||
}
|
||||
|
||||
public static JFXDialog dialog(Region content) {
|
||||
// TODO: temp fix
|
||||
decorator.showDialog(new Region());
|
||||
return decorator.showDialog(content);
|
||||
public static void dialog(Region content) {
|
||||
if (decorator != null)
|
||||
decorator.showDialog(content);
|
||||
}
|
||||
|
||||
public static void dialog(String text) {
|
||||
@@ -117,46 +219,101 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void dialog(String text, String title) {
|
||||
dialog(text, title, MessageBox.INFORMATION_MESSAGE);
|
||||
dialog(text, title, MessageType.INFO);
|
||||
}
|
||||
|
||||
public static void dialog(String text, String title, int type) {
|
||||
public static void dialog(String text, String title, MessageType type) {
|
||||
dialog(text, title, type, null);
|
||||
}
|
||||
|
||||
public static void dialog(String text, String title, int type, Runnable onAccept) {
|
||||
dialog(new MessageDialogPane(text, title, decorator.getDialog(), 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 confirmDialog(String text, String title, Runnable onAccept, Runnable onCancel) {
|
||||
dialog(new MessageDialogPane(text, title, decorator.getDialog(), onAccept, onCancel));
|
||||
public static void confirm(String text, String title, Runnable yes, Runnable no) {
|
||||
confirm(text, title, MessageType.QUESTION, yes, no);
|
||||
}
|
||||
|
||||
public static void inputDialog(String text, Consumer<String> onResult) {
|
||||
dialog(new InputDialogPane(text, decorator.getDialog(), onResult));
|
||||
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 taskDialog(TaskExecutor executor, String title, String subtitle, Runnable onCancel) {
|
||||
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) {
|
||||
return prompt(title, onResult, "");
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult, String initialValue) {
|
||||
InputDialogPane pane = new InputDialogPane(title, initialValue, onResult);
|
||||
dialog(pane);
|
||||
return pane.getCompletableFuture();
|
||||
}
|
||||
|
||||
public static CompletableFuture<List<PromptDialogPane.Builder.Question<?>>> prompt(PromptDialogPane.Builder builder) {
|
||||
PromptDialogPane pane = new PromptDialogPane(builder);
|
||||
dialog(pane);
|
||||
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) {
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
|
||||
pane.setTitle(title);
|
||||
pane.setSubtitle(subtitle);
|
||||
pane.setExecutor(executor);
|
||||
executor.start();
|
||||
dialog(pane);
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static void closeDialog() {
|
||||
decorator.getDialog().close();
|
||||
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, Consumer<Region> 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) {
|
||||
if (decorator.getNowPage() == node)
|
||||
decorator.showPage(null);
|
||||
else
|
||||
decorator.showPage(node);
|
||||
decorator.navigate(node);
|
||||
}
|
||||
|
||||
public static void showUpdate() {
|
||||
getDecorator().showUpdate();
|
||||
public static void showToast(String content) {
|
||||
decorator.showToast(content);
|
||||
}
|
||||
|
||||
public static void onHyperlinkAction(String href) {
|
||||
if (href.startsWith("hmcl://")) {
|
||||
if ("hmcl://settings/feedback".equals(href)) {
|
||||
Controllers.getSettingsPage().showFeedback();
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
}
|
||||
} else {
|
||||
FXUtils.openLink(href);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isStopped() {
|
||||
return decorator == null;
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
rootPage = null;
|
||||
versionPage = null;
|
||||
gameListPage = null;
|
||||
settingsPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
* 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
|
||||
@@ -13,7 +13,7 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
@@ -22,34 +22,36 @@ import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class CrashWindow extends Stage {
|
||||
|
||||
private Label lblCrash = new Label();
|
||||
private Button btnContact = new Button();
|
||||
private TextArea textArea = new TextArea();
|
||||
|
||||
public CrashWindow(String text) {
|
||||
if (Main.UPDATE_CHECKER.isOutOfDate())
|
||||
lblCrash.setText(Main.i18n("launcher.crash_out_dated"));
|
||||
Label lblCrash = new Label();
|
||||
if (UpdateChecker.isOutdated())
|
||||
lblCrash.setText(i18n("launcher.crash_out_dated"));
|
||||
else
|
||||
lblCrash.setText(Main.i18n("launcher.crash"));
|
||||
lblCrash.setText(i18n("launcher.crash"));
|
||||
lblCrash.setWrapText(true);
|
||||
|
||||
TextArea textArea = new TextArea();
|
||||
textArea.setText(text);
|
||||
textArea.setEditable(false);
|
||||
|
||||
btnContact.setText(Main.i18n("launcher.contact"));
|
||||
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Main.CONTACT));
|
||||
Button btnContact = new Button();
|
||||
btnContact.setText(i18n("launcher.contact"));
|
||||
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
HBox box = new HBox();
|
||||
box.setStyle("-fx-padding: 8px;");
|
||||
box.getChildren().add(btnContact);
|
||||
@@ -65,12 +67,10 @@ public class CrashWindow extends Stage {
|
||||
|
||||
Scene scene = new Scene(pane, 800, 480);
|
||||
setScene(scene);
|
||||
getIcons().add(new Image("/assets/img/icon.png"));
|
||||
setTitle(Main.i18n("message.error"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
setTitle(i18n("message.error"));
|
||||
|
||||
setOnCloseRequest(e -> {
|
||||
System.exit(1);
|
||||
});
|
||||
setOnCloseRequest(e -> System.exit(1));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,637 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialog;
|
||||
import com.jfoenix.controls.JFXDrawer;
|
||||
import com.jfoenix.controls.JFXHamburger;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.svg.SVGGlyph;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.BoundingBox;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogWizardDisplayer;
|
||||
import org.jackhuang.hmcl.ui.wizard.*;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public final class Decorator extends StackPane implements TaskExecutorDialogWizardDisplayer {
|
||||
private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE),
|
||||
glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); });
|
||||
private static final SVGGlyph resizeMax = Lang.apply(new SVGGlyph(0, "RESIZE_MAX", "M726 810v-596h-428v596h428zM726 44q34 0 59 25t25 59v768q0 34-25 60t-59 26h-428q-34 0-59-26t-25-60v-768q0-34 25-60t59-26z", Color.WHITE),
|
||||
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
|
||||
private static final SVGGlyph resizeMin = Lang.apply(new SVGGlyph(0, "RESIZE_MIN", "M80.842 943.158v-377.264h565.894v377.264h-565.894zM0 404.21v619.79h727.578v-619.79h-727.578zM377.264 161.684h565.894v377.264h-134.736v80.842h215.578v-619.79h-727.578v323.37h80.842v-161.686z", Color.WHITE),
|
||||
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
|
||||
private static final SVGGlyph close = Lang.apply(new SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE),
|
||||
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
|
||||
|
||||
private final ObjectProperty<Runnable> onCloseButtonAction = new SimpleObjectProperty<>(Main::stopApplication);
|
||||
private final BooleanProperty customMaximize = new SimpleBooleanProperty(false);
|
||||
|
||||
private final Stage primaryStage;
|
||||
private final Node mainPage;
|
||||
private final boolean max, min;
|
||||
private final WizardController wizardController = new WizardController(this);
|
||||
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private double xOffset, yOffset, newX, newY, initX, initY;
|
||||
private boolean allowMove, isDragging, dialogShown, maximized;
|
||||
private BoundingBox originalBox, maximizedBox;
|
||||
private TransitionHandler animationHandler;
|
||||
|
||||
@FXML
|
||||
private StackPane contentPlaceHolder;
|
||||
@FXML
|
||||
private StackPane drawerWrapper;
|
||||
@FXML
|
||||
private BorderPane titleContainer;
|
||||
@FXML
|
||||
private BorderPane leftRootPane;
|
||||
@FXML
|
||||
private HBox buttonsContainer;
|
||||
@FXML
|
||||
private JFXButton backNavButton;
|
||||
@FXML
|
||||
private JFXButton refreshNavButton;
|
||||
@FXML
|
||||
private JFXButton closeNavButton;
|
||||
@FXML
|
||||
private JFXButton refreshMenuButton;
|
||||
@FXML
|
||||
private Label titleLabel;
|
||||
@FXML
|
||||
private Label lblTitle;
|
||||
@FXML
|
||||
private AdvancedListBox leftPane;
|
||||
@FXML
|
||||
private JFXDrawer drawer;
|
||||
@FXML
|
||||
private StackPane titleBurgerContainer;
|
||||
@FXML
|
||||
private JFXHamburger titleBurger;
|
||||
@FXML
|
||||
private JFXDialog dialog;
|
||||
@FXML
|
||||
private JFXButton btnMin;
|
||||
@FXML
|
||||
private JFXButton btnMax;
|
||||
@FXML
|
||||
private JFXButton btnClose;
|
||||
@FXML
|
||||
private HBox updatePane;
|
||||
@FXML
|
||||
private HBox navLeft;
|
||||
|
||||
public Decorator(Stage primaryStage, Node mainPage, String title) {
|
||||
this(primaryStage, mainPage, title, true, true);
|
||||
}
|
||||
|
||||
public Decorator(Stage primaryStage, Node mainPage, String title, boolean max, boolean min) {
|
||||
this.primaryStage = primaryStage;
|
||||
this.mainPage = mainPage;
|
||||
this.max = max;
|
||||
this.min = min;
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/decorator.fxml");
|
||||
|
||||
updatePane.setCursor(Cursor.HAND);
|
||||
updatePane.setOnMouseClicked(event -> Main.UPDATE_CHECKER.checkOutdate());
|
||||
|
||||
primaryStage.initStyle(StageStyle.UNDECORATED);
|
||||
btnClose.setGraphic(close);
|
||||
btnMin.setGraphic(minus);
|
||||
btnMax.setGraphic(resizeMax);
|
||||
|
||||
lblTitle.setText(title);
|
||||
|
||||
buttonsContainer.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
titleContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getClickCount() == 2)
|
||||
btnMax.fire();
|
||||
});
|
||||
|
||||
dialog.setDialogContainer(drawerWrapper);
|
||||
dialog.setOnDialogClosed(e -> dialogShown = false);
|
||||
dialog.setOnDialogOpened(e -> dialogShown = true);
|
||||
|
||||
if (!min) buttonsContainer.getChildren().remove(btnMin);
|
||||
if (!max) buttonsContainer.getChildren().remove(btnMax);
|
||||
|
||||
//JFXDepthManager.setDepth(titleContainer, 1);
|
||||
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true);
|
||||
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
|
||||
if (!isDragging) allowMove = false;
|
||||
});
|
||||
Rectangle rectangle = new Rectangle(0, 0, 0, 0);
|
||||
rectangle.widthProperty().bind(titleContainer.widthProperty());
|
||||
rectangle.heightProperty().bind(Bindings.createDoubleBinding(() -> titleContainer.getHeight() + 100, titleContainer.heightProperty()));
|
||||
titleContainer.setClip(rectangle);
|
||||
|
||||
animationHandler = new TransitionHandler(contentPlaceHolder);
|
||||
|
||||
FXUtils.setOverflowHidden((Pane) lookup("#contentPlaceHolderRoot"));
|
||||
FXUtils.setOverflowHidden(drawerWrapper);
|
||||
|
||||
loadBackground();
|
||||
}
|
||||
|
||||
private void loadBackground() {
|
||||
Image background;
|
||||
|
||||
if (Settings.INSTANCE.getBackgroundImageType() == EnumBackgroundImage.DEFAULT)
|
||||
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), "");
|
||||
else
|
||||
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), Settings.INSTANCE.getBackgroundImage());
|
||||
|
||||
drawerWrapper.setBackground(new Background(new BackgroundImage(background, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true))));
|
||||
}
|
||||
|
||||
private static Image searchBackgroundImage(Image def, String customPath) {
|
||||
Random random = new Random();
|
||||
boolean loaded = false;
|
||||
Image background = def;
|
||||
|
||||
// custom path
|
||||
if (StringUtils.isNotBlank(customPath)) {
|
||||
try {
|
||||
background = new Image("file:" + customPath);
|
||||
loaded = true;
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
// bgskin
|
||||
if (!loaded) {
|
||||
File backgroundImageFile = new File("bg");
|
||||
if (backgroundImageFile.isDirectory()) {
|
||||
File[] backgroundPath = backgroundImageFile.listFiles(file -> StringUtils.containsOne(FileUtils.getExtension(file), "png", "jpg"));
|
||||
if (backgroundPath != null && backgroundPath.length > 0) {
|
||||
int index = random.nextInt(backgroundPath.length);
|
||||
try {
|
||||
background = new Image("file:" + backgroundPath[index].getAbsolutePath());
|
||||
loaded = true;
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// background.png
|
||||
if (!loaded) {
|
||||
File backgroundImageFile = new File("background.png");
|
||||
if (backgroundImageFile.exists()) {
|
||||
try {
|
||||
background = new Image("file:" + backgroundImageFile.getAbsolutePath());
|
||||
loaded = true;
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// background.jpg
|
||||
if (!loaded) {
|
||||
File backgroundImageFile = new File("background.jpg");
|
||||
if (backgroundImageFile.exists()) {
|
||||
try {
|
||||
background = new Image("file:" + backgroundImageFile.getAbsolutePath());
|
||||
loaded = true;
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return background;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onMouseMoved(MouseEvent mouseEvent) {
|
||||
if (!primaryStage.isMaximized() && !primaryStage.isFullScreen() && !maximized) {
|
||||
if (!primaryStage.isResizable())
|
||||
updateInitMouseValues(mouseEvent);
|
||||
else {
|
||||
double x = mouseEvent.getX(), y = mouseEvent.getY();
|
||||
Bounds boundsInParent = getBoundsInParent();
|
||||
if (getBorder() != null && getBorder().getStrokes().size() > 0) {
|
||||
double borderWidth = this.contentPlaceHolder.snappedLeftInset();
|
||||
if (this.isRightEdge(x, y, boundsInParent)) {
|
||||
if (y < borderWidth) {
|
||||
setCursor(Cursor.NE_RESIZE);
|
||||
} else if (y > this.getHeight() - borderWidth) {
|
||||
setCursor(Cursor.SE_RESIZE);
|
||||
} else {
|
||||
setCursor(Cursor.E_RESIZE);
|
||||
}
|
||||
} else if (this.isLeftEdge(x, y, boundsInParent)) {
|
||||
if (y < borderWidth) {
|
||||
setCursor(Cursor.NW_RESIZE);
|
||||
} else if (y > this.getHeight() - borderWidth) {
|
||||
setCursor(Cursor.SW_RESIZE);
|
||||
} else {
|
||||
setCursor(Cursor.W_RESIZE);
|
||||
}
|
||||
} else if (this.isTopEdge(x, y, boundsInParent)) {
|
||||
setCursor(Cursor.N_RESIZE);
|
||||
} else if (this.isBottomEdge(x, y, boundsInParent)) {
|
||||
setCursor(Cursor.S_RESIZE);
|
||||
} else {
|
||||
setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
|
||||
this.updateInitMouseValues(mouseEvent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onMouseReleased() {
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onMouseDragged(MouseEvent mouseEvent) {
|
||||
this.isDragging = true;
|
||||
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
|
||||
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !this.primaryStage.isMaximized() && !this.maximized) {
|
||||
this.newX = mouseEvent.getScreenX();
|
||||
this.newY = mouseEvent.getScreenY();
|
||||
double deltax = this.newX - this.initX;
|
||||
double deltay = this.newY - this.initY;
|
||||
Cursor cursor = this.getCursor();
|
||||
if (Cursor.E_RESIZE == cursor) {
|
||||
this.setStageWidth(this.primaryStage.getWidth() + deltax);
|
||||
mouseEvent.consume();
|
||||
} else if (Cursor.NE_RESIZE == cursor) {
|
||||
if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) {
|
||||
this.primaryStage.setY(this.primaryStage.getY() + deltay);
|
||||
}
|
||||
|
||||
this.setStageWidth(this.primaryStage.getWidth() + deltax);
|
||||
mouseEvent.consume();
|
||||
} else if (Cursor.SE_RESIZE == cursor) {
|
||||
this.setStageWidth(this.primaryStage.getWidth() + deltax);
|
||||
this.setStageHeight(this.primaryStage.getHeight() + deltay);
|
||||
mouseEvent.consume();
|
||||
} else if (Cursor.S_RESIZE == cursor) {
|
||||
this.setStageHeight(this.primaryStage.getHeight() + deltay);
|
||||
mouseEvent.consume();
|
||||
} else if (Cursor.W_RESIZE == cursor) {
|
||||
if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) {
|
||||
this.primaryStage.setX(this.primaryStage.getX() + deltax);
|
||||
}
|
||||
|
||||
mouseEvent.consume();
|
||||
} else if (Cursor.SW_RESIZE == cursor) {
|
||||
if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) {
|
||||
this.primaryStage.setX(this.primaryStage.getX() + deltax);
|
||||
}
|
||||
|
||||
this.setStageHeight(this.primaryStage.getHeight() + deltay);
|
||||
mouseEvent.consume();
|
||||
} else if (Cursor.NW_RESIZE == cursor) {
|
||||
if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) {
|
||||
this.primaryStage.setX(this.primaryStage.getX() + deltax);
|
||||
}
|
||||
|
||||
if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) {
|
||||
this.primaryStage.setY(this.primaryStage.getY() + deltay);
|
||||
}
|
||||
|
||||
mouseEvent.consume();
|
||||
} else if (Cursor.N_RESIZE == cursor) {
|
||||
if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) {
|
||||
this.primaryStage.setY(this.primaryStage.getY() + deltay);
|
||||
}
|
||||
|
||||
mouseEvent.consume();
|
||||
} else if (this.allowMove) {
|
||||
this.primaryStage.setX(mouseEvent.getScreenX() - this.xOffset);
|
||||
this.primaryStage.setY(mouseEvent.getScreenY() - this.yOffset);
|
||||
mouseEvent.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onMin() {
|
||||
primaryStage.setIconified(true);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onMax() {
|
||||
if (!max) return;
|
||||
if (!this.isCustomMaximize()) {
|
||||
this.primaryStage.setMaximized(!this.primaryStage.isMaximized());
|
||||
this.maximized = this.primaryStage.isMaximized();
|
||||
if (this.primaryStage.isMaximized()) {
|
||||
this.btnMax.setGraphic(resizeMin);
|
||||
this.btnMax.setTooltip(new Tooltip("Restore Down"));
|
||||
} else {
|
||||
this.btnMax.setGraphic(resizeMax);
|
||||
this.btnMax.setTooltip(new Tooltip("Maximize"));
|
||||
}
|
||||
} else {
|
||||
if (!this.maximized) {
|
||||
this.originalBox = new BoundingBox(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight());
|
||||
Screen screen = Screen.getScreensForRectangle(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight()).get(0);
|
||||
Rectangle2D bounds = screen.getVisualBounds();
|
||||
this.maximizedBox = new BoundingBox(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
|
||||
primaryStage.setX(this.maximizedBox.getMinX());
|
||||
primaryStage.setY(this.maximizedBox.getMinY());
|
||||
primaryStage.setWidth(this.maximizedBox.getWidth());
|
||||
primaryStage.setHeight(this.maximizedBox.getHeight());
|
||||
this.btnMax.setGraphic(resizeMin);
|
||||
this.btnMax.setTooltip(new Tooltip("Restore Down"));
|
||||
} else {
|
||||
primaryStage.setX(this.originalBox.getMinX());
|
||||
primaryStage.setY(this.originalBox.getMinY());
|
||||
primaryStage.setWidth(this.originalBox.getWidth());
|
||||
primaryStage.setHeight(this.originalBox.getHeight());
|
||||
this.originalBox = null;
|
||||
this.btnMax.setGraphic(resizeMax);
|
||||
this.btnMax.setTooltip(new Tooltip("Maximize"));
|
||||
}
|
||||
|
||||
this.maximized = !this.maximized;
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onClose() {
|
||||
onCloseButtonAction.get().run();
|
||||
}
|
||||
|
||||
private void updateInitMouseValues(MouseEvent mouseEvent) {
|
||||
initX = mouseEvent.getScreenX();
|
||||
initY = mouseEvent.getScreenY();
|
||||
xOffset = mouseEvent.getSceneX();
|
||||
yOffset = mouseEvent.getSceneY();
|
||||
}
|
||||
|
||||
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
|
||||
return x < getWidth() && x > getWidth() - contentPlaceHolder.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean isTopEdge(double x, double y, Bounds boundsInParent) {
|
||||
return y >= 0 && y < contentPlaceHolder.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
|
||||
return y < getHeight() && y > getHeight() - contentPlaceHolder.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
|
||||
return x >= 0 && x < contentPlaceHolder.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean setStageWidth(double width) {
|
||||
if (width >= primaryStage.getMinWidth() && width >= titleContainer.getMinWidth()) {
|
||||
primaryStage.setWidth(width);
|
||||
initX = newX;
|
||||
return true;
|
||||
} else {
|
||||
if (width >= primaryStage.getMinWidth() && width <= titleContainer.getMinWidth())
|
||||
primaryStage.setWidth(titleContainer.getMinWidth());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setStageHeight(double height) {
|
||||
if (height >= primaryStage.getMinHeight() && height >= titleContainer.getHeight()) {
|
||||
primaryStage.setHeight(height);
|
||||
initY = newY;
|
||||
return true;
|
||||
} else {
|
||||
if (height >= primaryStage.getMinHeight() && height <= titleContainer.getHeight())
|
||||
primaryStage.setHeight(titleContainer.getHeight());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setMaximized(boolean maximized) {
|
||||
if (this.maximized != maximized) {
|
||||
Platform.runLater(btnMax::fire);
|
||||
}
|
||||
}
|
||||
|
||||
public void showUpdate() {
|
||||
updatePane.setVisible(true);
|
||||
}
|
||||
|
||||
private void showCloseNavButton() {
|
||||
navLeft.getChildren().add(closeNavButton);
|
||||
}
|
||||
|
||||
private void hideCloseNavButton() {
|
||||
navLeft.getChildren().remove(closeNavButton);
|
||||
}
|
||||
|
||||
private void setContent(Node content, AnimationProducer animation) {
|
||||
animationHandler.setContent(content, animation);
|
||||
|
||||
if (content instanceof Region) {
|
||||
((Region) content).setMinSize(0, 0);
|
||||
FXUtils.setOverflowHidden((Region) content);
|
||||
}
|
||||
|
||||
if (content instanceof Refreshable)
|
||||
refreshNavButton.setVisible(true);
|
||||
|
||||
if (content != mainPage)
|
||||
backNavButton.setVisible(true);
|
||||
|
||||
String prefix = category == null ? "" : category + " - ";
|
||||
|
||||
titleLabel.textProperty().unbind();
|
||||
|
||||
if (content instanceof WizardPage)
|
||||
titleLabel.setText(prefix + ((WizardPage) content).getTitle());
|
||||
|
||||
if (content instanceof DecoratorPage)
|
||||
titleLabel.textProperty().bind(((DecoratorPage) content).titleProperty());
|
||||
}
|
||||
|
||||
private String category;
|
||||
private Node nowPage;
|
||||
|
||||
public Node getNowPage() {
|
||||
return nowPage;
|
||||
}
|
||||
|
||||
public void showPage(Node content) {
|
||||
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
|
||||
if (content != null)
|
||||
contentPlaceHolder.getStyleClass().add("gray-background");
|
||||
|
||||
Node c = content == null ? mainPage : content;
|
||||
onEnd();
|
||||
if (nowPage instanceof DecoratorPage)
|
||||
((DecoratorPage) nowPage).onClose();
|
||||
nowPage = content;
|
||||
|
||||
setContent(c, ContainerAnimations.FADE.getAnimationProducer());
|
||||
|
||||
if (c instanceof Region) {
|
||||
// Let root pane fix window size.
|
||||
StackPane parent = (StackPane) c.getParent();
|
||||
((Region) c).prefWidthProperty().bind(parent.widthProperty());
|
||||
((Region) c).prefHeightProperty().bind(parent.heightProperty());
|
||||
}
|
||||
}
|
||||
|
||||
public JFXDialog showDialog(Region content) {
|
||||
dialog.setContent(content);
|
||||
if (!dialogShown)
|
||||
dialog.show();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public void startWizard(WizardProvider wizardProvider) {
|
||||
startWizard(wizardProvider, null);
|
||||
}
|
||||
|
||||
public void startWizard(WizardProvider wizardProvider, String category) {
|
||||
this.category = category;
|
||||
wizardController.setProvider(wizardProvider);
|
||||
wizardController.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
backNavButton.setVisible(true);
|
||||
backNavButton.setDisable(false);
|
||||
showCloseNavButton();
|
||||
refreshNavButton.setVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnd() {
|
||||
backNavButton.setVisible(false);
|
||||
hideCloseNavButton();
|
||||
refreshNavButton.setVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
|
||||
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
|
||||
contentPlaceHolder.getStyleClass().add("white-background");
|
||||
setContent(page, nav.getAnimation().getAnimationProducer());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onRefresh() {
|
||||
((Refreshable) contentPlaceHolder.getChildren().get(0)).refresh();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onCloseNav() {
|
||||
wizardController.onCancel();
|
||||
showPage(null);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onBack() {
|
||||
if (wizardController.canPrev())
|
||||
wizardController.onPrev(true);
|
||||
else
|
||||
onCloseNav();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Queue<Object> getCancelQueue() {
|
||||
return cancelQueue;
|
||||
}
|
||||
|
||||
public Runnable getOnCloseButtonAction() {
|
||||
return onCloseButtonAction.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Runnable> onCloseButtonActionProperty() {
|
||||
return onCloseButtonAction;
|
||||
}
|
||||
|
||||
public void setOnCloseButtonAction(Runnable onCloseButtonAction) {
|
||||
this.onCloseButtonAction.set(onCloseButtonAction);
|
||||
}
|
||||
|
||||
public boolean isCustomMaximize() {
|
||||
return customMaximize.get();
|
||||
}
|
||||
|
||||
public BooleanProperty customMaximizeProperty() {
|
||||
return customMaximize;
|
||||
}
|
||||
|
||||
public void setCustomMaximize(boolean customMaximize) {
|
||||
this.customMaximize.set(customMaximize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WizardController getWizardController() {
|
||||
return wizardController;
|
||||
}
|
||||
|
||||
public JFXDialog getDialog() {
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public AdvancedListBox getLeftPane() {
|
||||
return leftPane;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,41 +13,51 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.MultiCharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.task.SilentException;
|
||||
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;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class DialogController {
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
|
||||
public static AuthInfo logIn(Account account) throws Exception {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
public final class DialogController {
|
||||
private DialogController() {
|
||||
}
|
||||
|
||||
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
|
||||
if (account instanceof ClassicAccount) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
||||
JFXUtilities.runInFX(() -> {
|
||||
YggdrasilAccountLoginPane pane = new YggdrasilAccountLoginPane((YggdrasilAccount) account, it -> {
|
||||
res.set(it);
|
||||
latch.countDown();
|
||||
Controllers.closeDialog();
|
||||
}, () -> {
|
||||
latch.countDown();
|
||||
Controllers.closeDialog();
|
||||
});
|
||||
pane.setDialog(Controllers.dialog(pane));
|
||||
runInFX(() -> {
|
||||
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(SilentException::new);
|
||||
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);
|
||||
});
|
||||
latch.await();
|
||||
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
|
||||
}
|
||||
return account.logIn(MultiCharacterSelector.DEFAULT);
|
||||
return account.logIn();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
* 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
|
||||
@@ -13,70 +13,140 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.adapters.ReflectionHelper;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
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.Property;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
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.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
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.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.IOException;
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.ReflectionHelper.call;
|
||||
import static org.jackhuang.hmcl.util.ReflectionHelper.construct;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class FXUtils {
|
||||
private FXUtils() {
|
||||
}
|
||||
|
||||
public static void runInFX(Runnable runnable) {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
Platform.runLater(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
public static void checkFxUserThread() {
|
||||
if (!Platform.isFxApplicationThread()) {
|
||||
throw new IllegalStateException("Not on FX application thread; currentThread = "
|
||||
+ Thread.currentThread().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public static InvalidationListener onInvalidating(Runnable action) {
|
||||
return arg -> action.run();
|
||||
}
|
||||
|
||||
public static <T> void onChange(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
value.addListener((a, b, c) -> consumer.accept(c));
|
||||
}
|
||||
|
||||
public static <T> void onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
value.addListener(new WeakChangeListener<>((a, b, c) -> consumer.accept(c)));
|
||||
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;
|
||||
}
|
||||
|
||||
public static <T> void onChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
onChange(value, consumer);
|
||||
consumer.accept(value.getValue());
|
||||
onChange(value, consumer);
|
||||
}
|
||||
|
||||
public static <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
onWeakChange(value, 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();
|
||||
}
|
||||
|
||||
public static void limitSize(ImageView imageView, double maxWidth, double maxHeight) {
|
||||
@@ -92,102 +162,205 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static void setValidateWhileTextChanged(JFXTextField field) {
|
||||
field.textProperty().addListener(o -> field.validate());
|
||||
field.validate();
|
||||
private static class ListenerPair<T> {
|
||||
private final ObservableValue<T> value;
|
||||
private final ChangeListener<? super T> listener;
|
||||
|
||||
ListenerPair(ObservableValue<T> value, ChangeListener<? super T> listener) {
|
||||
this.value = value;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
void bind() {
|
||||
value.addListener(listener);
|
||||
}
|
||||
|
||||
void unbind() {
|
||||
value.removeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setValidateWhileTextChanged(JFXPasswordField field) {
|
||||
field.textProperty().addListener(o -> field.validate());
|
||||
field.validate();
|
||||
public static <T> void addListener(Node node, String key, ObservableValue<T> value, Consumer<? super T> callback) {
|
||||
ListenerPair<T> pair = new ListenerPair<>(value, (a, b, newValue) -> callback.accept(newValue));
|
||||
node.getProperties().put(key, pair);
|
||||
pair.bind();
|
||||
}
|
||||
|
||||
public static void setOverflowHidden(Region region) {
|
||||
public static void removeListener(Node node, String key) {
|
||||
tryCast(node.getProperties().get(key), ListenerPair.class)
|
||||
.ifPresent(info -> {
|
||||
info.unbind();
|
||||
node.getProperties().remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
public static <K, T> void setupCellValueFactory(JFXTreeTableColumn<K, T> column, Function<K, ObservableValue<T>> mapper) {
|
||||
column.setCellValueFactory(param -> {
|
||||
if (column.validateValue(param))
|
||||
return mapper.apply(param.getValue().getValue());
|
||||
else
|
||||
return column.getComputedValue(param);
|
||||
});
|
||||
}
|
||||
|
||||
public static Node wrapMargin(Node node, Insets insets) {
|
||||
StackPane.setMargin(node, insets);
|
||||
return new StackPane(node);
|
||||
}
|
||||
|
||||
public static void setValidateWhileTextChanged(Node field, boolean validate) {
|
||||
if (field instanceof JFXTextField) {
|
||||
if (validate) {
|
||||
addListener(field, "FXUtils.validation", ((JFXTextField) field).textProperty(), o -> ((JFXTextField) field).validate());
|
||||
} else {
|
||||
removeListener(field, "FXUtils.validation");
|
||||
}
|
||||
((JFXTextField) field).validate();
|
||||
} else if (field instanceof JFXPasswordField) {
|
||||
if (validate) {
|
||||
addListener(field, "FXUtils.validation", ((JFXPasswordField) field).textProperty(), o -> ((JFXPasswordField) field).validate());
|
||||
} else {
|
||||
removeListener(field, "FXUtils.validation");
|
||||
}
|
||||
((JFXPasswordField) field).validate();
|
||||
} else
|
||||
throw new IllegalArgumentException("Only JFXTextField and JFXPasswordField allowed");
|
||||
}
|
||||
|
||||
public static boolean getValidateWhileTextChanged(Node field) {
|
||||
return field.getProperties().containsKey("FXUtils.validation");
|
||||
}
|
||||
|
||||
public static Rectangle setOverflowHidden(Region region) {
|
||||
Rectangle rectangle = new Rectangle();
|
||||
rectangle.widthProperty().bind(region.widthProperty());
|
||||
rectangle.heightProperty().bind(region.heightProperty());
|
||||
region.setClip(rectangle);
|
||||
return rectangle;
|
||||
}
|
||||
|
||||
public static void limitWidth(Region region, double width) {
|
||||
public static Rectangle setOverflowHidden(Region region, double arc) {
|
||||
Rectangle rectangle = setOverflowHidden(region);
|
||||
rectangle.setArcWidth(arc);
|
||||
rectangle.setArcHeight(arc);
|
||||
return rectangle;
|
||||
}
|
||||
|
||||
public static void setLimitWidth(Region region, double width) {
|
||||
region.setMaxWidth(width);
|
||||
region.setMinWidth(width);
|
||||
region.setPrefWidth(width);
|
||||
}
|
||||
|
||||
public static void limitHeight(Region region, double height) {
|
||||
public static double getLimitWidth(Region region) {
|
||||
return region.getMaxWidth();
|
||||
}
|
||||
|
||||
public static void setLimitHeight(Region region, double height) {
|
||||
region.setMaxHeight(height);
|
||||
region.setMinHeight(height);
|
||||
region.setPrefHeight(height);
|
||||
}
|
||||
|
||||
public static double getLimitHeight(Region region) {
|
||||
return region.getMaxHeight();
|
||||
}
|
||||
|
||||
public static Node limitingSize(Node node, double width, double height) {
|
||||
StackPane pane = new StackPane(node);
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(pane, width);
|
||||
FXUtils.setLimitHeight(pane, height);
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static void smoothScrolling(ScrollPane scrollPane) {
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
public static void loadFXML(Node node, String absolutePath) {
|
||||
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), Main.RESOURCE_BUNDLE);
|
||||
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
|
||||
loader.setRoot(node);
|
||||
loader.setController(node);
|
||||
Lang.invoke(() -> loader.load());
|
||||
}
|
||||
|
||||
public static void resetChildren(JFXMasonryPane pane, List<Node> children) {
|
||||
// Fixes mis-repositioning.
|
||||
ReflectionHelper.setFieldContent(JFXMasonryPane.class, pane, "oldBoxes", null);
|
||||
pane.getChildren().setAll(children);
|
||||
}
|
||||
|
||||
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
|
||||
try {
|
||||
// Java 8
|
||||
call(construct(Class.forName("javafx.scene.control.Tooltip$TooltipBehavior"), new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false),
|
||||
"install", node, tooltip);
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
// Java 9
|
||||
call(tooltip, "setShowDelay", new Duration(openDelay));
|
||||
call(tooltip, "setShowDuration", new Duration(visibleDelay));
|
||||
call(tooltip, "setHideDelay", new Duration(closeDelay));
|
||||
} catch (Throwable e2) {
|
||||
e.addSuppressed(e2);
|
||||
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e);
|
||||
}
|
||||
Tooltip.install(node, tooltip);
|
||||
loader.load();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean alert(Alert.AlertType type, String title, String contentText) {
|
||||
return alert(type, title, contentText, null);
|
||||
public static void installFastTooltip(Node node, Tooltip tooltip) {
|
||||
installTooltip(node, 50, 5000, 0, tooltip);
|
||||
}
|
||||
|
||||
public static boolean alert(Alert.AlertType type, String title, String contentText, String headerText) {
|
||||
Alert alert = new Alert(type);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(headerText);
|
||||
alert.setContentText(contentText);
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
return result.isPresent() && result.get() == ButtonType.OK;
|
||||
public static void installFastTooltip(Node node, String tooltip) {
|
||||
installFastTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
public static Optional<String> inputDialog(String title, String contentText) {
|
||||
return inputDialog(title, contentText, null);
|
||||
public static void installSlowTooltip(Node node, Tooltip tooltip) {
|
||||
installTooltip(node, 500, 5000, 0, tooltip);
|
||||
}
|
||||
|
||||
public static Optional<String> inputDialog(String title, String contentText, String headerText) {
|
||||
return inputDialog(title, contentText, headerText, "");
|
||||
public static void installSlowTooltip(Node node, String tooltip) {
|
||||
installSlowTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
public static Optional<String> inputDialog(String title, String contentText, String headerText, String defaultValue) {
|
||||
TextInputDialog dialog = new TextInputDialog(defaultValue);
|
||||
dialog.setTitle(title);
|
||||
dialog.setHeaderText(headerText);
|
||||
dialog.setContentText(contentText);
|
||||
return dialog.showAndWait();
|
||||
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 (timeline != null) timeline.play();
|
||||
node.getProperties().put(animationKey, timeline);
|
||||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
file.mkdirs();
|
||||
if (!FileUtils.makeDirectory(file)) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = file.getAbsolutePath();
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
@@ -199,37 +372,130 @@ public final class FXUtils {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void showFileInExplorer(Path file) {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select", e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Currently unsupported.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
|
||||
|
||||
if (java.awt.Desktop.isDesktopSupported()) {
|
||||
thread(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
||||
if (is.read() != -1) {
|
||||
Runtime.getRuntime().exec(new String[]{browser, link});
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "No known browser found");
|
||||
}
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
|
||||
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) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static void showWebDialog(String title, String content) {
|
||||
showWebDialog(title, content, 800, 480);
|
||||
}
|
||||
|
||||
public static void showWebDialog(String title, String content, int width, int height) {
|
||||
try {
|
||||
WebStage stage = new WebStage(width, height);
|
||||
stage.getWebView().getEngine().loadContent(content);
|
||||
stage.setTitle(title);
|
||||
stage.showAndWait();
|
||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
||||
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
final JFrame frame = new JFrame(title);
|
||||
frame.setSize(width, height);
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
frame.setLocationByPlatform(true);
|
||||
//noinspection ConstantConditions
|
||||
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
|
||||
frame.setLayout(new BorderLayout());
|
||||
|
||||
final JProgressBar progressBar = new JProgressBar();
|
||||
progressBar.setIndeterminate(true);
|
||||
frame.add(progressBar, BorderLayout.PAGE_START);
|
||||
|
||||
Schedulers.defaultScheduler().execute(() -> {
|
||||
final JEditorPane pane = new JEditorPane("text/html", content);
|
||||
pane.setEditable(false);
|
||||
pane.addHyperlinkListener(event -> {
|
||||
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
|
||||
openLink(event.getURL().toExternalForm());
|
||||
}
|
||||
});
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
progressBar.setVisible(false);
|
||||
frame.add(new JScrollPane(pane), BorderLayout.CENTER);
|
||||
});
|
||||
});
|
||||
|
||||
frame.setVisible(true);
|
||||
frame.toFront();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindInt(JFXTextField textField, Property<Number> property) {
|
||||
textField.textProperty().bindBidirectional(property, SafeIntStringConverter.INSTANCE);
|
||||
textField.textProperty().bindBidirectional(property, SafeStringConverter.fromInteger());
|
||||
}
|
||||
|
||||
public static void unbindInt(JFXTextField textField, Property<Number> property) {
|
||||
@@ -260,64 +526,128 @@ public final class FXUtils {
|
||||
checkBox.selectedProperty().unbindBidirectional(property);
|
||||
}
|
||||
|
||||
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum> property) {
|
||||
/**
|
||||
* 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)}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Deprecated
|
||||
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum<?>> property) {
|
||||
unbindEnum(comboBox);
|
||||
ChangeListener<Number> listener = (a, b, newValue) -> {
|
||||
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
|
||||
};
|
||||
@SuppressWarnings("rawtypes")
|
||||
ChangeListener<Number> listener = (a, b, newValue) ->
|
||||
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
|
||||
comboBox.getSelectionModel().select(property.getValue().ordinal());
|
||||
comboBox.getProperties().put("listener", listener);
|
||||
comboBox.getProperties().put("FXUtils.bindEnum.listener", listener);
|
||||
comboBox.getSelectionModel().selectedIndexProperty().addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Deprecated
|
||||
public static void unbindEnum(JFXComboBox<?> comboBox) {
|
||||
ChangeListener listener = Lang.get(comboBox.getProperties(), "listener", ChangeListener.class, null);
|
||||
@SuppressWarnings("rawtypes")
|
||||
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 smoothScrolling(ListView<?> listView) {
|
||||
listView.skinProperty().addListener(o -> {
|
||||
ScrollBar bar = (ScrollBar) listView.lookup(".scroll-bar");
|
||||
Node virtualFlow = listView.lookup(".virtual-flow");
|
||||
double[] frictions = new double[]{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};
|
||||
double[] pushes = new double[]{1};
|
||||
double[] derivatives = new double[frictions.length];
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
return new Image(url);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ResourceNotFoundError("Cannot access image: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
bar.addEventHandler(MouseEvent.DRAG_DETECTED, e -> timeline.stop());
|
||||
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
|
||||
applyDragListener(node, filter, callback, null);
|
||||
}
|
||||
|
||||
EventHandler<ScrollEvent> scrollEventHandler = event -> {
|
||||
if (event.getEventType() == ScrollEvent.SCROLL) {
|
||||
int direction = event.getDeltaY() > 0 ? -1 : 1;
|
||||
for (int i = 0; i < pushes.length; ++i)
|
||||
derivatives[i] += direction * pushes[i];
|
||||
if (timeline.getStatus() == Animation.Status.STOPPED)
|
||||
timeline.play();
|
||||
event.consume();
|
||||
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback, Runnable dragDropped) {
|
||||
node.setOnDragOver(event -> {
|
||||
if (event.getGestureSource() != node && event.getDragboard().hasFiles()) {
|
||||
if (event.getDragboard().getFiles().stream().anyMatch(filter::accept))
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
node.setOnDragDropped(event -> {
|
||||
List<File> files = event.getDragboard().getFiles();
|
||||
if (files != null) {
|
||||
List<File> acceptFiles = files.stream().filter(filter::accept).collect(Collectors.toList());
|
||||
if (!acceptFiles.isEmpty()) {
|
||||
callback.accept(acceptFiles);
|
||||
event.setDropCompleted(true);
|
||||
}
|
||||
};
|
||||
|
||||
bar.addEventHandler(ScrollEvent.ANY, scrollEventHandler);
|
||||
virtualFlow.setOnScroll(scrollEventHandler);
|
||||
|
||||
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];
|
||||
double height = listView.getLayoutBounds().getHeight();
|
||||
bar.setValue(Math.min(Math.max(bar.getValue() + dy / height, 0), 1));
|
||||
if (Math.abs(dy) < 0.001)
|
||||
timeline.stop();
|
||||
listView.requestLayout();
|
||||
}));
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
}
|
||||
if (dragDropped != null)
|
||||
dragDropped.run();
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
public static final Image DEFAULT_ICON = new Image("/assets/img/icon.png");
|
||||
public static <T> StringConverter<T> stringConverter(Function<T, String> func) {
|
||||
return new StringConverter<T>() {
|
||||
|
||||
@Override
|
||||
public String toString(T object) {
|
||||
return object == null ? "" : func.apply(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T fromString(String string) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
|
||||
return view -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (!empty) {
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(graphicBuilder.apply(item));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
@@ -330,4 +660,90 @@ public final class FXUtils {
|
||||
return "Interpolator.SINE";
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Based on https://stackoverflow.com/a/57552025
|
||||
// Fix #874: Use it instead of SwingFXUtils.toFXImage
|
||||
public 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;
|
||||
}
|
||||
|
||||
public static void copyText(String text) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(text);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
|
||||
Controllers.showToast(i18n("message.copied"));
|
||||
}
|
||||
|
||||
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
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<javafx.scene.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");
|
||||
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
|
||||
hyperlink.setOnAction(e -> hyperlinkAction.accept(href));
|
||||
texts.add(hyperlink);
|
||||
} 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.log(Level.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
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.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
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.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.Logging.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 java;
|
||||
private final LibraryAnalyzer analyzer;
|
||||
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
|
||||
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
|
||||
private final StringProperty reason = new SimpleStringProperty(i18n("game.crash.reason.unknown"));
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty();
|
||||
private final Label feedbackLabel = new Label(i18n("game.crash.feedback"));
|
||||
|
||||
private final ManagedProcess managedProcess;
|
||||
private final DefaultGameRepository repository;
|
||||
private final ProcessListener.ExitType exitType;
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
|
||||
private final List<Pair<String, Log4jLevel>> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Pair<String, Log4jLevel>> logs) {
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.launchOptions = launchOptions;
|
||||
this.logs = logs;
|
||||
this.analyzer = LibraryAnalyzer.analyze(version);
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
|
||||
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
? launchOptions.getJava().getVersion()
|
||||
: launchOptions.getJava().getVersion() + " (" + launchOptions.getJava().getArchitecture().getDisplayName() + ")";
|
||||
|
||||
this.view = new View();
|
||||
|
||||
setScene(new Scene(view, 800, 480));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("game.crash.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
|
||||
analyzeCrashReport();
|
||||
}
|
||||
|
||||
private void analyzeCrashReport() {
|
||||
loading.set(true);
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
|
||||
Set<String> keywords = Collections.emptySet();
|
||||
String crashReport = null;
|
||||
try {
|
||||
crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to read crash report", e);
|
||||
}
|
||||
if (crashReport == null) {
|
||||
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
|
||||
}
|
||||
if (crashReport != null) {
|
||||
keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport);
|
||||
}
|
||||
return pair(
|
||||
CrashReportAnalyzer.anaylze(rawLog),
|
||||
keywords);
|
||||
}).whenCompleteAsync((pair, exception) -> {
|
||||
loading.set(false);
|
||||
|
||||
if (exception != null) {
|
||||
LOG.log(Level.WARNING, "Failed to analyze crash report", exception);
|
||||
reason.set(i18n("game.crash.reason.unknown"));
|
||||
} else {
|
||||
List<CrashReportAnalyzer.Result> results = pair.getKey();
|
||||
Set<String> keywords = pair.getValue();
|
||||
StringBuilder reasonText = new StringBuilder();
|
||||
for (CrashReportAnalyzer.Result result : results) {
|
||||
switch (result.getRule()) {
|
||||
case TOO_OLD_JAVA:
|
||||
reasonText.append(i18n("game.crash.reason.too_old_java",
|
||||
CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected")))))
|
||||
.append("\n");
|
||||
break;
|
||||
case MOD_RESOLUTION_CONFLICT:
|
||||
case MOD_RESOLUTION_MISSING:
|
||||
case MOD_RESOLUTION_COLLECTION:
|
||||
reasonText.append(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:
|
||||
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("mod")),
|
||||
result.getMatcher().group("version")));
|
||||
break;
|
||||
case TWILIGHT_FOREST_OPTIFINE:
|
||||
reasonText.append(i18n("game.crash.reason.mod", "OptiFine"));
|
||||
break;
|
||||
default:
|
||||
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
|
||||
.toArray()))
|
||||
.append("\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (results.isEmpty()) {
|
||||
if (!keywords.isEmpty()) {
|
||||
reason.set(i18n("game.crash.reason.stacktrace", String.join(", ", keywords)));
|
||||
} else {
|
||||
reason.set(i18n("game.crash.reason.unknown"));
|
||||
}
|
||||
|
||||
feedbackLabel.setVisible(true);
|
||||
} else {
|
||||
reason.set(reasonText.toString());
|
||||
}
|
||||
}
|
||||
}, Schedulers.javafx()).exceptionally(Lang::handleUncaughtException);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
|
||||
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
|
||||
logWindow.showNormal();
|
||||
}
|
||||
|
||||
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(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.thenRunAsync(() -> {
|
||||
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();
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
}, Schedulers.javafx())
|
||||
.exceptionally(e -> {
|
||||
LOG.log(Level.WARNING, "Failed to export game crash info", e);
|
||||
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;
|
||||
}
|
||||
|
||||
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 version = new TwoLineListItem();
|
||||
version.getStyleClass().setAll("two-line-item-second-large");
|
||||
version.setTitle(i18n("archive.game_version"));
|
||||
version.setSubtitle(GameCrashWindow.this.version.getId());
|
||||
|
||||
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.subtitleProperty().bind(GameCrashWindow.this.os);
|
||||
|
||||
TwoLineListItem arch = new TwoLineListItem();
|
||||
arch.getStyleClass().setAll("two-line-item-second-large");
|
||||
arch.setTitle(i18n("system.architecture"));
|
||||
arch.subtitleProperty().bind(GameCrashWindow.this.arch);
|
||||
|
||||
infoPane.getChildren().setAll(version, 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()) {
|
||||
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());
|
||||
|
||||
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());
|
||||
|
||||
TwoLineListItem reason = new TwoLineListItem();
|
||||
reason.getStyleClass().setAll("two-line-item-second-large", "wrap-text");
|
||||
reason.setTitle(i18n("game.crash.reason"));
|
||||
reason.subtitleProperty().bind(GameCrashWindow.this.reason);
|
||||
|
||||
gameDirPane.setPadding(new Insets(8));
|
||||
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
|
||||
gameDirPane.getChildren().setAll(gameDir, javaDir, reason);
|
||||
}
|
||||
|
||||
HBox toolBar = new HBox();
|
||||
{
|
||||
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportGameCrashInfoButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
exportGameCrashInfoButton.getStyleClass().add("jfx-button-raised");
|
||||
exportGameCrashInfoButton.setOnMouseClicked(e -> exportGameCrashInfo());
|
||||
|
||||
JFXButton logButton = new JFXButton(i18n("logwindow.title"));
|
||||
logButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
logButton.getStyleClass().add("jfx-button-raised");
|
||||
logButton.setOnMouseClicked(e -> showLogWindow());
|
||||
|
||||
toolBar.setPadding(new Insets(8));
|
||||
toolBar.setSpacing(8);
|
||||
toolBar.getStyleClass().add("jfx-tool-bar");
|
||||
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, feedbackLabel);
|
||||
}
|
||||
|
||||
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
|
||||
import org.jackhuang.hmcl.game.GameVersion;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.download.InstallerWizardProvider;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class InstallerController {
|
||||
private Profile profile;
|
||||
private String versionId;
|
||||
private Version version;
|
||||
@FXML
|
||||
private ScrollPane scrollPane;
|
||||
@FXML private VBox contentPane;
|
||||
private String forge;
|
||||
private String liteLoader;
|
||||
private String optiFine;
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
public void loadVersion(Profile profile, String versionId) {
|
||||
this.profile = profile;
|
||||
this.versionId = versionId;
|
||||
this.version = profile.getRepository().getVersion(versionId).resolve(profile.getRepository());
|
||||
|
||||
contentPane.getChildren().clear();
|
||||
forge = liteLoader = optiFine = null;
|
||||
|
||||
for (Library library : version.getLibraries()) {
|
||||
Consumer<InstallerItem> removeAction = x -> {
|
||||
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
|
||||
newList.remove(library);
|
||||
new MaintainTask(profile.getRepository(), version.setLibraries(newList))
|
||||
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
|
||||
.with(profile.getRepository().refreshVersionsAsync())
|
||||
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
|
||||
.start();
|
||||
};
|
||||
|
||||
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
|
||||
contentPane.getChildren().add(new InstallerItem("Forge", library.getVersion(), removeAction));
|
||||
forge = library.getVersion();
|
||||
}
|
||||
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
|
||||
contentPane.getChildren().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction));
|
||||
liteLoader = library.getVersion();
|
||||
}
|
||||
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
|
||||
contentPane.getChildren().add(new InstallerItem("OptiFine", library.getVersion(), removeAction));
|
||||
optiFine = library.getVersion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onAdd() {
|
||||
Optional<String> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version));
|
||||
|
||||
if (!gameVersion.isPresent())
|
||||
Controllers.dialog("version.cannot_read");
|
||||
else
|
||||
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version, forge, liteLoader, optiFine));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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
|
||||
@@ -13,42 +13,220 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.fxml.FXML;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.EventHandler;
|
||||
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 org.jackhuang.hmcl.Main;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class InstallerItem extends BorderPane {
|
||||
private final Consumer<InstallerItem> deleteCallback;
|
||||
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 StringProperty dependencyName = 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<>();
|
||||
|
||||
@FXML
|
||||
private Label lblInstallerArtifact;
|
||||
|
||||
@FXML
|
||||
private Label lblInstallerVersion;
|
||||
|
||||
public InstallerItem(String artifact, String version, Consumer<InstallerItem> deleteCallback) {
|
||||
this.deleteCallback = deleteCallback;
|
||||
FXUtils.loadFXML(this, "/assets/fxml/version/installer-item.fxml");
|
||||
|
||||
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
|
||||
JFXDepthManager.setDepth(this, 1);
|
||||
lblInstallerArtifact.setText(artifact);
|
||||
lblInstallerVersion.setText(Main.i18n("archive.version") + ": " + version);
|
||||
public InstallerItem(LibraryAnalyzer.LibraryType id) {
|
||||
this(id.getPatchId());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onDelete() {
|
||||
deleteCallback.accept(this);
|
||||
public InstallerItem(String id) {
|
||||
this.id = id;
|
||||
|
||||
switch (id) {
|
||||
case "game":
|
||||
imageUrl = "/assets/img/grass.png";
|
||||
break;
|
||||
case "fabric":
|
||||
case "fabric-api":
|
||||
imageUrl = "/assets/img/fabric.png";
|
||||
break;
|
||||
case "forge":
|
||||
imageUrl = "/assets/img/forge.png";
|
||||
break;
|
||||
case "liteloader":
|
||||
imageUrl = "/assets/img/chicken.png";
|
||||
break;
|
||||
case "optifine":
|
||||
imageUrl = "/assets/img/command.png";
|
||||
break;
|
||||
default:
|
||||
imageUrl = 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new InstallerItemSkin(this);
|
||||
}
|
||||
|
||||
public static class InstallerItemGroup {
|
||||
public final InstallerItem game = new InstallerItem(MINECRAFT);
|
||||
public final InstallerItem fabric = new InstallerItem(FABRIC);
|
||||
public final InstallerItem fabricApi = new InstallerItem(FABRIC_API);
|
||||
public final InstallerItem forge = new InstallerItem(FORGE);
|
||||
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
|
||||
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
|
||||
|
||||
public InstallerItemGroup() {
|
||||
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion));
|
||||
|
||||
liteLoader.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion));
|
||||
|
||||
optiFine.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion));
|
||||
|
||||
for (InstallerItem fabric : new InstallerItem[]{fabric, fabricApi}) {
|
||||
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));
|
||||
}
|
||||
|
||||
fabricApi.dependencyName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() == null) return FABRIC.getPatchId();
|
||||
else return null;
|
||||
}, fabric.libraryVersion));
|
||||
}
|
||||
|
||||
public InstallerItem[] getLibraries() {
|
||||
return new InstallerItem[]{game, forge, liteLoader, optiFine, fabric, fabricApi};
|
||||
}
|
||||
}
|
||||
|
||||
public static class InstallerItemSkin extends SkinBase<InstallerItem> {
|
||||
|
||||
InstallerItemSkin(InstallerItem control) {
|
||||
super(control);
|
||||
|
||||
HBox hbox = new HBox();
|
||||
hbox.getStyleClass().add("md-list-cell");
|
||||
hbox.setPadding(new Insets(8));
|
||||
RipplerContainer container = new RipplerContainer(hbox);
|
||||
getChildren().setAll(container);
|
||||
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
if (control.imageUrl != null) {
|
||||
ImageView view = new ImageView(new Image(control.imageUrl, 32, 32, true, true));
|
||||
Node node = FXUtils.limitingSize(view, 32, 32);
|
||||
node.setMouseTransparent(true);
|
||||
hbox.getChildren().add(node);
|
||||
}
|
||||
|
||||
Label nameLabel = new Label();
|
||||
nameLabel.setMouseTransparent(true);
|
||||
hbox.getChildren().add(nameLabel);
|
||||
nameLabel.setPrefWidth(80);
|
||||
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();
|
||||
label.setMouseTransparent(true);
|
||||
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) {
|
||||
return i18n("install.installer.not_installed");
|
||||
} else {
|
||||
return i18n("install.installer.version", version);
|
||||
}
|
||||
}, control.incompatibleLibraryName, control.incompatibleWithGame, control.libraryVersion));
|
||||
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
|
||||
BorderPane.setAlignment(label, 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);
|
||||
|
||||
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);
|
||||
|
||||
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
|
||||
if (clickable) {
|
||||
container.onMouseClickedProperty().bind(control.action);
|
||||
hbox.setCursor(Cursor.HAND);
|
||||
} else {
|
||||
container.onMouseClickedProperty().unbind();
|
||||
container.onMouseClickedProperty().set(null);
|
||||
hbox.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.text.Text;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.ProfileChangedEvent;
|
||||
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.AccountHelper;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.ClassTitle;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedItem;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class LeftPaneController {
|
||||
private final AdvancedListBox leftPane;
|
||||
private final VBox profilePane = new VBox();
|
||||
private final VersionListItem accountItem = new VersionListItem("", "");
|
||||
|
||||
public LeftPaneController(AdvancedListBox leftPane) {
|
||||
this.leftPane = leftPane;
|
||||
|
||||
leftPane.startCategory(Main.i18n("account").toUpperCase())
|
||||
.add(Lang.apply(new RipplerContainer(accountItem), rippler -> {
|
||||
rippler.setOnMouseClicked(e -> Controllers.navigate(new AccountsPage()));
|
||||
accountItem.setOnSettingsButtonClicked(() -> Controllers.navigate(new AccountsPage()));
|
||||
}))
|
||||
.startCategory(Main.i18n("launcher").toUpperCase())
|
||||
.add(Lang.apply(new IconedItem(SVG.gear("black", 20, 20), Main.i18n("settings.launcher")), iconedItem -> {
|
||||
iconedItem.prefWidthProperty().bind(leftPane.widthProperty());
|
||||
iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
|
||||
}))
|
||||
.add(new ClassTitle(Lang.apply(new BorderPane(), borderPane -> {
|
||||
borderPane.setLeft(Lang.apply(new VBox(), vBox -> {
|
||||
vBox.getChildren().setAll(new Text(Main.i18n("profile.title").toUpperCase()));
|
||||
}));
|
||||
JFXButton addProfileButton = new JFXButton();
|
||||
addProfileButton.setGraphic(SVG.plus("black", 10, 10));
|
||||
addProfileButton.getStyleClass().add("toggle-icon-tiny");
|
||||
addProfileButton.setOnMouseClicked(e ->
|
||||
Controllers.getDecorator().showPage(new ProfilePage(null)));
|
||||
borderPane.setRight(addProfileButton);
|
||||
})))
|
||||
.add(profilePane);
|
||||
|
||||
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
|
||||
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
|
||||
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions);
|
||||
|
||||
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), it -> {
|
||||
if (it == null) {
|
||||
accountItem.setVersionName(Main.i18n("account.missing"));
|
||||
accountItem.setGameVersion(Main.i18n("message.unknown"));
|
||||
} else {
|
||||
accountItem.setVersionName(Accounts.getCurrentCharacter(it));
|
||||
accountItem.setGameVersion(AccountsPage.accountType(it));
|
||||
}
|
||||
|
||||
if (it instanceof YggdrasilAccount) {
|
||||
Image image = AccountHelper.getSkin((YggdrasilAccount) it, 4);
|
||||
accountItem.setImage(image, AccountHelper.getViewport(4));
|
||||
} else
|
||||
accountItem.setImage(AccountHelper.getDefaultSkin(it, 4), AccountHelper.getViewport(4));
|
||||
});
|
||||
}
|
||||
|
||||
private void onProfileChanged(ProfileChangedEvent event) {
|
||||
Profile profile = event.getProfile();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
for (Node node : profilePane.getChildren()) {
|
||||
if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof Pair<?, ?>) {
|
||||
boolean current = Objects.equals(((Pair) node.getProperties().get("profile")).getKey(), profile.getName());
|
||||
((RipplerContainer) node).setSelected(current);
|
||||
((VersionListItem) ((RipplerContainer) node).getContainer()).setGameVersion(current ? Main.i18n("profile.selected") : "");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onProfilesLoading() {
|
||||
LinkedList<RipplerContainer> list = new LinkedList<>();
|
||||
for (Profile profile : Settings.INSTANCE.getProfiles()) {
|
||||
VersionListItem item = new VersionListItem(profile.getName());
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(item);
|
||||
item.setOnSettingsButtonClicked(() -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
|
||||
ripplerContainer.setOnMouseClicked(e -> Settings.INSTANCE.setSelectedProfile(profile));
|
||||
ripplerContainer.getProperties().put("profile", new Pair<>(profile.getName(), item));
|
||||
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
|
||||
list.add(ripplerContainer);
|
||||
}
|
||||
Platform.runLater(() -> profilePane.getChildren().setAll(list));
|
||||
}
|
||||
|
||||
private boolean checkedModpack = false;
|
||||
|
||||
private void onRefreshedVersions(RefreshedVersionsEvent event) {
|
||||
JFXUtilities.runInFX(() -> {
|
||||
boolean flag = true;
|
||||
HMCLGameRepository repository = (HMCLGameRepository) event.getSource();
|
||||
if (!checkedModpack) {
|
||||
checkedModpack = true;
|
||||
|
||||
if (repository.getVersionCount() == 0) {
|
||||
File modpackFile = new File("modpack.zip").getAbsoluteFile();
|
||||
if (modpackFile.exists()) {
|
||||
try {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
|
||||
Controllers.taskDialog(ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
|
||||
.with(Task.of(Schedulers.javafx(), () -> {
|
||||
Controllers.closeDialog();
|
||||
checkAccount();
|
||||
})).executor(),
|
||||
Main.i18n("modpack.installing"), "", null);
|
||||
flag = false;
|
||||
} catch (UnsupportedModpackException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flag)
|
||||
checkAccount();
|
||||
});
|
||||
}
|
||||
|
||||
private void checkAccount() {
|
||||
if (Settings.INSTANCE.getAccounts().isEmpty())
|
||||
Controllers.navigate(new AccountsPage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
|
||||
public abstract class ListPage<T extends Node> extends ListPageBase<T> {
|
||||
private final BooleanProperty refreshable = new SimpleBooleanProperty(this, "refreshable", false);
|
||||
|
||||
public abstract void add();
|
||||
|
||||
public void refresh() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new ListPageSkin(this);
|
||||
}
|
||||
|
||||
public boolean isRefreshable() {
|
||||
return refreshable.get();
|
||||
}
|
||||
|
||||
public BooleanProperty refreshableProperty() {
|
||||
return refreshable;
|
||||
}
|
||||
|
||||
public void setRefreshable(boolean refreshable) {
|
||||
this.refreshable.set(refreshable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public void setItems(ObservableList<T> items) {
|
||||
this.items.set(items);
|
||||
}
|
||||
|
||||
public ListProperty<T> itemsProperty() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return loading.get();
|
||||
}
|
||||
|
||||
public void setLoading(boolean loading) {
|
||||
this.loading.set(loading);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
|
||||
public ListPageSkin(ListPage<?> skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||
Pane placeholder = new Pane();
|
||||
VBox list = new VBox();
|
||||
|
||||
StackPane contentPane = new StackPane();
|
||||
{
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
{
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
list.maxWidthProperty().bind(scrollPane.widthProperty());
|
||||
list.setSpacing(10);
|
||||
|
||||
VBox content = new VBox();
|
||||
content.getChildren().setAll(list, placeholder);
|
||||
|
||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
VBox vBox = new VBox();
|
||||
{
|
||||
vBox.getStyleClass().add("card-list");
|
||||
vBox.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
vBox.setPickOnBounds(false);
|
||||
|
||||
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());
|
||||
|
||||
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());
|
||||
|
||||
vBox.getChildren().setAll(btnAdd);
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.refreshableProperty(),
|
||||
refreshable -> {
|
||||
if (refreshable) {
|
||||
list.setPadding(new Insets(10, 10, 15 + 40 + 15 + 40 + 15, 10));
|
||||
vBox.getChildren().setAll(btnRefresh, btnAdd);
|
||||
} else {
|
||||
list.setPadding(new Insets(10, 10, 15 + 40 + 15, 10));
|
||||
vBox.getChildren().setAll(btnAdd);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keep a blank space to prevent buttons from blocking up mod items.
|
||||
BorderPane group = new BorderPane();
|
||||
group.setPickOnBounds(false);
|
||||
group.setBottom(vBox);
|
||||
placeholder.minHeightProperty().bind(vBox.heightProperty());
|
||||
|
||||
contentPane.getChildren().setAll(scrollPane, group);
|
||||
}
|
||||
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.setContent(contentPane);
|
||||
|
||||
getChildren().setAll(spinnerPane);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2013 huangyuhui <huanghongxun2008@126.com>
|
||||
* 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
|
||||
@@ -13,37 +13,54 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
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.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
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.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -51,192 +68,293 @@ import java.util.concurrent.CountDownLatch;
|
||||
*/
|
||||
public final class LogWindow extends Stage {
|
||||
|
||||
private final ReadOnlyIntegerWrapper fatal = new ReadOnlyIntegerWrapper(0);
|
||||
private final ReadOnlyIntegerWrapper error = new ReadOnlyIntegerWrapper(0);
|
||||
private final ReadOnlyIntegerWrapper warn = new ReadOnlyIntegerWrapper(0);
|
||||
private final ReadOnlyIntegerWrapper info = new ReadOnlyIntegerWrapper(0);
|
||||
private final ReadOnlyIntegerWrapper debug = new ReadOnlyIntegerWrapper(0);
|
||||
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 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 CountDownLatch latch = new CountDownLatch(1);
|
||||
public final EventManager<Event> onDone = new EventManager<>();
|
||||
private final ChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
|
||||
|
||||
private Consumer<String> exportGameCrashInfoCallback;
|
||||
|
||||
private boolean stopCheckLogCount = false;
|
||||
|
||||
public LogWindow() {
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(Settings.INSTANCE.getTheme().getStylesheets());
|
||||
setTitle(Main.i18n("logwindow.title"));
|
||||
getIcons().add(new Image("/assets/img/icon.png"));
|
||||
}
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("logwindow.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
|
||||
public LogWindow(String text) {
|
||||
this();
|
||||
|
||||
onDone.register(() -> {
|
||||
logLine(text, Log4jLevel.INFO);
|
||||
});
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty fatalProperty() {
|
||||
return fatal.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getFatal() {
|
||||
return fatal.get();
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty errorProperty() {
|
||||
return error.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getError() {
|
||||
return error.get();
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty warnProperty() {
|
||||
return warn.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getWarn() {
|
||||
return warn.get();
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty infoProperty() {
|
||||
return info.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getInfo() {
|
||||
return info.get();
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty debugProperty() {
|
||||
return debug.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getDebug() {
|
||||
return debug.get();
|
||||
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
|
||||
}
|
||||
|
||||
public void logLine(String line, Log4jLevel level) {
|
||||
Element div = impl.document.createElement("div");
|
||||
// a <pre> element to prevent multiple spaces and tabs being removed.
|
||||
Element pre = impl.document.createElement("pre");
|
||||
pre.setTextContent(line);
|
||||
div.appendChild(pre);
|
||||
impl.body.appendChild(div);
|
||||
impl.engine.executeScript("checkNewLog(\"" + level.name().toLowerCase() + "\");scrollToBottom();");
|
||||
Log log = new Log(parseEscapeSequence(line), level);
|
||||
logs.add(log);
|
||||
if (levelShownMap.get(level).get())
|
||||
impl.listView.getItems().add(log);
|
||||
|
||||
switch (level) {
|
||||
case FATAL:
|
||||
fatal.set(fatal.get() + 1);
|
||||
break;
|
||||
case ERROR:
|
||||
error.set(error.get() + 1);
|
||||
break;
|
||||
case WARN:
|
||||
warn.set(warn.get() + 1);
|
||||
break;
|
||||
case INFO:
|
||||
info.set(info.get() + 1);
|
||||
break;
|
||||
case DEBUG:
|
||||
debug.set(debug.get() + 1);
|
||||
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
|
||||
if (!stopCheckLogCount) checkLogCount();
|
||||
}
|
||||
|
||||
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
|
||||
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
|
||||
this.impl.showCrashReport.set(true);
|
||||
stopCheckLogCount = true;
|
||||
for (Log log : impl.listView.getItems()) {
|
||||
if (log.log.contains("Minecraft Crash Report")) {
|
||||
Platform.runLater(() -> {
|
||||
impl.listView.scrollTo(log);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
show();
|
||||
}
|
||||
|
||||
public void showNormal() {
|
||||
this.impl.showCrashReport.set(false);
|
||||
show();
|
||||
}
|
||||
|
||||
private void shakeLogs() {
|
||||
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private void checkLogCount() {
|
||||
while (logs.size() > config().getLogLines()) {
|
||||
Log removedLog = logs.removeFirst();
|
||||
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
|
||||
// TODO: fix O(n)
|
||||
impl.listView.getItems().remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForLoaded() throws InterruptedException {
|
||||
latch.await();
|
||||
private static class Log {
|
||||
private final String log;
|
||||
private final Log4jLevel level;
|
||||
|
||||
public Log(String log, Log4jLevel level) {
|
||||
this.log = log;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
public class LogWindowImpl extends StackPane {
|
||||
public class LogWindowImpl extends Control {
|
||||
|
||||
@FXML
|
||||
private WebView webView;
|
||||
@FXML
|
||||
private ToggleButton btnFatals;
|
||||
@FXML
|
||||
private ToggleButton btnErrors;
|
||||
@FXML
|
||||
private ToggleButton btnWarns;
|
||||
@FXML
|
||||
private ToggleButton btnInfos;
|
||||
@FXML
|
||||
private ToggleButton btnDebugs;
|
||||
@FXML
|
||||
private ComboBox<String> cboLines;
|
||||
|
||||
WebEngine engine;
|
||||
Node body;
|
||||
Document document;
|
||||
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 BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||
|
||||
LogWindowImpl() {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/log.fxml");
|
||||
|
||||
engine = webView.getEngine();
|
||||
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
|
||||
.replace("${FONT}", Settings.INSTANCE.getFont().getSize() + "px \"" + Settings.INSTANCE.getFont().getFamily() + "\""));
|
||||
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
|
||||
if (newValue == Worker.State.SUCCEEDED) {
|
||||
document = engine.getDocument();
|
||||
body = document.getElementsByTagName("body").item(0);
|
||||
engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
|
||||
latch.countDown();
|
||||
onDone.fireEvent(new Event(LogWindow.this));
|
||||
}
|
||||
});
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
boolean flag = false;
|
||||
cboLines.getItems().setAll("500", "2000", "5000");
|
||||
for (String i : cboLines.getItems())
|
||||
if (Integer.toString(Settings.INSTANCE.getLogLines()).equals(i)) {
|
||||
if (Integer.toString(config().getLogLines()).equals(i)) {
|
||||
cboLines.getSelectionModel().select(i);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
||||
Settings.INSTANCE.setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
|
||||
engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
|
||||
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
|
||||
});
|
||||
|
||||
if (!flag)
|
||||
cboLines.getSelectionModel().select(0);
|
||||
|
||||
btnFatals.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(fatal.get()) + " fatals", fatal));
|
||||
btnErrors.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(error.get()) + " errors", error));
|
||||
btnWarns.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(warn.get()) + " warns", warn));
|
||||
btnInfos.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(info.get()) + " infos", info));
|
||||
btnDebugs.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(debug.get()) + " debugs", debug));
|
||||
|
||||
btnFatals.selectedProperty().addListener(o -> specificChanged());
|
||||
btnErrors.selectedProperty().addListener(o -> specificChanged());
|
||||
btnWarns.selectedProperty().addListener(o -> specificChanged());
|
||||
btnInfos.selectedProperty().addListener(o -> specificChanged());
|
||||
btnDebugs.selectedProperty().addListener(o -> specificChanged());
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private void specificChanged() {
|
||||
String res = "";
|
||||
if (btnFatals.isSelected())
|
||||
res += "\"fatal\", ";
|
||||
if (btnErrors.isSelected())
|
||||
res += "\"error\", ";
|
||||
if (btnWarns.isSelected())
|
||||
res += "\"warn\", ";
|
||||
if (btnInfos.isSelected())
|
||||
res += "\"info\", ";
|
||||
if (btnDebugs.isSelected())
|
||||
res += "\"debug\", ";
|
||||
if (StringUtils.isNotBlank(res))
|
||||
res = StringUtils.substringBeforeLast(res, ", ");
|
||||
engine.executeScript("specific([" + res + "])");
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onTerminateGame() {
|
||||
LauncherHelper.stopManagedProcesses();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onClear() {
|
||||
engine.executeScript("clear()");
|
||||
impl.listView.getItems().clear();
|
||||
logs.clear();
|
||||
}
|
||||
|
||||
private void onExportLogs() {
|
||||
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()));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.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) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onExportGameCrashInfo() {
|
||||
if (exportGameCrashInfoCallback == null) return;
|
||||
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new LogWindowSkin(this);
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
protected LogWindowSkin(LogWindowImpl control) {
|
||||
super(control);
|
||||
|
||||
VBox vbox = new VBox(3);
|
||||
vbox.setPadding(new Insets(3, 0, 3, 0));
|
||||
vbox.setStyle("-fx-background-color: white");
|
||||
getChildren().setAll(vbox);
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
borderPane.setPadding(new Insets(0, 3, 0, 3));
|
||||
|
||||
{
|
||||
HBox hBox = new HBox(3);
|
||||
hBox.setPadding(new Insets(0, 0, 0, 4));
|
||||
hBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
Label label = new Label(i18n("logwindow.show_lines"));
|
||||
hBox.getChildren().setAll(label, control.cboLines);
|
||||
|
||||
borderPane.setLeft(hBox);
|
||||
}
|
||||
|
||||
{
|
||||
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))
|
||||
);
|
||||
borderPane.setRight(hBox);
|
||||
}
|
||||
|
||||
vbox.getChildren().add(borderPane);
|
||||
}
|
||||
|
||||
{
|
||||
ListView<Log> listView = control.listView;
|
||||
listView.getItems().addListener((InvalidationListener) observable -> {
|
||||
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.setCellFactory(x -> new ListCell<Log>() {
|
||||
{
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
Region clippedContainer = (Region)listView.lookup(".clipped-container");
|
||||
if (clippedContainer != null) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
}
|
||||
setPadding(new Insets(2));
|
||||
setWrapText(true);
|
||||
setGraphic(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Log item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
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);
|
||||
if (empty) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(item.log);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
vbox.getChildren().add(listView);
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane bottom = new BorderPane();
|
||||
|
||||
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportGameCrashInfoButton.setOnMouseClicked(e -> getSkinnable().onExportGameCrashInfo());
|
||||
exportGameCrashInfoButton.visibleProperty().bind(getSkinnable().showCrashReport);
|
||||
bottom.setLeft(exportGameCrashInfoButton);
|
||||
|
||||
HBox hBox = new HBox(3);
|
||||
bottom.setRight(hBox);
|
||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
hBox.setPadding(new Insets(0, 3, 0, 3));
|
||||
|
||||
JFXCheckBox autoScrollCheckBox = new JFXCheckBox(i18n("logwindow.autoscroll"));
|
||||
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());
|
||||
|
||||
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
||||
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
|
||||
|
||||
vbox.getChildren().add(bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXMasonryPane;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.ProfileChangedEvent;
|
||||
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.GameVersion;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("main_page"));
|
||||
|
||||
private Profile profile;
|
||||
private String rightClickedVersion;
|
||||
|
||||
@FXML
|
||||
private JFXButton btnRefresh;
|
||||
|
||||
@FXML
|
||||
private JFXButton btnAdd;
|
||||
|
||||
@FXML
|
||||
private JFXMasonryPane masonryPane;
|
||||
|
||||
@FXML
|
||||
private JFXListView versionList;
|
||||
|
||||
private JFXPopup versionPopup;
|
||||
|
||||
{
|
||||
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
|
||||
|
||||
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(() -> Platform.runLater(this::loadVersions));
|
||||
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
|
||||
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
|
||||
|
||||
versionPopup = new JFXPopup(versionList);
|
||||
getChildren().remove(versionList);
|
||||
|
||||
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), Main.i18n("install")));
|
||||
FXUtils.installTooltip(btnAdd, 0, 5000, 0, new Tooltip(Main.i18n("install")));
|
||||
btnRefresh.setOnMouseClicked(e -> Settings.INSTANCE.getSelectedProfile().getRepository().refreshVersionsAsync().start());
|
||||
FXUtils.installTooltip(btnRefresh, 0, 5000, 0, new Tooltip(Main.i18n("button.refresh")));
|
||||
}
|
||||
|
||||
private Node buildNode(Profile profile, String version, String game) {
|
||||
VersionItem item = new VersionItem();
|
||||
item.setUpdate(profile.getRepository().isModpack(version));
|
||||
item.setGameVersion(game);
|
||||
item.setVersionName(version);
|
||||
item.setOnLaunchButtonClicked(e -> {
|
||||
if (Settings.INSTANCE.getSelectedAccount() == null)
|
||||
Controllers.dialog(Main.i18n("login.empty_username"));
|
||||
else
|
||||
LauncherHelper.INSTANCE.launch(version, null);
|
||||
});
|
||||
item.setOnScriptButtonClicked(e -> {
|
||||
if (Settings.INSTANCE.getSelectedAccount() == null)
|
||||
Controllers.dialog(Main.i18n("login.empty_username"));
|
||||
else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setInitialDirectory(profile.getRepository().getRunDirectory(version));
|
||||
chooser.setTitle(Main.i18n("version.launch_script.save"));
|
||||
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
? new FileChooser.ExtensionFilter(Main.i18n("extension.bat"), "*.bat")
|
||||
: new FileChooser.ExtensionFilter(Main.i18n("extension.sh"), "*.sh"));
|
||||
File file = chooser.showSaveDialog(Controllers.getStage());
|
||||
if (file != null)
|
||||
LauncherHelper.INSTANCE.launch(version, file);
|
||||
}
|
||||
});
|
||||
item.setOnSettingsButtonClicked(e -> {
|
||||
Controllers.getDecorator().showPage(Controllers.getVersionPage());
|
||||
Controllers.getVersionPage().load(version, profile);
|
||||
});
|
||||
item.setOnUpdateButtonClicked(event -> {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(Main.i18n("modpack.choose"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.i18n("modpack"), "*.zip"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile != null) {
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(null);
|
||||
try {
|
||||
TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, version, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(version)))
|
||||
.then(Task.of(Schedulers.javafx(), Controllers::closeDialog)).executor();
|
||||
pane.setExecutor(executor);
|
||||
pane.setTitle(Main.i18n("modpack.update"));
|
||||
executor.start();
|
||||
Controllers.dialog(pane);
|
||||
} catch (UnsupportedModpackException e) {
|
||||
Controllers.dialog(Main.i18n("modpack.unsupported"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE);
|
||||
} catch (MismatchedModpackTypeException e) {
|
||||
Controllers.dialog(Main.i18n("modpack.mismatched_type"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE);
|
||||
} catch (IOException e) {
|
||||
Controllers.dialog(Main.i18n("modpack.invalid"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
item.setOnMouseClicked(event -> {
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
rightClickedVersion = version;
|
||||
versionList.getSelectionModel().select(-1);
|
||||
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
|
||||
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
if (Settings.INSTANCE.getSelectedAccount() == null)
|
||||
Controllers.dialog(Main.i18n("login.empty_username"));
|
||||
else
|
||||
LauncherHelper.INSTANCE.launch(version, null);
|
||||
}
|
||||
});
|
||||
File iconFile = profile.getRepository().getVersionIcon(version);
|
||||
if (iconFile.exists())
|
||||
item.setImage(new Image("file:" + iconFile.getAbsolutePath()));
|
||||
return item;
|
||||
}
|
||||
|
||||
public void onProfilesLoading() {
|
||||
// TODO: Profiles
|
||||
}
|
||||
|
||||
public void onProfileChanged(ProfileChangedEvent event) {
|
||||
Platform.runLater(() -> loadVersions(event.getProfile()));
|
||||
}
|
||||
|
||||
private void loadVersions() {
|
||||
loadVersions(Settings.INSTANCE.getSelectedProfile());
|
||||
}
|
||||
|
||||
private void loadVersions(Profile profile) {
|
||||
this.profile = profile;
|
||||
List<Node> children = new LinkedList<>();
|
||||
List<Version> versions = new LinkedList<>(profile.getRepository().getVersions());
|
||||
for (Version version : versions) {
|
||||
children.add(buildNode(profile, version.getId(), GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version.getId())).orElse("Unknown")));
|
||||
}
|
||||
FXUtils.resetChildren(masonryPane, children);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onVersionManagement() {
|
||||
versionPopup.hide();
|
||||
switch (versionList.getSelectionModel().getSelectedIndex()) {
|
||||
case 0:
|
||||
VersionPage.renameVersion(profile, rightClickedVersion);
|
||||
break;
|
||||
case 1:
|
||||
VersionPage.deleteVersion(profile, rightClickedVersion);
|
||||
break;
|
||||
case 2:
|
||||
VersionPage.exportVersion(profile, rightClickedVersion);
|
||||
break;
|
||||
case 3:
|
||||
FXUtils.openFolder(profile.getRepository().getRunDirectory(rightClickedVersion));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import com.jfoenix.controls.JFXTabPane;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.mod.ModInfo;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ModController {
|
||||
@FXML
|
||||
private ScrollPane scrollPane;
|
||||
|
||||
@FXML private StackPane rootPane;
|
||||
|
||||
@FXML private VBox modPane;
|
||||
|
||||
@FXML private StackPane contentPane;
|
||||
@FXML private JFXSpinner spinner;
|
||||
|
||||
private JFXTabPane parentTab;
|
||||
private ModManager modManager;
|
||||
private String versionId;
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
|
||||
rootPane.setOnDragOver(event -> {
|
||||
if (event.getGestureSource() != rootPane && event.getDragboard().hasFiles())
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
rootPane.setOnDragDropped(event -> {
|
||||
List<File> mods = event.getDragboard().getFiles();
|
||||
Stream<File> stream = null;
|
||||
if (mods != null)
|
||||
stream = mods.stream()
|
||||
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)));
|
||||
if (stream != null && stream.findAny().isPresent()) {
|
||||
stream.forEach(it -> {
|
||||
try {
|
||||
modManager.addMod(versionId, it);
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
|
||||
}
|
||||
});
|
||||
loadMods(modManager, versionId);
|
||||
event.setDropCompleted(true);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
public void loadMods(ModManager modManager, String versionId) {
|
||||
this.modManager = modManager;
|
||||
this.versionId = versionId;
|
||||
Task.of(variables -> {
|
||||
synchronized (ModController.this) {
|
||||
Platform.runLater(() -> {
|
||||
rootPane.getChildren().remove(contentPane);
|
||||
spinner.setVisible(true);
|
||||
});
|
||||
|
||||
modManager.refreshMods(versionId);
|
||||
|
||||
// Surprisingly, if there are a great number of mods, this processing will cause a long UI pause,
|
||||
// constructing UI elements.
|
||||
// We must do this asynchronously.
|
||||
LinkedList<ModItem> list = new LinkedList<>();
|
||||
for (ModInfo modInfo : modManager.getMods(versionId)) {
|
||||
ModItem item = new ModItem(modInfo, i -> {
|
||||
modManager.removeMods(versionId, modInfo);
|
||||
loadMods(modManager, versionId);
|
||||
});
|
||||
modInfo.activeProperty().addListener((a, b, newValue) -> {
|
||||
if (newValue)
|
||||
item.getStyleClass().remove("disabled");
|
||||
else
|
||||
item.getStyleClass().add("disabled");
|
||||
});
|
||||
if (!modInfo.isActive())
|
||||
item.getStyleClass().add("disabled");
|
||||
|
||||
list.add(item);
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
rootPane.getChildren().add(contentPane);
|
||||
spinner.setVisible(false);
|
||||
});
|
||||
variables.set("list", list);
|
||||
}
|
||||
}).subscribe(Schedulers.javafx(), variables -> {
|
||||
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||
if (newValue != null && newValue.getUserData() == ModController.this)
|
||||
modPane.getChildren().setAll(variables.<List<ModItem>>get("list"));
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onAdd() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(Main.i18n("mods.choose_mod"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(Main.i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
|
||||
File res = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (res == null) return;
|
||||
Task.of(() -> modManager.addMod(versionId, res))
|
||||
.subscribe(Task.of(Schedulers.javafx(), () -> loadMods(modManager, versionId)));
|
||||
}
|
||||
|
||||
public void setParentTab(JFXTabPane parentTab) {
|
||||
this.parentTab = parentTab;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.mod.ModInfo;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class ModItem extends BorderPane {
|
||||
private final Label lblModFileName = new Label();
|
||||
private final Label lblModAuthor = new Label();
|
||||
private final JFXCheckBox chkEnabled = new JFXCheckBox();
|
||||
|
||||
public ModItem(ModInfo info, Consumer<ModItem> deleteCallback) {
|
||||
lblModFileName.setStyle("-fx-font-size: 15;");
|
||||
lblModAuthor.setStyle("-fx-font-size: 10;");
|
||||
BorderPane.setAlignment(chkEnabled, Pos.CENTER);
|
||||
setLeft(chkEnabled);
|
||||
|
||||
VBox center = new VBox();
|
||||
BorderPane.setAlignment(center, Pos.CENTER);
|
||||
center.getChildren().addAll(lblModFileName, lblModAuthor);
|
||||
setCenter(center);
|
||||
|
||||
JFXButton right = new JFXButton();
|
||||
right.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||
right.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(right, Pos.CENTER);
|
||||
right.setGraphic(SVG.close("black", 15, 15));
|
||||
setRight(right);
|
||||
|
||||
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
|
||||
JFXDepthManager.setDepth(this, 1);
|
||||
lblModFileName.setText(info.getFileName());
|
||||
lblModAuthor.setText(info.getName() + ", " + Main.i18n("archive.version") + ": " + info.getVersion() + ", " + Main.i18n("archive.game_version") + ": " + info.getGameVersion() + ", " + Main.i18n("archive.author") + ": " + info.getAuthors());
|
||||
chkEnabled.setSelected(info.isActive());
|
||||
chkEnabled.selectedProperty().addListener((a, b, newValue) -> {
|
||||
info.activeProperty().set(newValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
* 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
|
||||
@@ -13,116 +13,529 @@
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
|
||||
public final class SVG {
|
||||
private SVG() {
|
||||
}
|
||||
|
||||
private static Node createSVGPath(String d, String fill, double width, double height) {
|
||||
public interface SVGIcon {
|
||||
Node createIcon(ObjectBinding<? extends Paint> fill, double width, double height);
|
||||
}
|
||||
|
||||
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);
|
||||
path.setStyle("-fx-fill: " + fill + ";");
|
||||
if (fill != null)
|
||||
path.fillProperty().bind(fill);
|
||||
|
||||
if (width < 0 || height < 0) {
|
||||
StackPane pane = new StackPane(path);
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
return pane;
|
||||
}
|
||||
|
||||
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
|
||||
// default fill: white, width: 24, height 24
|
||||
|
||||
public static Node gear(String 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);
|
||||
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);
|
||||
}
|
||||
|
||||
public static Node back(String 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 static Node gearOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node close(String 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 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 static Node dotsVertical(String 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 cancel(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node delete(String 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 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 accountEdit(String 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 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 expand(String fill, double width, double 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 deleteOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z",
|
||||
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(String fill, double width, double 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(String 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 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(String 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 rocketLaunchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node script(String 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 launchOutline(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 pencil(String 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 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 refresh(String 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 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 folderOpen(String 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 pencilOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node update(String 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 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 closeCircle(String 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 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 checkCircle(String 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 folderOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node infoCircle(String 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 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 helpCircle(String 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 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 alert(String fill, double width, double 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 helpCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
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(String fill, double width, double height) {
|
||||
public static Node alertOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16", 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 plusCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
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 wrenchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
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);
|
||||
}
|
||||
|
||||
public static Node puzzle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node cube(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node pack(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node textureBox(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node gamepad(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node fire(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node monitorScreenshot(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node texture(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node alphaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node betaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node releaseCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node informationOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node handHearOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node mojang(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node microsoft(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M2,3H11V12H2V3M11,22H2V13H11V22M21,3V12H12V3H21M21,22H12V13H21V22Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node accountOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node accountGroupOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node accountArrowRightOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node styleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node downloadOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node applicationOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node earth(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node bell(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node contentSaveMoveOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node account(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node server(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node messageAlertOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node checkCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node closeCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node clockOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node magnify(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node menuUp(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M7,15L12,10L17,15H7Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node menuDown(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M7,10L12,15L17,10H7Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node restore(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node bug(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node discord(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node lan(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node thumbUpOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node thumbDownOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node selectAll(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"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",
|
||||
fill, width, height);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class SafeIntStringConverter extends StringConverter<Number> {
|
||||
public static final SafeIntStringConverter INSTANCE = new SafeIntStringConverter();
|
||||
|
||||
private SafeIntStringConverter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return Optional.ofNullable(string).map(Lang::toIntOrNull).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
return Optional.ofNullable(object).map(Object::toString).orElse("");
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.ui.construct.FileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.FontComboBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
public final class SettingsPage extends StackPane implements DecoratorPage {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("settings.launcher"));
|
||||
|
||||
@FXML
|
||||
private JFXTextField txtProxyHost;
|
||||
@FXML
|
||||
private JFXTextField txtProxyPort;
|
||||
@FXML
|
||||
private JFXTextField txtProxyUsername;
|
||||
@FXML
|
||||
private JFXTextField txtProxyPassword;
|
||||
@FXML
|
||||
private JFXTextField txtFontSize;
|
||||
@FXML
|
||||
private JFXComboBox<?> cboProxyType;
|
||||
@FXML
|
||||
private JFXComboBox<Label> cboLanguage;
|
||||
@FXML
|
||||
private JFXComboBox<?> cboDownloadSource;
|
||||
@FXML
|
||||
private FontComboBox cboFont;
|
||||
@FXML
|
||||
private FileItem fileCommonLocation;
|
||||
@FXML
|
||||
private Label lblDisplay;
|
||||
@FXML
|
||||
private Label lblUpdate;
|
||||
@FXML
|
||||
private Label lblUpdateSub;
|
||||
@FXML
|
||||
private JFXButton btnUpdate;
|
||||
@FXML
|
||||
private ScrollPane scroll;
|
||||
@FXML
|
||||
private MultiFileItem backgroundItem;
|
||||
@FXML
|
||||
private MultiFileItem themeItem;
|
||||
|
||||
{
|
||||
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
|
||||
|
||||
FXUtils.limitWidth(cboLanguage, 400);
|
||||
FXUtils.limitWidth(cboDownloadSource, 400);
|
||||
|
||||
FXUtils.smoothScrolling(scroll);
|
||||
|
||||
txtProxyHost.setText(Settings.INSTANCE.getProxyHost());
|
||||
txtProxyHost.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyHost(newValue));
|
||||
|
||||
txtProxyPort.setText(Settings.INSTANCE.getProxyPort());
|
||||
txtProxyPort.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyPort(newValue));
|
||||
|
||||
txtProxyUsername.setText(Settings.INSTANCE.getProxyUser());
|
||||
txtProxyUsername.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyUser(newValue));
|
||||
|
||||
txtProxyPassword.setText(Settings.INSTANCE.getProxyPass());
|
||||
txtProxyPassword.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyPass(newValue));
|
||||
|
||||
cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.INSTANCE.getDownloadProvider()));
|
||||
cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue())));
|
||||
|
||||
cboFont.getSelectionModel().select(Settings.INSTANCE.getFont().getFamily());
|
||||
cboFont.valueProperty().addListener((a, b, newValue) -> {
|
||||
Font font = Font.font(newValue, Settings.INSTANCE.getFont().getSize());
|
||||
Settings.INSTANCE.setFont(font);
|
||||
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
|
||||
});
|
||||
|
||||
txtFontSize.setText(Double.toString(Settings.INSTANCE.getFont().getSize()));
|
||||
txtFontSize.getValidators().add(new Validator(it -> Lang.toDoubleOrNull(it) != null));
|
||||
txtFontSize.textProperty().addListener((a, b, newValue) -> {
|
||||
if (txtFontSize.validate()) {
|
||||
Font font = Font.font(Settings.INSTANCE.getFont().getFamily(), Double.parseDouble(newValue));
|
||||
Settings.INSTANCE.setFont(font);
|
||||
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
|
||||
}
|
||||
});
|
||||
|
||||
lblDisplay.setStyle("-fx-font: " + Settings.INSTANCE.getFont().getSize() + " \"" + Settings.INSTANCE.getFont().getFamily() + "\";");
|
||||
|
||||
ObservableList<Label> list = FXCollections.observableArrayList();
|
||||
for (Locales.SupportedLocale locale : Locales.LOCALES)
|
||||
list.add(new Label(locale.getName(Settings.INSTANCE.getLocale().getResourceBundle())));
|
||||
|
||||
cboLanguage.setItems(list);
|
||||
cboLanguage.getSelectionModel().select(Locales.LOCALES.indexOf(Settings.INSTANCE.getLocale()));
|
||||
cboLanguage.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setLocale(Locales.getLocale(newValue.intValue())));
|
||||
|
||||
cboProxyType.getSelectionModel().select(Proxies.PROXIES.indexOf(Settings.INSTANCE.getProxyType()));
|
||||
cboProxyType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyType(Proxies.getProxyType(newValue.intValue())));
|
||||
|
||||
fileCommonLocation.setProperty(Settings.INSTANCE.commonPathProperty());
|
||||
|
||||
FXUtils.installTooltip(btnUpdate, 0, 5000, 0, new Tooltip(Main.i18n("update.tooltip")));
|
||||
checkUpdate();
|
||||
|
||||
// background
|
||||
backgroundItem.loadChildren(Collections.singletonList(
|
||||
backgroundItem.createChildren(Main.i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
|
||||
));
|
||||
|
||||
FXUtils.bindString(backgroundItem.getTxtCustom(), Settings.INSTANCE.backgroundImageProperty());
|
||||
|
||||
backgroundItem.setCustomUserData(EnumBackgroundImage.CUSTOM);
|
||||
backgroundItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Settings.INSTANCE.getBackgroundImageType()).findFirst().ifPresent(it -> it.setSelected(true));
|
||||
|
||||
Settings.INSTANCE.backgroundImageProperty().setChangedListener(it -> initBackgroundItemSubtitle());
|
||||
Settings.INSTANCE.backgroundImageTypeProperty().setChangedListener(it -> initBackgroundItemSubtitle());
|
||||
initBackgroundItemSubtitle();
|
||||
|
||||
backgroundItem.setToggleSelectedListener(newValue -> {
|
||||
Settings.INSTANCE.setBackgroundImageType((EnumBackgroundImage) newValue.getUserData());
|
||||
});
|
||||
|
||||
// theme
|
||||
themeItem.loadChildren(Arrays.asList(
|
||||
themeItem.createChildren(Main.i18n("color.blue"), Theme.BLUE),
|
||||
themeItem.createChildren(Main.i18n("color.dark_blue"), Theme.DARK_BLUE),
|
||||
themeItem.createChildren(Main.i18n("color.green"), Theme.GREEN),
|
||||
themeItem.createChildren(Main.i18n("color.orange"), Theme.ORANGE),
|
||||
themeItem.createChildren(Main.i18n("color.purple"), Theme.PURPLE),
|
||||
themeItem.createChildren(Main.i18n("color.red"), Theme.RED)
|
||||
));
|
||||
|
||||
themeItem.getGroup().getToggles().stream().filter(it -> Settings.INSTANCE.getTheme() == it.getUserData()).findFirst().ifPresent(it -> it.setSelected(true));
|
||||
themeItem.setToggleSelectedListener(newValue -> Settings.INSTANCE.setTheme((Theme) newValue.getUserData()));
|
||||
Settings.INSTANCE.themeProperty().setChangedListenerAndOperate(it ->
|
||||
themeItem.setSubtitle(Main.i18n("color." + it.name().toLowerCase())));
|
||||
}
|
||||
|
||||
private void initBackgroundItemSubtitle() {
|
||||
switch (Settings.INSTANCE.getBackgroundImageType()) {
|
||||
case DEFAULT:
|
||||
backgroundItem.setSubtitle(Main.i18n("launcher.background.default"));
|
||||
break;
|
||||
case CUSTOM:
|
||||
backgroundItem.setSubtitle(Settings.INSTANCE.getBackgroundImage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public void checkUpdate() {
|
||||
btnUpdate.setVisible(Main.UPDATE_CHECKER.isOutOfDate());
|
||||
|
||||
if (Main.UPDATE_CHECKER.isOutOfDate()) {
|
||||
lblUpdateSub.setText(Main.i18n("update.newest_version", Main.UPDATE_CHECKER.getNewVersion().toString()));
|
||||
lblUpdateSub.getStyleClass().setAll("update-label");
|
||||
|
||||
lblUpdate.setText(Main.i18n("update.found"));
|
||||
lblUpdate.getStyleClass().setAll("update-label");
|
||||
} else {
|
||||
lblUpdateSub.setText(Main.i18n("update.latest"));
|
||||
lblUpdateSub.getStyleClass().setAll("subtitle-label");
|
||||
|
||||
lblUpdate.setText(Main.i18n("update"));
|
||||
lblUpdate.getStyleClass().setAll();
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onUpdate() {
|
||||
Main.UPDATE_CHECKER.checkOutdate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
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.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;
|
||||
|
||||
public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>> extends SkinBase<T> {
|
||||
|
||||
public ToolbarListPageSkin(T skinnable) {
|
||||
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");
|
||||
|
||||
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.setAlignment(Pos.CENTER_LEFT);
|
||||
toolbar.setPickOnBounds(false);
|
||||
toolbar.getChildren().setAll(toolbarButtons);
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
{
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
ComponentList.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
VBox content = new VBox();
|
||||
|
||||
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
|
||||
root.getContent().add(scrollPane);
|
||||
}
|
||||
|
||||
spinnerPane.setContent(root);
|
||||
|
||||
getChildren().setAll(spinnerPane);
|
||||
}
|
||||
|
||||
public static Node wrap(Node node) {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.setPadding(new Insets(0, 5, 0, 2));
|
||||
stackPane.getChildren().setAll(node);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton(String text, SVG.SVGIcon creator, 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.setText(text);
|
||||
ret.setOnMouseClicked(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton2(String text, SVG.SVGIcon creator, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(creator.createIcon(Theme.blackFillBinding(), -1, -1)));
|
||||
ret.setText(text);
|
||||
ret.setOnMouseClicked(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, 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)));
|
||||
FXUtils.installFastTooltip(ret, tooltip);
|
||||
ret.setOnMouseClicked(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected abstract List<Node> initializeToolbar(T skinnable);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.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 org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class UpgradeDialog extends JFXDialogLayout {
|
||||
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
|
||||
{
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
}
|
||||
|
||||
{
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
WebView webView = new WebView();
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
try {
|
||||
WebEngine engine = webView.getEngine();
|
||||
engine.load(CHANGELOG_URL + remoteVersion.getChannel().channelName);
|
||||
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
|
||||
String viewURL = engine.getLoadWorker().getMessage().trim();
|
||||
if (!viewURL.startsWith(CHANGELOG_URL)) {
|
||||
engine.getLoadWorker().cancel();
|
||||
FXUtils.openLink(viewURL);
|
||||
}
|
||||
});
|
||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
||||
LOG.log(Level.WARNING, "WebView is missing or initialization failed", e);
|
||||
FXUtils.openLink(url);
|
||||
}
|
||||
setBody(webView);
|
||||
}
|
||||
|
||||
{
|
||||
JFXButton updateButton = new JFXButton(i18n("update.accept"));
|
||||
updateButton.getStyleClass().add("dialog-accept");
|
||||
updateButton.setOnMouseClicked(e -> updateRunnable.run());
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
|
||||
|
||||
setActions(updateButton, cancelButton);
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public final class VersionItem extends StackPane {
|
||||
@FXML
|
||||
private Pane icon;
|
||||
@FXML
|
||||
private VBox content;
|
||||
@FXML
|
||||
private StackPane header;
|
||||
@FXML
|
||||
private StackPane body;
|
||||
@FXML
|
||||
private JFXButton btnSettings;
|
||||
@FXML
|
||||
private JFXButton btnUpdate;
|
||||
@FXML
|
||||
private JFXButton btnLaunch;
|
||||
@FXML
|
||||
private JFXButton btnScript;
|
||||
@FXML
|
||||
private Label lblVersionName;
|
||||
@FXML
|
||||
private Label lblGameVersion;
|
||||
@FXML
|
||||
private ImageView iconView;
|
||||
|
||||
private EventHandler<? super MouseEvent> launchClickedHandler = null;
|
||||
|
||||
public VersionItem() {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/version-item.fxml");
|
||||
FXUtils.limitWidth(this, 160);
|
||||
FXUtils.limitHeight(this, 156);
|
||||
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0));
|
||||
btnSettings.setGraphic(SVG.gear("black", 15, 15));
|
||||
btnUpdate.setGraphic(SVG.update("black", 15, 15));
|
||||
btnLaunch.setGraphic(SVG.launch("black", 15, 15));
|
||||
btnScript.setGraphic(SVG.script("black", 15, 15));
|
||||
|
||||
FXUtils.installTooltip(btnSettings, 0, 5000, 0, new Tooltip(Main.i18n("version.settings")));
|
||||
FXUtils.installTooltip(btnUpdate, 0, 5000, 0, new Tooltip(Main.i18n("version.update")));
|
||||
FXUtils.installTooltip(btnLaunch, 0, 5000, 0, new Tooltip(Main.i18n("version.launch")));
|
||||
FXUtils.installTooltip(btnScript, 0, 5000, 0, new Tooltip(Main.i18n("version.launch_script")));
|
||||
|
||||
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 16, header.boundsInParentProperty(), icon.heightProperty()));
|
||||
FXUtils.limitSize(iconView, 32, 32);
|
||||
|
||||
setOnMouseClicked(e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY)
|
||||
if (e.getClickCount() == 2)
|
||||
Optional.ofNullable(launchClickedHandler).ifPresent(h -> h.handle(e));
|
||||
});
|
||||
}
|
||||
|
||||
public void setUpdate(boolean update) {
|
||||
btnUpdate.setVisible(update);
|
||||
}
|
||||
|
||||
public void setVersionName(String versionName) {
|
||||
lblVersionName.setText(versionName);
|
||||
}
|
||||
|
||||
public void setGameVersion(String gameVersion) {
|
||||
lblGameVersion.setText(gameVersion);
|
||||
}
|
||||
|
||||
public void setImage(Image image) {
|
||||
iconView.setImage(image);
|
||||
}
|
||||
|
||||
public void setOnSettingsButtonClicked(EventHandler<? super MouseEvent> handler) {
|
||||
btnSettings.setOnMouseClicked(handler);
|
||||
}
|
||||
|
||||
public void setOnScriptButtonClicked(EventHandler<? super MouseEvent> handler) {
|
||||
btnScript.setOnMouseClicked(handler);
|
||||
}
|
||||
|
||||
public void setOnLaunchButtonClicked(EventHandler<? super MouseEvent> handler) {
|
||||
launchClickedHandler = handler;
|
||||
btnLaunch.setOnMouseClicked(handler);
|
||||
}
|
||||
|
||||
public void setOnUpdateButtonClicked(EventHandler<? super MouseEvent> handler) {
|
||||
btnUpdate.setOnMouseClicked(handler);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
public final class VersionListItem extends StackPane {
|
||||
@FXML
|
||||
private StackPane imageViewContainer;
|
||||
@FXML
|
||||
private Label lblVersionName;
|
||||
@FXML
|
||||
private Label lblGameVersion;
|
||||
@FXML
|
||||
private ImageView imageView;
|
||||
private Runnable handler;
|
||||
|
||||
public VersionListItem(String versionName) {
|
||||
this(versionName, "");
|
||||
}
|
||||
|
||||
public VersionListItem(String versionName, String gameVersion) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/version-list-item.fxml");
|
||||
|
||||
lblVersionName.setText(versionName);
|
||||
lblGameVersion.setText(gameVersion);
|
||||
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
FXUtils.limitWidth(imageViewContainer, 32);
|
||||
FXUtils.limitHeight(imageViewContainer, 32);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onSettings() {
|
||||
handler.run();
|
||||
}
|
||||
|
||||
public void setOnSettingsButtonClicked(Runnable handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public void setVersionName(String versionName) {
|
||||
lblVersionName.setText(versionName);
|
||||
}
|
||||
|
||||
public void setGameVersion(String gameVersion) {
|
||||
lblGameVersion.setText(gameVersion);
|
||||
}
|
||||
|
||||
public void setImage(Image image, Rectangle2D viewport) {
|
||||
imageView.setImage(image);
|
||||
imageView.setViewport(viewport);
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXTabPane;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", null);
|
||||
|
||||
@FXML
|
||||
private VersionSettingsController versionSettingsController;
|
||||
@FXML
|
||||
private Tab modTab;
|
||||
@FXML
|
||||
private ModController modController;
|
||||
@FXML
|
||||
private InstallerController installerController;
|
||||
@FXML
|
||||
private JFXListView<?> browseList;
|
||||
@FXML
|
||||
private JFXListView<?> managementList;
|
||||
@FXML
|
||||
private JFXButton btnBrowseMenu;
|
||||
@FXML
|
||||
private JFXButton btnDelete;
|
||||
@FXML
|
||||
private JFXButton btnManagementMenu;
|
||||
@FXML
|
||||
private JFXButton btnExport;
|
||||
@FXML
|
||||
private StackPane rootPane;
|
||||
@FXML
|
||||
private StackPane contentPane;
|
||||
@FXML
|
||||
private JFXTabPane tabPane;
|
||||
|
||||
private JFXPopup browsePopup;
|
||||
private JFXPopup managementPopup;
|
||||
|
||||
private Profile profile;
|
||||
private String version;
|
||||
|
||||
{
|
||||
FXUtils.loadFXML(this, "/assets/fxml/version/version.fxml");
|
||||
|
||||
getChildren().removeAll(browseList, managementList);
|
||||
|
||||
browsePopup = new JFXPopup(browseList);
|
||||
managementPopup = new JFXPopup(managementList);
|
||||
|
||||
FXUtils.installTooltip(btnDelete, 0, 5000, 0, new Tooltip(Main.i18n("version.manage.remove")));
|
||||
FXUtils.installTooltip(btnBrowseMenu, 0, 5000, 0, new Tooltip(Main.i18n("settings.game.exploration")));
|
||||
FXUtils.installTooltip(btnManagementMenu, 0, 5000, 0, new Tooltip(Main.i18n("settings.game.management")));
|
||||
FXUtils.installTooltip(btnExport, 0, 5000, 0, new Tooltip(Main.i18n("modpack.export")));
|
||||
}
|
||||
|
||||
public void load(String id, Profile profile) {
|
||||
this.version = id;
|
||||
this.profile = profile;
|
||||
|
||||
title.set(Main.i18n("settings.game") + " - " + id);
|
||||
|
||||
versionSettingsController.loadVersionSetting(profile, id);
|
||||
modController.setParentTab(tabPane);
|
||||
modTab.setUserData(modController);
|
||||
modController.loadMods(profile.getModManager(), id);
|
||||
installerController.loadVersion(profile, id);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onBrowseMenu() {
|
||||
browseList.getSelectionModel().select(-1);
|
||||
browsePopup.show(btnBrowseMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -12, 15);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onManagementMenu() {
|
||||
managementList.getSelectionModel().select(-1);
|
||||
managementPopup.show(btnManagementMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -12, 15);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onDelete() {
|
||||
deleteVersion(profile, version);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onExport() {
|
||||
exportVersion(profile, version);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onBrowse() {
|
||||
String sub;
|
||||
switch (browseList.getSelectionModel().getSelectedIndex()) {
|
||||
case 0:
|
||||
sub = "";
|
||||
break;
|
||||
case 1:
|
||||
sub = "mods";
|
||||
break;
|
||||
case 2:
|
||||
sub = "coremods";
|
||||
break;
|
||||
case 3:
|
||||
sub = "config";
|
||||
break;
|
||||
case 4:
|
||||
sub = "resourcepacks";
|
||||
break;
|
||||
case 5:
|
||||
sub = "screenshots";
|
||||
break;
|
||||
case 6:
|
||||
sub = "saves";
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
}
|
||||
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub));
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onManagement() {
|
||||
switch (managementList.getSelectionModel().getSelectedIndex()) {
|
||||
case 0: // rename a version
|
||||
renameVersion(profile, version);
|
||||
break;
|
||||
case 1: // remove a version
|
||||
deleteVersion(profile, version);
|
||||
break;
|
||||
case 2: // redownload asset index
|
||||
new GameAssetIndexDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version).resolve(profile.getRepository())).start();
|
||||
break;
|
||||
case 3: // delete libraries
|
||||
FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries"));
|
||||
break;
|
||||
case 4:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public static void deleteVersion(Profile profile, String version) {
|
||||
Controllers.confirmDialog(Main.i18n("version.manage.remove.confirm", version), Main.i18n("message.confirm"), () -> {
|
||||
if (profile.getRepository().removeVersionFromDisk(version)) {
|
||||
profile.getRepository().refreshVersionsAsync().start();
|
||||
Controllers.navigate(null);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
public static void renameVersion(Profile profile, String version) {
|
||||
Controllers.inputDialog(Main.i18n("version.manage.rename.message"), res -> {
|
||||
if (profile.getRepository().renameVersion(version, res)) {
|
||||
profile.getRepository().refreshVersionsAsync().start();
|
||||
Controllers.navigate(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void exportVersion(Profile profile, String version) {
|
||||
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), Main.i18n("modpack.wizard"));
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.NumberValidator;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class VersionSettingsController {
|
||||
private VersionSetting lastVersionSetting = null;
|
||||
private Profile profile;
|
||||
private String versionId;
|
||||
|
||||
@FXML private VBox rootPane;
|
||||
@FXML private ScrollPane scroll;
|
||||
@FXML private JFXTextField txtWidth;
|
||||
@FXML private JFXTextField txtHeight;
|
||||
@FXML private JFXTextField txtMaxMemory;
|
||||
@FXML private JFXTextField txtJVMArgs;
|
||||
@FXML private JFXTextField txtGameArgs;
|
||||
@FXML private JFXTextField txtMetaspace;
|
||||
@FXML private JFXTextField txtWrapper;
|
||||
@FXML private JFXTextField txtPrecallingCommand;
|
||||
@FXML private JFXTextField txtServerIP;
|
||||
@FXML private ComponentList advancedSettingsPane;
|
||||
@FXML private JFXComboBox<?> cboLauncherVisibility;
|
||||
@FXML private JFXCheckBox chkFullscreen;
|
||||
@FXML private Label lblPhysicalMemory;
|
||||
@FXML private JFXToggleButton chkNoJVMArgs;
|
||||
@FXML private JFXToggleButton chkNoCommon;
|
||||
@FXML private JFXToggleButton chkNoGameCheck;
|
||||
@FXML private MultiFileItem globalItem;
|
||||
@FXML private MultiFileItem javaItem;
|
||||
@FXML private MultiFileItem gameDirItem;
|
||||
@FXML private JFXToggleButton chkShowLogs;
|
||||
@FXML private JFXButton btnIconSelection;
|
||||
@FXML private ImageView iconView;
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
lblPhysicalMemory.setText(Main.i18n("settings.physical_memory") + ": " + OperatingSystem.TOTAL_MEMORY + "MB");
|
||||
|
||||
FXUtils.smoothScrolling(scroll);
|
||||
|
||||
double limitWidth = 300;
|
||||
FXUtils.limitWidth(txtMaxMemory, limitWidth);
|
||||
FXUtils.limitWidth(cboLauncherVisibility, limitWidth);
|
||||
|
||||
double limitHeight = 10;
|
||||
FXUtils.limitHeight(chkNoJVMArgs, limitHeight);
|
||||
FXUtils.limitHeight(chkNoCommon, limitHeight);
|
||||
FXUtils.limitHeight(chkNoGameCheck, limitHeight);
|
||||
FXUtils.limitHeight(chkShowLogs, limitHeight);
|
||||
|
||||
NumberValidator nonnull = new NumberValidator("Must be a number.", false);
|
||||
NumberValidator nullable = new NumberValidator("Must be a number.", true);
|
||||
|
||||
txtWidth.setValidators(nonnull);
|
||||
FXUtils.setValidateWhileTextChanged(txtWidth);
|
||||
txtHeight.setValidators(nonnull);
|
||||
FXUtils.setValidateWhileTextChanged(txtHeight);
|
||||
txtMaxMemory.setValidators(nonnull);
|
||||
FXUtils.setValidateWhileTextChanged(txtMaxMemory);
|
||||
txtMetaspace.setValidators(nullable);
|
||||
FXUtils.setValidateWhileTextChanged(txtMetaspace);
|
||||
|
||||
Task.of(variables -> {
|
||||
variables.set("list", JavaVersion.getJREs().values().stream().map(javaVersion ->
|
||||
javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion)
|
||||
).collect(Collectors.toList()));
|
||||
}).subscribe(Schedulers.javafx(), variables ->
|
||||
javaItem.loadChildren(variables.<Collection<Node>>get("list"))
|
||||
);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
javaItem.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
|
||||
|
||||
gameDirItem.loadChildren(Arrays.asList(
|
||||
gameDirItem.createChildren(Main.i18n("settings.advanced.game_dir.default"), EnumGameDirectory.ROOT_FOLDER),
|
||||
gameDirItem.createChildren(Main.i18n("settings.advanced.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER)
|
||||
));
|
||||
|
||||
globalItem.loadChildren(Arrays.asList(
|
||||
globalItem.createChildren(Main.i18n("settings.type.global"), true),
|
||||
globalItem.createChildren(Main.i18n("settings.type.special"), false)
|
||||
));
|
||||
|
||||
FXUtils.installTooltip(btnIconSelection, 0, 5000, 0, new Tooltip(Main.i18n("button.edit")));
|
||||
}
|
||||
|
||||
public void loadVersionSetting(Profile profile, String versionId) {
|
||||
rootPane.getChildren().remove(advancedSettingsPane);
|
||||
|
||||
this.profile = profile;
|
||||
this.versionId = versionId;
|
||||
|
||||
VersionSetting versionSetting = profile.getVersionSetting(versionId);
|
||||
|
||||
gameDirItem.setDisable(profile.getRepository().isModpack(versionId));
|
||||
globalItem.setDisable(profile.getRepository().isModpack(versionId));
|
||||
|
||||
if (lastVersionSetting != null) {
|
||||
FXUtils.unbindInt(txtWidth, lastVersionSetting.widthProperty());
|
||||
FXUtils.unbindInt(txtHeight, lastVersionSetting.heightProperty());
|
||||
FXUtils.unbindInt(txtMaxMemory, lastVersionSetting.maxMemoryProperty());
|
||||
FXUtils.unbindString(javaItem.getTxtCustom(), lastVersionSetting.javaDirProperty());
|
||||
FXUtils.unbindString(gameDirItem.getTxtCustom(), lastVersionSetting.gameDirProperty());
|
||||
FXUtils.unbindString(txtJVMArgs, lastVersionSetting.javaArgsProperty());
|
||||
FXUtils.unbindString(txtGameArgs, lastVersionSetting.minecraftArgsProperty());
|
||||
FXUtils.unbindString(txtMetaspace, lastVersionSetting.permSizeProperty());
|
||||
FXUtils.unbindString(txtWrapper, lastVersionSetting.wrapperProperty());
|
||||
FXUtils.unbindString(txtPrecallingCommand, lastVersionSetting.preLaunchCommandProperty());
|
||||
FXUtils.unbindString(txtServerIP, lastVersionSetting.serverIpProperty());
|
||||
FXUtils.unbindBoolean(chkFullscreen, lastVersionSetting.fullscreenProperty());
|
||||
FXUtils.unbindBoolean(chkNoGameCheck, lastVersionSetting.notCheckGameProperty());
|
||||
FXUtils.unbindBoolean(chkNoCommon, lastVersionSetting.noCommonProperty());
|
||||
FXUtils.unbindBoolean(chkShowLogs, lastVersionSetting.showLogsProperty());
|
||||
FXUtils.unbindEnum(cboLauncherVisibility);
|
||||
}
|
||||
|
||||
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
|
||||
FXUtils.bindInt(txtHeight, versionSetting.heightProperty());
|
||||
FXUtils.bindInt(txtMaxMemory, versionSetting.maxMemoryProperty());
|
||||
FXUtils.bindString(javaItem.getTxtCustom(), versionSetting.javaDirProperty());
|
||||
FXUtils.bindString(gameDirItem.getTxtCustom(), versionSetting.gameDirProperty());
|
||||
FXUtils.bindString(txtJVMArgs, versionSetting.javaArgsProperty());
|
||||
FXUtils.bindString(txtGameArgs, versionSetting.minecraftArgsProperty());
|
||||
FXUtils.bindString(txtMetaspace, versionSetting.permSizeProperty());
|
||||
FXUtils.bindString(txtWrapper, versionSetting.wrapperProperty());
|
||||
FXUtils.bindString(txtPrecallingCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.bindString(txtServerIP, versionSetting.serverIpProperty());
|
||||
FXUtils.bindBoolean(chkFullscreen, versionSetting.fullscreenProperty());
|
||||
FXUtils.bindBoolean(chkNoGameCheck, versionSetting.notCheckGameProperty());
|
||||
FXUtils.bindBoolean(chkNoCommon, versionSetting.noCommonProperty());
|
||||
FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty());
|
||||
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
|
||||
|
||||
boolean flag = false;
|
||||
Toggle defaultToggle = null;
|
||||
for (Toggle toggle : javaItem.getGroup().getToggles()) {
|
||||
if (toggle.getUserData() == Lang.invoke(versionSetting::getJavaVersion)) {
|
||||
toggle.setSelected(true);
|
||||
flag = true;
|
||||
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
|
||||
defaultToggle = toggle;
|
||||
}
|
||||
}
|
||||
|
||||
javaItem.setToggleSelectedListener(newValue -> {
|
||||
if (javaItem.isCustomToggle(newValue)) {
|
||||
versionSetting.setJava("Custom");
|
||||
} else {
|
||||
versionSetting.setJava(((JavaVersion) newValue.getUserData()).getVersion());
|
||||
}
|
||||
});
|
||||
|
||||
if (!flag) {
|
||||
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
|
||||
}
|
||||
|
||||
versionSetting.javaDirProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
|
||||
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
|
||||
initJavaSubtitle(versionSetting);
|
||||
|
||||
if (versionSetting.isUsesGlobal())
|
||||
globalItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Boolean.TRUE).findFirst().ifPresent(it -> it.setSelected(true));
|
||||
else
|
||||
globalItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Boolean.FALSE).findFirst().ifPresent(it -> it.setSelected(true));
|
||||
globalItem.setToggleSelectedListener(newValue -> {
|
||||
if ((Boolean) newValue.getUserData())
|
||||
profile.globalizeVersionSetting(versionId);
|
||||
else
|
||||
profile.specializeVersionSetting(versionId);
|
||||
|
||||
Platform.runLater(() -> loadVersionSetting(profile, versionId));
|
||||
});
|
||||
|
||||
versionSetting.usesGlobalProperty().setChangedListenerAndOperate(it ->
|
||||
globalItem.setSubtitle(Main.i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special")));
|
||||
|
||||
gameDirItem.getGroup().getToggles().stream()
|
||||
.filter(it -> it.getUserData() == versionSetting.getGameDirType())
|
||||
.findFirst().ifPresent(toggle -> toggle.setSelected(true));
|
||||
|
||||
gameDirItem.setCustomUserData(EnumGameDirectory.CUSTOM);
|
||||
gameDirItem.setToggleSelectedListener(newValue -> {
|
||||
versionSetting.setGameDirType((EnumGameDirectory) newValue.getUserData());
|
||||
});
|
||||
|
||||
versionSetting.gameDirProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
|
||||
versionSetting.gameDirTypeProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
|
||||
initGameDirSubtitle(versionSetting);
|
||||
|
||||
lastVersionSetting = versionSetting;
|
||||
|
||||
loadIcon();
|
||||
}
|
||||
|
||||
private void initJavaSubtitle(VersionSetting versionSetting) {
|
||||
Task.of(variables -> variables.set("java", versionSetting.getJavaVersion()))
|
||||
.subscribe(Task.of(Schedulers.javafx(),
|
||||
variables -> javaItem.setSubtitle(variables.<JavaVersion>getOptional("java")
|
||||
.map(JavaVersion::getBinary).map(File::getAbsolutePath).orElse("Invalid Java Directory"))));
|
||||
}
|
||||
|
||||
private void initGameDirSubtitle(VersionSetting versionSetting) {
|
||||
gameDirItem.setSubtitle(profile.getRepository().getRunDirectory(versionId).getAbsolutePath());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onShowAdvanced() {
|
||||
if (!rootPane.getChildren().contains(advancedSettingsPane))
|
||||
rootPane.getChildren().add(advancedSettingsPane);
|
||||
else
|
||||
rootPane.getChildren().remove(advancedSettingsPane);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onExploreIcon() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.i18n("extension.png"), "*.png"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile != null) {
|
||||
File iconFile = profile.getRepository().getVersionIcon(versionId);
|
||||
try {
|
||||
FileUtils.copyFile(selectedFile, iconFile);
|
||||
loadIcon();
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to copy icon file from " + selectedFile + " to " + iconFile, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadIcon() {
|
||||
File iconFile = profile.getRepository().getVersionIcon(versionId);
|
||||
if (iconFile.exists())
|
||||
iconView.setImage(new Image("file:" + iconFile.getAbsolutePath()));
|
||||
else
|
||||
iconView.setImage(FXUtils.DEFAULT_ICON);
|
||||
FXUtils.limitSize(iconView, 32, 32);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 853 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user