Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -0,0 +1 @@
|
||||
*.exe filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Java CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-package: jdk+fx
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
@@ -17,20 +17,25 @@ NVIDIA
|
||||
/build/
|
||||
/HMCL/build/
|
||||
/HMCLCore/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
|
||||
# idea
|
||||
.idea
|
||||
/out/
|
||||
/HMCL/out/
|
||||
/HMCLCore/out/
|
||||
/HMCLTransformerDiscoveryService/out/
|
||||
|
||||
# eclipse
|
||||
/bin/
|
||||
/HMCL/bin/
|
||||
/HMCLCore/bin/
|
||||
/HMCLTransformerDiscoveryService/bin/
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
# netbeans
|
||||
.nb-gradle
|
||||
|
||||
*.exe
|
||||
+68
-18
@@ -1,6 +1,15 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '4.0.0'
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '7.0.0'
|
||||
}
|
||||
|
||||
import java.nio.file.FileSystems
|
||||
@@ -27,8 +36,8 @@ 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")
|
||||
}
|
||||
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
@@ -69,35 +78,70 @@ 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) }
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.',
|
||||
'Main-Class': mainClassName,
|
||||
'Multi-Release': 'true',
|
||||
'Implementation-Version': version
|
||||
sourceSets {
|
||||
java11 {
|
||||
java {
|
||||
srcDirs = ['src/main/java11']
|
||||
}
|
||||
}
|
||||
finalizedBy shadowJar
|
||||
}
|
||||
|
||||
compileJava11Java {
|
||||
javaCompiler = javaToolchains.compilerFor {
|
||||
languageVersion = JavaLanguageVersion.of(11)
|
||||
}
|
||||
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
dependsOn shadowJar
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
classifier = null
|
||||
|
||||
exclude 'META-INF/maven/**'
|
||||
exclude 'META-INF/NOTICE.txt'
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
|
||||
dependencies {
|
||||
exclude(dependency('org.jetbrains:annotations'))
|
||||
manifest {
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
|
||||
'Main-Class': mainClassName,
|
||||
'Multi-Release': 'true',
|
||||
'Implementation-Version': version,
|
||||
'Add-Opens': [
|
||||
'java.base/java.lang',
|
||||
'java.base/java.lang.reflect',
|
||||
'javafx.graphics/javafx.css',
|
||||
'javafx.base/com.sun.javafx.runtime',
|
||||
'javafx.controls/com.sun.javafx.scene.control.behavior',
|
||||
'javafx.controls/javafx.scene.control.skin',
|
||||
'javafx.controls/com.sun.javafx.scene.control',
|
||||
'javafx.base/com.sun.javafx.binding',
|
||||
'javafx.base/com.sun.javafx.event',
|
||||
'javafx.graphics/com.sun.javafx.stage'
|
||||
].join(" "),
|
||||
'Add-Exports': [
|
||||
'java.base/jdk.internal.loader',
|
||||
'javafx.controls/com.sun.javafx.scene.control.behavior',
|
||||
'javafx.controls/javafx.scene.control.skin',
|
||||
'javafx.controls/com.sun.javafx.scene.control',
|
||||
'javafx.base/com.sun.javafx.binding',
|
||||
'javafx.graphics/com.sun.javafx.stage',
|
||||
'javafx.base/com.sun.javafx.event'
|
||||
].join(" ")
|
||||
}
|
||||
|
||||
doLast {
|
||||
repack(jar.archivePath)
|
||||
repack(jar.archivePath) // see repack()
|
||||
attachSignature(jar.archivePath)
|
||||
createChecksum(jar.archivePath)
|
||||
}
|
||||
@@ -126,6 +170,11 @@ processResources {
|
||||
|
||||
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) {
|
||||
@@ -140,6 +189,7 @@ task makePack(dependsOn: jar) {
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -157,4 +207,4 @@ task makeExecutables(dependsOn: jar) doLast {
|
||||
|
||||
build.dependsOn makePackXz
|
||||
build.dependsOn makePackGz
|
||||
build.dependsOn makeExecutables
|
||||
build.dependsOn makeExecutables
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -22,39 +22,46 @@ import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
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.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
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 (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to load config", e);
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
|
||||
}
|
||||
|
||||
@@ -64,8 +71,8 @@ public final class Launcher extends Application {
|
||||
// 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());
|
||||
|
||||
initIcon();
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
@@ -76,9 +83,25 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
TaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
||||
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
||||
|
||||
try {
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
@@ -108,7 +131,6 @@ public final class Launcher extends Application {
|
||||
Schedulers.shutdown();
|
||||
Controllers.shutdown();
|
||||
Platform.exit();
|
||||
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,6 +159,11 @@ public final class Launcher extends Application {
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
File jarFile = new File(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath());
|
||||
String ext = FileUtils.getExtension(jarFile);
|
||||
if ("jar".equals(ext) || "exe".equals(ext))
|
||||
result.add(jarFile);
|
||||
}
|
||||
if (result.isEmpty())
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -17,18 +17,24 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import javax.net.ssl.*;
|
||||
import javax.swing.*;
|
||||
import java.io.File;
|
||||
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.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
@@ -42,17 +48,20 @@ public final class Main {
|
||||
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
System.setProperty("javafx.autoproxy.disable", "true");
|
||||
|
||||
checkJavaFX();
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::checkDSTRootCAX3, "CA Certificate Check", true);
|
||||
thread(() -> {
|
||||
fixLetsEncrypt();
|
||||
checkDSTRootCAX3();
|
||||
}, "CA Certificate Check", true);
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
return;
|
||||
}
|
||||
checkJavaFX();
|
||||
|
||||
// Fix title bar not displaying in GTK systems
|
||||
System.setProperty("jdk.gtk.version", "2");
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
@@ -68,9 +77,13 @@ public final class Main {
|
||||
|
||||
private static void checkJavaFX() {
|
||||
try {
|
||||
Class.forName("javafx.application.Application");
|
||||
} catch (ClassNotFoundException e) {
|
||||
showErrorAndExit(i18n("fatal.missing_javafx"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,4 +128,34 @@ public final class Main {
|
||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
}
|
||||
|
||||
static void fixLetsEncrypt() {
|
||||
try {
|
||||
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());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -17,11 +17,12 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Stores metadata about this application.
|
||||
*/
|
||||
@@ -39,5 +40,14 @@ public final class Metadata {
|
||||
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
|
||||
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
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.
|
||||
return Paths.get(home, ".cache", "hmcl");
|
||||
}
|
||||
return OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -21,8 +21,16 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -48,4 +56,29 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
res.put("${launcher_version}", Metadata.VERSION);
|
||||
return res;
|
||||
}
|
||||
|
||||
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,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -19,26 +19,40 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||
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.ProxyManager;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
|
||||
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);
|
||||
@@ -49,26 +63,41 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameDirectoryType getGameDirectoryType(String id) {
|
||||
if (beingModpackVersions.contains(id) || isModpack(id)) {
|
||||
return GameDirectoryType.VERSION_FOLDER;
|
||||
} else {
|
||||
return getVersionSetting(id).getGameDirType();
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@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");
|
||||
@@ -94,19 +123,57 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
clean(getRunDirectory(id));
|
||||
}
|
||||
|
||||
private File getVersionSettingFile(String id) {
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
File srcDir = getVersionRoot(srcId);
|
||||
File dstDir = getVersionRoot(dstId);
|
||||
|
||||
if (dstDir.exists()) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
oldVersionSetting.setUsesGlobal(false);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,18 +182,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;
|
||||
}
|
||||
|
||||
@@ -135,17 +202,27 @@ 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 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");
|
||||
}
|
||||
@@ -154,29 +231,28 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
if (id == null || !isLoaded())
|
||||
return newImage("/assets/img/grass.png");
|
||||
|
||||
Version version = getVersion(id);
|
||||
Version version = getVersion(id).resolve(this);
|
||||
File iconFile = getVersionIconFile(id);
|
||||
if (iconFile.exists())
|
||||
return new Image("file:" + iconFile.getAbsolutePath());
|
||||
else if (!version.getPatches().isEmpty() ||
|
||||
version.getMainClass() != null &&
|
||||
("net.minecraft.launchwrapper.Launch".equals(version.getMainClass())
|
||||
else if (version.getMainClass() != null &&
|
||||
(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|
||||
|| version.getMainClass().startsWith("net.fabricmc")
|
||||
|| "cpw.mods.modlauncher.Launcher".equals(version.getMainClass())))
|
||||
|| LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass())))
|
||||
return newImage("/assets/img/furnace.png");
|
||||
else
|
||||
return newImage("/assets/img/grass.png");
|
||||
}
|
||||
|
||||
public boolean saveVersionSetting(String id) {
|
||||
if (!versionSettings.containsKey(id))
|
||||
if (!localVersionSettings.containsKey(id))
|
||||
return false;
|
||||
File file = getVersionSettingFile(id);
|
||||
File file = getLocalVersionSettingFile(id);
|
||||
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
|
||||
return false;
|
||||
|
||||
try {
|
||||
FileUtils.writeText(file, GSON.toJson(versionSettings.get(id)));
|
||||
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
|
||||
@@ -190,9 +266,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
* @return specialized version setting, null if given version does not exist.
|
||||
*/
|
||||
public VersionSetting specializeVersionSetting(String id) {
|
||||
VersionSetting vs = getVersionSetting(id);
|
||||
VersionSetting vs = getLocalVersionSetting(id);
|
||||
if (vs == null)
|
||||
vs = createVersionSetting(id);
|
||||
vs = createLocalVersionSetting(id);
|
||||
if (vs == null)
|
||||
return null;
|
||||
vs.setUsesGlobal(false);
|
||||
@@ -200,7 +276,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
public void globalizeVersionSetting(String id) {
|
||||
VersionSetting vs = getVersionSetting(id);
|
||||
VersionSetting vs = getLocalVersionSetting(id);
|
||||
if (vs != null)
|
||||
vs.setUsesGlobal(true);
|
||||
}
|
||||
@@ -209,6 +285,54 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return FORBIDDEN.contains(id);
|
||||
}
|
||||
|
||||
public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException {
|
||||
VersionSetting vs = getVersionSetting(version);
|
||||
|
||||
JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionName(Metadata.TITLE)
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory(vs.getMaxMemory())
|
||||
.setMinMemory(vs.getMinMemory())
|
||||
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
|
||||
.setWidth(vs.getWidth())
|
||||
.setHeight(vs.getHeight())
|
||||
.setFullscreen(vs.isFullscreen())
|
||||
.setServerIp(vs.getServerIp())
|
||||
.setWrapper(vs.getWrapper())
|
||||
.setPrecalledCommand(vs.getPreLaunchCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs());
|
||||
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
|
||||
public File getModpackConfiguration(String version) {
|
||||
return new File(getVersionRoot(version), "modpack.cfg");
|
||||
@@ -222,6 +346,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
beingModpackVersions.remove(id);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Export the game to a mod pack file.
|
||||
*/
|
||||
public class HMCLModpackExportTask extends Task<Void> {
|
||||
private final DefaultGameRepository repository;
|
||||
private final String version;
|
||||
private final List<String> whitelist;
|
||||
private final Modpack modpack;
|
||||
private final File output;
|
||||
|
||||
/**
|
||||
* @param output mod pack file.
|
||||
* @param version to locate version.json
|
||||
*/
|
||||
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.whitelist = whitelist;
|
||||
this.modpack = modpack;
|
||||
this.output = output;
|
||||
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed()) output.delete();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
ArrayList<String> blackList = new ArrayList<>(ModAdviser.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 (Zipper zip = new Zipper(output.toPath())) {
|
||||
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> {
|
||||
if (path.isEmpty())
|
||||
return true;
|
||||
for (String s : blackList)
|
||||
if (path.equals(s))
|
||||
return false;
|
||||
for (String s : whitelist)
|
||||
if (path.equals(s))
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
Version mv = repository.getResolvedVersion(version);
|
||||
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
|
||||
.orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + version));
|
||||
zip.putTextFile(JsonUtils.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
|
||||
zip.putTextFile(JsonUtils.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -19,7 +19,8 @@ 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.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;
|
||||
@@ -34,17 +35,20 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final File zipFile;
|
||||
private final String name;
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependency;
|
||||
private final Modpack modpack;
|
||||
private final List<Task<?>> dependencies = new LinkedList<>();
|
||||
private final List<Task<?>> dependents = new LinkedList<>();
|
||||
|
||||
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;
|
||||
@@ -73,6 +77,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/minecraft", it -> !"pack.json".equals(it), config));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,9 +93,26 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
|
||||
Version version = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
|
||||
dependencies.add(repository.save(version));
|
||||
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getStages() {
|
||||
return Stream.concat(
|
||||
dependents.stream().flatMap(task -> task.getStages().stream()),
|
||||
Stream.of("hmcl.modpack")
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "HMCL";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -20,58 +20,54 @@ package org.jackhuang.hmcl.game;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.download.java.JavaRepository;
|
||||
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
|
||||
import org.jackhuang.hmcl.launch.PermissionException;
|
||||
import org.jackhuang.hmcl.launch.ProcessCreationException;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
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.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.LogWindow;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -96,6 +92,7 @@ public final class LauncherHelper {
|
||||
this.setting = profile.getVersionSetting(selectedVersion);
|
||||
this.launcherVisibility = setting.getLauncherVisibility();
|
||||
this.showLogs = setting.isShowLogs();
|
||||
this.launchingStepsPane.setTitle(i18n("version.launch"));
|
||||
}
|
||||
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
|
||||
@@ -115,9 +112,9 @@ public final class LauncherHelper {
|
||||
try {
|
||||
checkGameState(profile, setting, version, () -> {
|
||||
Controllers.dialog(launchingStepsPane);
|
||||
Schedulers.newThread().execute(this::launch0);
|
||||
Schedulers.defaultScheduler().execute(this::launch0);
|
||||
});
|
||||
} catch (InterruptedException ignore) {
|
||||
} catch (InterruptedException | RejectedExecutionException ignore) {
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -133,28 +130,35 @@ public final class LauncherHelper {
|
||||
DefaultDependencyManager dependencyManager = profile.getDependency();
|
||||
Version version = MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion));
|
||||
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
|
||||
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
|
||||
CountDownLatch launchingLatch = new CountDownLatch(1);
|
||||
|
||||
TaskExecutor executor = Task.runAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
|
||||
TaskExecutor executor = dependencyManager.checkPatchCompletionAsync(repository.getVersion(selectedVersion), integrityCheck)
|
||||
.thenComposeAsync(Task.allOf(
|
||||
Task.composeAsync(() -> {
|
||||
if (setting.isNotCheckGame())
|
||||
return null;
|
||||
else
|
||||
return dependencyManager.checkGameCompletionAsync(version, integrityCheck);
|
||||
}), Task.composeAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
|
||||
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new CurseCompletionTask(dependencyManager, selectedVersion);
|
||||
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
|
||||
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
|
||||
else
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}))).withStage("launch.state.dependencies")
|
||||
.thenComposeAsync(() -> {
|
||||
if (setting.isNotCheckGame())
|
||||
return null;
|
||||
else
|
||||
return dependencyManager.checkGameCompletionAsync(version);
|
||||
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version)).orElse(null);
|
||||
})
|
||||
.thenRunAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS))
|
||||
.thenComposeAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
|
||||
if ("Curse".equals(configuration.getType()))
|
||||
return new CurseCompletionTask(dependencyManager, selectedVersion);
|
||||
else
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.thenRunAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN))
|
||||
.thenSupplyAsync(i18n("account.methods"), () -> {
|
||||
.thenComposeAsync(Task.supplyAsync(() -> {
|
||||
try {
|
||||
return account.logIn();
|
||||
} catch (CredentialExpiredException e) {
|
||||
@@ -164,31 +168,27 @@ public final class LauncherHelper {
|
||||
LOG.warning("Authentication failed, try playing offline: " + e);
|
||||
return account.playOffline().orElseThrow(() -> e);
|
||||
}
|
||||
})
|
||||
.thenApplyAsync(Schedulers.javafx(), authInfo -> {
|
||||
emitStatus(LoadingState.LAUNCHING);
|
||||
return authInfo;
|
||||
})
|
||||
.thenApplyAsync(authInfo -> new HMCLGameLauncher(
|
||||
repository,
|
||||
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
|
||||
authInfo,
|
||||
setting.toLaunchOptions(profile.getGameDir()),
|
||||
launcherVisibility == LauncherVisibility.CLOSE
|
||||
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
|
||||
: new HMCLProcessListener(authInfo, gameVersion.isPresent())
|
||||
))
|
||||
.thenComposeAsync(launcher -> { // launcher is prev task's result
|
||||
}).withStage("launch.state.logging_in"))
|
||||
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
||||
return new HMCLGameLauncher(
|
||||
repository,
|
||||
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
|
||||
authInfo,
|
||||
repository.getLaunchOptions(selectedVersion, profile.getGameDir(), !setting.isNotCheckJVM()),
|
||||
launcherVisibility == LauncherVisibility.CLOSE
|
||||
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
|
||||
: new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent())
|
||||
);
|
||||
}).thenComposeAsync(launcher -> { // launcher is prev task's result
|
||||
if (scriptFile == null) {
|
||||
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
|
||||
return Task.supplyAsync(launcher::launch);
|
||||
} else {
|
||||
return new LaunchTask<ManagedProcess>(() -> {
|
||||
return Task.supplyAsync(() -> {
|
||||
launcher.makeLaunchScript(scriptFile);
|
||||
return null;
|
||||
}).setName(i18n("version.launch_script"));
|
||||
});
|
||||
}
|
||||
})
|
||||
.thenAcceptAsync(process -> { // process is LaunchTask's result
|
||||
}).thenAcceptAsync(process -> { // process is LaunchTask's result
|
||||
if (scriptFile == null) {
|
||||
PROCESSES.add(process);
|
||||
if (launcherVisibility == LauncherVisibility.CLOSE)
|
||||
@@ -204,28 +204,25 @@ public final class LauncherHelper {
|
||||
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
|
||||
});
|
||||
}
|
||||
})
|
||||
}).thenRunAsync(() -> {
|
||||
launchingLatch.await();
|
||||
}).withStage("launch.state.waiting_launching"))
|
||||
.withStagesHint(Lang.immutableListOf(
|
||||
"launch.state.dependencies",
|
||||
"launch.state.logging_in",
|
||||
"launch.state.waiting_launching"))
|
||||
.executor();
|
||||
|
||||
launchingStepsPane.setExecutor(executor, false);
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
final AtomicInteger finished = new AtomicInteger(0);
|
||||
|
||||
@Override
|
||||
public void onFinished(Task<?> task) {
|
||||
finished.incrementAndGet();
|
||||
int runningTasks = executor.getRunningTasks();
|
||||
Platform.runLater(() -> launchingStepsPane.setProgress(1.0 * finished.get() / runningTasks));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
if (!success && !Controllers.isStopped()) {
|
||||
Platform.runLater(() -> {
|
||||
// Check if the application has stopped
|
||||
// because onStop will be invoked if tasks fail when the executor service shut down.
|
||||
if (!Controllers.isStopped()) {
|
||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||
Platform.runLater(() -> {
|
||||
// Check if the application has stopped
|
||||
// because onStop will be invoked if tasks fail when the executor service shut down.
|
||||
if (!Controllers.isStopped()) {
|
||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||
if (!success) {
|
||||
Exception ex = executor.getException();
|
||||
if (ex != null) {
|
||||
String message;
|
||||
@@ -241,7 +238,46 @@ public final class LauncherHelper {
|
||||
} else if (ex instanceof NotDecompressingNativesException) {
|
||||
message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage();
|
||||
} else if (ex instanceof LibraryDownloadException) {
|
||||
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n";
|
||||
if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
if (responseCode == 404)
|
||||
message += i18n("download.code.404", url);
|
||||
else
|
||||
message += i18n("download.failed", url, responseCode);
|
||||
} else {
|
||||
message += StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof DownloadException) {
|
||||
URL url = ((DownloadException) ex).getUrl();
|
||||
if (ex.getCause() instanceof SocketTimeoutException) {
|
||||
message = i18n("install.failed.downloading.timeout", url);
|
||||
} else if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
|
||||
message = i18n("assets.index.malformed");
|
||||
} else if (ex instanceof AuthlibInjectorDownloadException) {
|
||||
message = i18n("account.failed.injector_download_failure");
|
||||
} else if (ex instanceof CharacterDeletedException) {
|
||||
message = i18n("account.failed.character_deleted");
|
||||
} else if (ex instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex;
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
if (responseCode == 404)
|
||||
message = i18n("download.code.404", url);
|
||||
else
|
||||
message = i18n("download.failed", url, responseCode);
|
||||
} else {
|
||||
message = StringUtils.getStackTrace(ex);
|
||||
}
|
||||
@@ -250,9 +286,9 @@ public final class LauncherHelper {
|
||||
MessageType.ERROR);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
launchingStepsPane.setExecutor(null);
|
||||
}
|
||||
launchingStepsPane.setExecutor(null);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -281,6 +317,22 @@ public final class LauncherHelper {
|
||||
flag = true;
|
||||
}
|
||||
|
||||
if (!flag && version.getJavaVersion() != null) {
|
||||
if (java.getParsedVersion() < version.getJavaVersion().getMajorVersion()) {
|
||||
Optional<JavaVersion> acceptableJava = JavaVersion.getJavas().stream()
|
||||
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
if (acceptableJava.isPresent()) {
|
||||
setting.setJavaVersion(acceptableJava.get());
|
||||
} else {
|
||||
Controllers.dialog(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), version.getJavaVersion().getMajorVersion()), i18n("message.warning"), MessageType.WARNING, () -> {
|
||||
downloadJava(version.getJavaVersion(), profile).thenAccept(x -> onAccept.run());
|
||||
});
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Game later than 1.7.2 accepts Java 8.
|
||||
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
|
||||
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
|
||||
@@ -304,7 +356,7 @@ public final class LauncherHelper {
|
||||
|
||||
// LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader.
|
||||
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
|
||||
&& version.getMainClass().contains("launchwrapper")
|
||||
&& LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|
||||
&& version.getLibraries().stream()
|
||||
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
|
||||
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
|
||||
@@ -336,7 +388,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
|
||||
org.jackhuang.hmcl.util.platform.Platform.IS_64_BIT) {
|
||||
Architecture.CURRENT.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_64) {
|
||||
final JavaVersion java32 = java;
|
||||
|
||||
// First find if same java version but whose platform is 64-bit installed.
|
||||
@@ -372,13 +424,13 @@ public final class LauncherHelper {
|
||||
setting.getMaxMemory() > 1.5 * 1024) {
|
||||
// 1.5 * 1024 is an inaccurate number.
|
||||
// Actual memory limit depends on operating system and memory.
|
||||
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageType.ERROR, onAccept);
|
||||
Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), onAccept, null);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
// Cannot allocate too much memory exceeding free space.
|
||||
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
|
||||
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageType.ERROR, onAccept);
|
||||
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), onAccept, null);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
@@ -390,7 +442,19 @@ public final class LauncherHelper {
|
||||
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
|
||||
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageType.ERROR, onAccept);
|
||||
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), onAccept, null);
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
|
||||
if (!flag) {
|
||||
boolean hasForge28_2_2 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
|
||||
.anyMatch(it ->
|
||||
VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0);
|
||||
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
||||
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
|
||||
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), onAccept, null);
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
@@ -399,13 +463,34 @@ public final class LauncherHelper {
|
||||
onAccept.run();
|
||||
}
|
||||
|
||||
public void emitStatus(LoadingState state) {
|
||||
if (state == LoadingState.DONE) {
|
||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
launchingStepsPane.setTitle(state.getLocalizedMessage());
|
||||
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
|
||||
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
|
||||
});
|
||||
|
||||
TaskExecutor executor = JavaRepository.downloadJava(javaVersion,
|
||||
profile.getDependency().getDownloadProvider()).executor(false);
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
super.onStop(success, executor);
|
||||
Platform.runLater(() -> {
|
||||
if (!success) {
|
||||
future.completeExceptionally(executor.getException());
|
||||
} else {
|
||||
future.complete(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
javaDownloadingPane.setExecutor(executor, true);
|
||||
Controllers.dialog(javaDownloadingPane);
|
||||
executor.start();
|
||||
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
private void checkExit() {
|
||||
@@ -432,19 +517,6 @@ public final class LauncherHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static class LaunchTask<T> extends Task<T> {
|
||||
private final ExceptionalSupplier<T, Exception> supplier;
|
||||
|
||||
public LaunchTask(ExceptionalSupplier<T, Exception> supplier) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
setResult(supplier.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The managed process listener.
|
||||
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
|
||||
@@ -452,15 +524,21 @@ public final class LauncherHelper {
|
||||
*/
|
||||
class HMCLProcessListener implements ProcessListener {
|
||||
|
||||
private final HMCLGameRepository repository;
|
||||
private final String version;
|
||||
private final Map<String, String> forbiddenTokens;
|
||||
private ManagedProcess process;
|
||||
private boolean lwjgl;
|
||||
private LogWindow logWindow;
|
||||
private final boolean detectWindow;
|
||||
private final LinkedList<Pair<String, Log4jLevel>> logs;
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch launchingLatch;
|
||||
|
||||
public HMCLProcessListener(AuthInfo authInfo, boolean detectWindow) {
|
||||
public HMCLProcessListener(HMCLGameRepository repository, String version, AuthInfo authInfo, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.launchingLatch = launchingLatch;
|
||||
this.detectWindow = detectWindow;
|
||||
|
||||
if (authInfo == null)
|
||||
@@ -479,11 +557,17 @@ public final class LauncherHelper {
|
||||
public void setProcess(ManagedProcess process) {
|
||||
this.process = process;
|
||||
|
||||
String command = new CommandBuilder().addAll(process.getCommands()).toString();
|
||||
for (Map.Entry<String, String> entry : forbiddenTokens.entrySet())
|
||||
command = command.replace(entry.getKey(), entry.getValue());
|
||||
|
||||
LOG.info("Launched process: " + command);
|
||||
|
||||
if (showLogs)
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
logWindow.show();
|
||||
latch.countDown();
|
||||
logWindowLatch.countDown();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -495,7 +579,7 @@ public final class LauncherHelper {
|
||||
// these codes will be executed.
|
||||
if (Controllers.getStage() != null) {
|
||||
Controllers.getStage().hide();
|
||||
emitStatus(LoadingState.DONE);
|
||||
launchingLatch.countDown();
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -503,17 +587,17 @@ public final class LauncherHelper {
|
||||
// Never come to here.
|
||||
break;
|
||||
case KEEP:
|
||||
Platform.runLater(() -> {
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
Platform.runLater(launchingLatch::countDown);
|
||||
break;
|
||||
case HIDE:
|
||||
launchingLatch.countDown();
|
||||
Platform.runLater(() -> {
|
||||
// If application was stopped and execution services did not finish termination,
|
||||
// these codes will be executed.
|
||||
if (Controllers.getStage() != null) {
|
||||
Controllers.getStage().close();
|
||||
emitStatus(LoadingState.DONE);
|
||||
Controllers.shutdown();
|
||||
Schedulers.shutdown();
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -525,29 +609,29 @@ public final class LauncherHelper {
|
||||
String newLog = log;
|
||||
for (Map.Entry<String, String> entry : forbiddenTokens.entrySet())
|
||||
newLog = newLog.replace(entry.getKey(), entry.getValue());
|
||||
String filteredLog = newLog;
|
||||
|
||||
if (level.lessOrEqual(Log4jLevel.ERROR))
|
||||
System.err.print(log);
|
||||
System.err.println(filteredLog);
|
||||
else
|
||||
System.out.print(log);
|
||||
System.out.println(filteredLog);
|
||||
|
||||
logs.add(pair(log, level));
|
||||
logs.add(pair(filteredLog, level));
|
||||
if (logs.size() > config().getLogLines())
|
||||
logs.removeFirst();
|
||||
|
||||
if (showLogs) {
|
||||
try {
|
||||
latch.await();
|
||||
logWindow.waitForLoaded();
|
||||
logWindowLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> logWindow.logLine(log, level));
|
||||
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
|
||||
}
|
||||
|
||||
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
|
||||
if (!lwjgl && (filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal") || !detectWindow)) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
}
|
||||
@@ -555,32 +639,37 @@ public final class LauncherHelper {
|
||||
|
||||
@Override
|
||||
public void onExit(int exitCode, ExitType exitType) {
|
||||
launchingLatch.countDown();
|
||||
|
||||
if (exitType == ExitType.INTERRUPTED)
|
||||
return;
|
||||
|
||||
// Game crashed before opening the game window.
|
||||
if (!lwjgl) finishLaunch();
|
||||
|
||||
if (exitType != ExitType.NORMAL && logWindow == null)
|
||||
if (exitType != ExitType.NORMAL) {
|
||||
repository.markVersionLaunchedAbnormally(version);
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
if (logWindow == null) {
|
||||
logWindow = new LogWindow();
|
||||
|
||||
switch (exitType) {
|
||||
case JVM_ERROR:
|
||||
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
|
||||
break;
|
||||
case APPLICATION_ERROR:
|
||||
logWindow.setTitle(i18n("launch.failed.exited_abnormally"));
|
||||
break;
|
||||
}
|
||||
switch (exitType) {
|
||||
case JVM_ERROR:
|
||||
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
|
||||
break;
|
||||
case APPLICATION_ERROR:
|
||||
logWindow.setTitle(i18n("launch.failed.exited_abnormally"));
|
||||
break;
|
||||
}
|
||||
|
||||
logWindow.show();
|
||||
logWindow.onDone.register(() -> {
|
||||
logWindow.logLine("Command: " + new CommandBuilder().addAll(process.getCommands()).toString(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
logWindow.showGameCrashReport();
|
||||
});
|
||||
}
|
||||
|
||||
checkExit();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -23,9 +23,13 @@ import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseManifest;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||
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.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -41,12 +45,20 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class ModpackHelper {
|
||||
private ModpackHelper() {}
|
||||
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception ignored) {
|
||||
ignored.printStackTrace();
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
@@ -65,6 +77,12 @@ public final class ModpackHelper {
|
||||
// ignore it, not a valid MultiMC modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return ServerModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid Server modpack.
|
||||
}
|
||||
|
||||
throw new UnsupportedModpackException(file.toString());
|
||||
}
|
||||
|
||||
@@ -91,6 +109,29 @@ public final class ModpackHelper {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
|
||||
public static Task<Void> getInstallTask(Profile profile, ServerModpackManifest manifest, 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
|
||||
}
|
||||
};
|
||||
|
||||
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
}
|
||||
|
||||
public static Task<Void> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
@@ -100,16 +141,13 @@ public final class ModpackHelper {
|
||||
VersionSetting vs = repository.specializeVersionSetting(name);
|
||||
repository.undoMark(name);
|
||||
if (vs != null)
|
||||
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
|
||||
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
|
||||
} else {
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
repository.removeVersionFromDisk(name);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,10 +158,26 @@ public final class ModpackHelper {
|
||||
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
|
||||
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
|
||||
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
else if (modpack.getManifest() instanceof ServerModpackManifest)
|
||||
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
|
||||
}
|
||||
|
||||
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));
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
|
||||
@@ -145,6 +199,16 @@ public final class ModpackHelper {
|
||||
throw new MismatchedModpackTypeException(HMCLModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
|
||||
case McbbsModpackLocalInstallTask.MODPACK_TYPE:
|
||||
if (!(modpack.getManifest() instanceof McbbsModpackManifest))
|
||||
throw new MismatchedModpackTypeException(McbbsModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
case ServerModpackLocalInstallTask.MODPACK_TYPE:
|
||||
if (!(modpack.getManifest() instanceof ServerModpackManifest))
|
||||
throw new MismatchedModpackTypeException(ServerModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
|
||||
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name));
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
@@ -152,7 +216,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(), ""));
|
||||
@@ -187,5 +251,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,46 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
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 java.util.Objects;
|
||||
|
||||
public final class MultiMCInstallVersionSettingTask extends Task<Void> {
|
||||
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;
|
||||
|
||||
setExecutor(Schedulers.javafx());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
ModpackHelper.toVersionSetting(manifest, vs);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -19,12 +19,18 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -33,16 +39,13 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
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.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -121,6 +124,9 @@ public final class TexturesLoader {
|
||||
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();
|
||||
@@ -131,6 +137,7 @@ public final class TexturesLoader {
|
||||
|
||||
// ==== 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);
|
||||
@@ -139,8 +146,8 @@ public final class TexturesLoader {
|
||||
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 (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} catch (Throwable e) {
|
||||
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +185,30 @@ public final class TexturesLoader {
|
||||
}
|
||||
}, uuidFallback);
|
||||
}
|
||||
|
||||
public static ObjectBinding<LoadedTexture> skinBinding(Account account) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
|
||||
return BindingMapping.of(account.getTextures())
|
||||
.map(textures -> textures
|
||||
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
|
||||
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
|
||||
.asyncMap(it -> {
|
||||
if (it.isPresent()) {
|
||||
Texture texture = it.get();
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
}, POOL);
|
||||
} else {
|
||||
return CompletableFuture.completedFuture(uuidFallback);
|
||||
}
|
||||
}, uuidFallback);
|
||||
}
|
||||
|
||||
// ====
|
||||
|
||||
// ==== Avatar ====
|
||||
@@ -202,16 +233,34 @@ public final class TexturesLoader {
|
||||
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
|
||||
return BindingMapping.of(skinBinding(service, uuid))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(img -> SwingFXUtils.toFXImage(img, null));
|
||||
.map(TexturesLoader::toFXImage);
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
|
||||
return BindingMapping.of(skinBinding(account))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(TexturesLoader::toFXImage);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
|
||||
() -> toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
|
||||
}
|
||||
}
|
||||
// ====
|
||||
}
|
||||
|
||||
// Based on https://stackoverflow.com/a/57552025
|
||||
// Fix #874: Use it instead of SwingFXUtils.toFXImage
|
||||
private static WritableImage toFXImage(BufferedImage image) {
|
||||
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
|
||||
PixelWriter pw = wr.getPixelWriter();
|
||||
|
||||
final int iw = image.getWidth();
|
||||
final int ih = image.getHeight();
|
||||
for (int x = 0; x < iw; x++) {
|
||||
for (int y = 0; y < ih; y++) {
|
||||
pw.setArgb(x, y, image.getRGB(x, y));
|
||||
}
|
||||
}
|
||||
return wr;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -29,19 +29,25 @@ import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
|
||||
|
||||
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;
|
||||
@@ -50,6 +56,7 @@ 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;
|
||||
@@ -61,9 +68,24 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public final class Accounts {
|
||||
private Accounts() {}
|
||||
|
||||
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
|
||||
private static void triggerAuthlibInjectorUpdateCheck() {
|
||||
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to check update for authlib-injector", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MicrosoftAccountLoginStage.INSTANCE));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_AUTHLIB_INJECTOR, FACTORY_MICROSOFT);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||
@@ -72,6 +94,7 @@ public final class Accounts {
|
||||
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));
|
||||
}
|
||||
@@ -94,6 +117,8 @@ public final class Accounts {
|
||||
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);
|
||||
}
|
||||
@@ -206,6 +231,10 @@ public final class Accounts {
|
||||
});
|
||||
}
|
||||
|
||||
if (!config().getAuthlibInjectorServers().isEmpty()) {
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
}
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
continue;
|
||||
@@ -243,7 +272,19 @@ public final class Accounts {
|
||||
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
|
||||
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
|
||||
if (authlibinjectorLocation == null) {
|
||||
return new AuthlibInjectorDownloader(Metadata.HMCL_DIRECTORY, DownloadProviders::getDownloadProvider);
|
||||
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));
|
||||
@@ -279,7 +320,8 @@ public final class Accounts {
|
||||
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_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))
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -110,6 +110,12 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("proxyPassword")
|
||||
private StringProperty proxyPass = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("width")
|
||||
private DoubleProperty width = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("height")
|
||||
private DoubleProperty height = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("theme")
|
||||
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
|
||||
|
||||
@@ -329,6 +335,30 @@ public final class Config implements Cloneable, Observable {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -52,12 +52,19 @@ public final class ConfigHolder {
|
||||
return configInstance;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
@@ -84,6 +91,26 @@ public final class ConfigHolder {
|
||||
}
|
||||
|
||||
private static Path locateConfig() {
|
||||
Path exePath = Paths.get("");
|
||||
try {
|
||||
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
|
||||
.toURI()).toAbsolutePath();
|
||||
if (Files.isRegularFile(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;
|
||||
@@ -93,7 +120,7 @@ public final class ConfigHolder {
|
||||
return dotConfig;
|
||||
|
||||
// create new
|
||||
return OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? config : dotConfig;
|
||||
return exePath.resolve(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? CONFIG_FILENAME : CONFIG_FILENAME_LINUX);
|
||||
}
|
||||
|
||||
private static Config loadConfig() throws IOException {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -17,44 +17,79 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import org.jackhuang.hmcl.download.AdaptedDownloadProvider;
|
||||
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.MojangDownloadProvider;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.MojangDownloadProvider;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
|
||||
public final class DownloadProviders {
|
||||
private DownloadProviders() {}
|
||||
|
||||
public static final Map<String, DownloadProvider> providersById = mapOf(
|
||||
pair("mojang", new MojangDownloadProvider()),
|
||||
pair("bmclapi", new BMCLAPIDownloadProvider()));
|
||||
private static final AdaptedDownloadProvider DOWNLOAD_PROVIDER = new AdaptedDownloadProvider();
|
||||
|
||||
public static final String DEFAULT_PROVIDER_ID = "bmclapi";
|
||||
public static final Map<String, DownloadProvider> providersById;
|
||||
|
||||
private static ObjectBinding<DownloadProvider> downloadProviderProperty;
|
||||
public static final String DEFAULT_PROVIDER_ID = "mcbbs";
|
||||
|
||||
static {
|
||||
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
|
||||
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
|
||||
if (bmclapiRootOverride != null) bmclapiRoot = bmclapiRootOverride;
|
||||
|
||||
providersById = mapOf(
|
||||
pair("mojang", new MojangDownloadProvider()),
|
||||
pair("bmclapi", new BMCLAPIDownloadProvider(bmclapiRoot)),
|
||||
pair("mcbbs", new BMCLAPIDownloadProvider("https://download.mcbbs.net")));
|
||||
}
|
||||
|
||||
static void init() {
|
||||
downloadProviderProperty = Bindings.createObjectBinding(
|
||||
() -> Optional.ofNullable(providersById.get(config().getDownloadType()))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID)),
|
||||
config().downloadTypeProperty());
|
||||
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
|
||||
DownloadProvider primary = Optional.ofNullable(providersById.get(config().getDownloadType()))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
DOWNLOAD_PROVIDER.setDownloadProviderCandidates(
|
||||
Stream.concat(
|
||||
Stream.of(primary),
|
||||
providersById.values().stream().filter(x -> x != primary)
|
||||
).collect(Collectors.toList())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static DownloadProvider getDownloadProvider() {
|
||||
return downloadProviderProperty.get();
|
||||
public static String getPrimaryDownloadProviderId() {
|
||||
String downloadType = config().getDownloadType();
|
||||
if (providersById.containsKey(downloadType))
|
||||
return downloadType;
|
||||
else
|
||||
return DEFAULT_PROVIDER_ID;
|
||||
}
|
||||
|
||||
public static ObservableObjectValue<DownloadProvider> downloadProviderProperty() {
|
||||
return downloadProviderProperty;
|
||||
public static AdaptedDownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
|
||||
AdaptedDownloadProvider adaptedDownloadProvider = new AdaptedDownloadProvider();
|
||||
DownloadProvider primary = Optional.ofNullable(providersById.get(primaryId))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
adaptedDownloadProvider.setDownloadProviderCandidates(
|
||||
Stream.concat(
|
||||
Stream.of(primary),
|
||||
providersById.values().stream().filter(x -> x != primary)
|
||||
).collect(Collectors.toList())
|
||||
);
|
||||
return adaptedDownloadProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current primary preferred download provider
|
||||
*/
|
||||
public static AdaptedDownloadProvider getDownloadProvider() {
|
||||
return DOWNLOAD_PROVIDER;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -24,6 +24,7 @@ import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
@@ -162,17 +163,15 @@ public final class Profile implements Observable {
|
||||
}
|
||||
|
||||
public DefaultDependencyManager getDependency() {
|
||||
return new DefaultDependencyManager(repository, DownloadProviders.getDownloadProvider(), HMCLCacheRepository.REPOSITORY);
|
||||
return getDependency(DownloadProviders.getDownloadProvider());
|
||||
}
|
||||
|
||||
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
|
||||
getGlobal().setUsesGlobal(true);
|
||||
return getGlobal();
|
||||
} else
|
||||
return vs;
|
||||
return repository.getVersionSetting(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -25,6 +25,8 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class Settings {
|
||||
|
||||
private static Settings instance;
|
||||
@@ -44,10 +46,14 @@ public class 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(() -> {
|
||||
@@ -73,4 +79,8 @@ public class Settings {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateSystemLocale() {
|
||||
Locale.setDefault(config().getLocalization().getLocale());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -23,7 +23,6 @@ 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;
|
||||
@@ -40,7 +39,7 @@ 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("#5C6BC0"), // blue
|
||||
Color.web("#283593"), // dark blue
|
||||
@@ -50,12 +49,14 @@ public class Theme {
|
||||
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 = color;
|
||||
this.paint = Color.web(color);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -79,12 +80,17 @@ public class Theme {
|
||||
}
|
||||
|
||||
public String[] getStylesheets() {
|
||||
Color textFill = getForegroundColor();
|
||||
|
||||
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("%font-color%", getColorDisplayName(getForegroundColor())));
|
||||
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int) Math.ceil(paint.getRed() * 256), (int) Math.ceil(paint.getGreen() * 256), (int) Math.ceil(paint.getBlue() * 256)))
|
||||
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
|
||||
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
|
||||
.replace("%font%", Optional.ofNullable(System.getProperty("hmcl.font.override")).map(fontFamily -> "-fx-font-family: " + fontFamily + ";").orElse("")));
|
||||
css = temp.toURI().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
|
||||
@@ -139,7 +145,7 @@ public class Theme {
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> blackFillBinding() {
|
||||
return Bindings.createObjectBinding(() -> Color.BLACK);
|
||||
return Bindings.createObjectBinding(() -> BLACK);
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> whiteFillBinding() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -19,24 +19,15 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.InvalidPathException;
|
||||
@@ -45,14 +36,12 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@JsonAdapter(VersionSetting.Serializer.class)
|
||||
public final class VersionSetting {
|
||||
public final class VersionSetting implements Cloneable {
|
||||
|
||||
public transient String id;
|
||||
|
||||
@@ -423,17 +412,17 @@ public final class VersionSetting {
|
||||
* 0 - .minecraft<br/>
|
||||
* 1 - .minecraft/versions/<version>/<br/>
|
||||
*/
|
||||
private final ObjectProperty<EnumGameDirectory> gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
|
||||
private final ObjectProperty<GameDirectoryType> gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", GameDirectoryType.ROOT_FOLDER);
|
||||
|
||||
public ObjectProperty<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);
|
||||
}
|
||||
|
||||
@@ -476,13 +465,20 @@ public final class VersionSetting {
|
||||
}
|
||||
|
||||
public JavaVersion getJavaVersion() throws InterruptedException {
|
||||
return getJavaVersion(true);
|
||||
}
|
||||
|
||||
public JavaVersion getJavaVersion(boolean checkJava) throws InterruptedException {
|
||||
// TODO: lazy initialization may result in UI suspension.
|
||||
if (StringUtils.isBlank(getJava()))
|
||||
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
|
||||
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
|
||||
else if (isUsesCustomJavaDir()) {
|
||||
try {
|
||||
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
||||
if (checkJava)
|
||||
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
||||
else
|
||||
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.PLATFORM);
|
||||
} catch (IOException | InvalidPathException e) {
|
||||
return null; // Custom Java Directory not found,
|
||||
}
|
||||
@@ -532,35 +528,32 @@ public final class VersionSetting {
|
||||
defaultJavaPathProperty.addListener(listener);
|
||||
}
|
||||
|
||||
public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException {
|
||||
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion()).orElse(JavaVersion.fromCurrentEnvironment());
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionName(Metadata.TITLE)
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setMinecraftArgs(getMinecraftArgs())
|
||||
.setJavaArgs(getJavaArgs())
|
||||
.setMaxMemory(getMaxMemory())
|
||||
.setMinMemory(getMinMemory())
|
||||
.setMetaspace(Lang.toIntOrNull(getPermSize()))
|
||||
.setWidth(getWidth())
|
||||
.setHeight(getHeight())
|
||||
.setFullscreen(isFullscreen())
|
||||
.setServerIp(getServerIp())
|
||||
.setWrapper(getWrapper())
|
||||
.setPrecalledCommand(getPreLaunchCommand())
|
||||
.setNoGeneratedJVMArgs(isNoJVMArgs());
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxyHost(config().getProxyHost());
|
||||
builder.setProxyPort(config().getProxyPort());
|
||||
if (config().hasProxyAuth()) {
|
||||
builder.setProxyUser(config().getProxyUser());
|
||||
builder.setProxyPass(config().getProxyPass());
|
||||
}
|
||||
}
|
||||
return builder.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.setPreLaunchCommand(getPreLaunchCommand());
|
||||
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.setLauncherVisibility(getLauncherVisibility());
|
||||
return versionSetting;
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
|
||||
@@ -626,7 +619,7 @@ public final class VersionSetting {
|
||||
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.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
|
||||
|
||||
return vs;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class 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,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -17,64 +17,53 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.download.java.JavaRepository;
|
||||
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.account.AccountList;
|
||||
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.profile.ProfileList;
|
||||
import org.jackhuang.hmcl.ui.versions.GameItem;
|
||||
import org.jackhuang.hmcl.ui.versions.GameList;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
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.ui.FXUtils.runInFX;
|
||||
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 = null;
|
||||
private static SettingsPage settingsPage = null;
|
||||
private static VersionPage versionPage = null;
|
||||
private static GameList gameListPage = null;
|
||||
private static AccountList accountListPage = null;
|
||||
private static ProfileList profileListPage = null;
|
||||
private static AuthlibInjectorServersPage serversPage = null;
|
||||
private static LeftPaneController leftPaneController;
|
||||
private static RootPage rootPage;
|
||||
private static DecoratorController decorator;
|
||||
|
||||
public static Scene getScene() {
|
||||
@@ -85,47 +74,6 @@ public final class Controllers {
|
||||
return stage;
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static SettingsPage getSettingsPage() {
|
||||
if (settingsPage == null)
|
||||
settingsPage = new SettingsPage();
|
||||
return settingsPage;
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static GameList getGameListPage() {
|
||||
if (gameListPage == null) {
|
||||
gameListPage = new GameList();
|
||||
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;
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static AccountList getAccountListPage() {
|
||||
if (accountListPage == null) {
|
||||
AccountList accountListPage = new AccountList();
|
||||
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
|
||||
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
|
||||
Controllers.accountListPage = accountListPage;
|
||||
}
|
||||
return accountListPage;
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static ProfileList getProfileListPage() {
|
||||
if (profileListPage == null) {
|
||||
ProfileList profileListPage = new ProfileList();
|
||||
profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
|
||||
profileListPage.profilesProperty().bindContent(Profiles.profilesProperty());
|
||||
Controllers.profileListPage = profileListPage;
|
||||
}
|
||||
return profileListPage;
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static VersionPage getVersionPage() {
|
||||
if (versionPage == null)
|
||||
@@ -133,6 +81,13 @@ public final class Controllers {
|
||||
return versionPage;
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static RootPage getRootPage() {
|
||||
if (rootPage == null)
|
||||
rootPage = new RootPage();
|
||||
return rootPage;
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static AuthlibInjectorServersPage getServersPage() {
|
||||
if (serversPage == null)
|
||||
@@ -145,62 +100,25 @@ public final class Controllers {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
public static MainPage getMainPage() {
|
||||
if (mainPage == null) {
|
||||
MainPage mainPage = new MainPage();
|
||||
FXUtils.applyDragListener(mainPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
|
||||
File modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
|
||||
if (version != null) {
|
||||
mainPage.setCurrentGame(version);
|
||||
} else {
|
||||
mainPage.setCurrentGame(i18n("version.empty"));
|
||||
}
|
||||
});
|
||||
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
|
||||
mainPage.latestVersionProperty().bind(
|
||||
BindingMapping.of(UpdateChecker.latestVersionProperty())
|
||||
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
|
||||
|
||||
Profiles.registerVersionsListener(profile -> {
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
List<Node> children = repository.getVersions().parallelStream()
|
||||
.filter(version -> !version.isHidden())
|
||||
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
|
||||
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
|
||||
.map(version -> {
|
||||
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
|
||||
node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId()));
|
||||
return node;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
runInFX(() -> {
|
||||
if (profile == Profiles.getSelectedProfile())
|
||||
mainPage.getVersions().setAll(children);
|
||||
});
|
||||
});
|
||||
Controllers.mainPage = mainPage;
|
||||
}
|
||||
return mainPage;
|
||||
}
|
||||
|
||||
public static LeftPaneController getLeftPaneController() {
|
||||
return leftPaneController;
|
||||
public static void onApplicationStop() {
|
||||
config().setHeight(stageHeight.get());
|
||||
config().setWidth(stageWidth.get());
|
||||
}
|
||||
|
||||
public static void initialize(Stage stage) {
|
||||
Logging.LOG.info("Start initializing application");
|
||||
|
||||
Controllers.stage = stage;
|
||||
MicrosoftAccountLoginStage.INSTANCE.initOwner(stage);
|
||||
|
||||
stage.setHeight(config().getHeight());
|
||||
stageHeight.bind(stage.heightProperty());
|
||||
stage.setWidth(config().getWidth());
|
||||
stageWidth.bind(stage.widthProperty());
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
decorator = new DecoratorController(stage, getMainPage());
|
||||
leftPaneController = new LeftPaneController();
|
||||
decorator.getDecorator().drawerProperty().setAll(leftPaneController);
|
||||
decorator = new DecoratorController(stage, getRootPage());
|
||||
|
||||
if (config().getCommonDirType() == EnumCommonDirectory.CUSTOM &&
|
||||
!FileUtils.canCreateDirectory(config().getCommonDirectory())) {
|
||||
@@ -208,13 +126,20 @@ public final class Controllers {
|
||||
dialog(i18n("launcher.cache_directory.invalid"));
|
||||
}
|
||||
|
||||
Task.runAsync(JavaVersion::initialize).start();
|
||||
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
|
||||
|
||||
scene = new Scene(decorator.getDecorator(), 800, 519);
|
||||
scene = new Scene(decorator.getDecorator());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
stage.setMinHeight(482 + 32);
|
||||
stage.setMinWidth(802 + 32);
|
||||
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
|
||||
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
||||
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
|
||||
|
||||
stage.getIcons().add(newImage("/assets/img/icon.png"));
|
||||
stage.setTitle(Metadata.TITLE);
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
}
|
||||
|
||||
public static void dialog(Region content) {
|
||||
@@ -238,35 +163,40 @@ public final class Controllers {
|
||||
dialog(new MessageDialogPane(text, title, type, onAccept));
|
||||
}
|
||||
|
||||
public static void confirmDialog(String text, String title, Runnable onAccept, Runnable onCancel) {
|
||||
public static void confirm(String text, String title, Runnable onAccept, Runnable onCancel) {
|
||||
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
|
||||
}
|
||||
|
||||
public static InputDialogPane inputDialog(String text, FutureCallback<String> onResult) {
|
||||
InputDialogPane pane = new InputDialogPane(text, onResult);
|
||||
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;
|
||||
return pane.getCompletableFuture();
|
||||
}
|
||||
|
||||
public static Region taskDialog(TaskExecutor executor, String title) {
|
||||
return taskDialog(executor, title, "");
|
||||
public static CompletableFuture<List<PromptDialogPane.Builder.Question<?>>> prompt(PromptDialogPane.Builder builder) {
|
||||
PromptDialogPane pane = new PromptDialogPane(builder);
|
||||
dialog(pane);
|
||||
return pane.getCompletableFuture();
|
||||
}
|
||||
|
||||
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
|
||||
return taskDialog(executor, title, subtitle, null);
|
||||
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title) {
|
||||
return taskDialog(executor, title, null);
|
||||
}
|
||||
|
||||
public static Region taskDialog(TaskExecutor executor, String title, String subtitle, Consumer<Region> onCancel) {
|
||||
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);
|
||||
dialog(pane);
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static void navigate(Node node) {
|
||||
decorator.getNavigator().navigate(node);
|
||||
decorator.getNavigator().navigate(node, ContainerAnimations.FADE.getAnimationProducer());
|
||||
}
|
||||
|
||||
public static boolean isStopped() {
|
||||
@@ -274,15 +204,11 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
mainPage = null;
|
||||
settingsPage = null;
|
||||
rootPage = null;
|
||||
versionPage = null;
|
||||
serversPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
gameListPage = null;
|
||||
accountListPage = null;
|
||||
profileListPage = null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -18,9 +18,9 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
@@ -28,7 +28,7 @@ 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;
|
||||
@@ -36,8 +36,6 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -61,12 +59,10 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -100,8 +96,10 @@ public final class FXUtils {
|
||||
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> WeakChangeListener<T> onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
WeakChangeListener<T> listener = new WeakChangeListener<>((a, b, c) -> consumer.accept(c));
|
||||
value.addListener(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
public static <T> void onChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
@@ -109,9 +107,9 @@ public final class FXUtils {
|
||||
onChange(value, consumer);
|
||||
}
|
||||
|
||||
public static <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
public static <T> WeakChangeListener<T> onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
consumer.accept(value.getValue());
|
||||
onWeakChange(value, consumer);
|
||||
return onWeakChange(value, consumer);
|
||||
}
|
||||
|
||||
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
|
||||
@@ -201,19 +199,19 @@ public final class FXUtils {
|
||||
return field.getProperties().containsKey("FXUtils.validation");
|
||||
}
|
||||
|
||||
public static void setOverflowHidden(Region region, boolean hidden) {
|
||||
if (hidden) {
|
||||
Rectangle rectangle = new Rectangle();
|
||||
rectangle.widthProperty().bind(region.widthProperty());
|
||||
rectangle.heightProperty().bind(region.heightProperty());
|
||||
region.setClip(rectangle);
|
||||
} else {
|
||||
region.setClip(null);
|
||||
}
|
||||
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 boolean getOverflowHidden(Region region) {
|
||||
return region.getClip() != null;
|
||||
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) {
|
||||
@@ -301,6 +299,27 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
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> void 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);
|
||||
} else {
|
||||
playAnimation(node, animationKey, new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(property, from, interpolator)),
|
||||
new KeyFrame(duration, new KeyValue(property, to, interpolator))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFolder(File file) {
|
||||
if (!FileUtils.makeDirectory(file)) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
|
||||
@@ -420,72 +439,6 @@ public final class FXUtils {
|
||||
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];
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
bar.addEventHandler(MouseEvent.DRAG_DETECTED, e -> timeline.stop());
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
public static <T> T runInUIThread(Supplier<T> supplier) {
|
||||
if (javafx.application.Platform.isFxApplicationThread()) {
|
||||
return supplier.get();
|
||||
} else {
|
||||
CountDownLatch doneLatch = new CountDownLatch(1);
|
||||
AtomicReference<T> reference = new AtomicReference<>();
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
reference.set(supplier.get());
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
try {
|
||||
doneLatch.await();
|
||||
} catch (InterruptedException var3) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
return reference.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -19,63 +19,198 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
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.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class InstallerItem extends BorderPane {
|
||||
public class InstallerItem extends Control {
|
||||
private final String id;
|
||||
private final String imageUrl;
|
||||
public final StringProperty libraryVersion = new SimpleStringProperty();
|
||||
public final StringProperty incompatibleLibraryName = new SimpleStringProperty();
|
||||
public final BooleanProperty incompatibleWithGame = new SimpleBooleanProperty();
|
||||
public final BooleanProperty removable = new SimpleBooleanProperty();
|
||||
public final BooleanProperty upgradable = new SimpleBooleanProperty(false);
|
||||
public final BooleanProperty installable = new SimpleBooleanProperty(true);
|
||||
public final ObjectProperty<EventHandler<? super MouseEvent>> removeAction = new SimpleObjectProperty<>();
|
||||
public final ObjectProperty<EventHandler<? super MouseEvent>> action = new SimpleObjectProperty<>();
|
||||
|
||||
public InstallerItem(String artifact, String version, @Nullable Runnable upgrade, @Nullable Consumer<InstallerItem> deleteCallback) {
|
||||
getStyleClass().add("two-line-list-item");
|
||||
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
|
||||
JFXDepthManager.setDepth(this, 1);
|
||||
public InstallerItem(LibraryAnalyzer.LibraryType id) {
|
||||
this(id.getPatchId());
|
||||
}
|
||||
|
||||
if (version != null) {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.setTitle(artifact);
|
||||
item.setSubtitle(i18n("archive.version") + ": " + version);
|
||||
setCenter(item);
|
||||
} else {
|
||||
Label label = new Label(artifact);
|
||||
label.setStyle("-fx-font-size: 15px;");
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
setCenter(label);
|
||||
}
|
||||
public InstallerItem(String id) {
|
||||
this.id = id;
|
||||
|
||||
{
|
||||
HBox hBox = new HBox();
|
||||
|
||||
if (upgrade != null) {
|
||||
JFXButton upgradeButton = new JFXButton();
|
||||
upgradeButton.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
|
||||
upgradeButton.getStyleClass().add("toggle-icon4");
|
||||
FXUtils.installFastTooltip(upgradeButton, i18n("install.change_version"));
|
||||
upgradeButton.setOnMouseClicked(e -> upgrade.run());
|
||||
hBox.getChildren().add(upgradeButton);
|
||||
}
|
||||
|
||||
if (deleteCallback != null) {
|
||||
JFXButton deleteButton = new JFXButton();
|
||||
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
|
||||
deleteButton.getStyleClass().add("toggle-icon4");
|
||||
deleteButton.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||
hBox.getChildren().add(deleteButton);
|
||||
}
|
||||
|
||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
setRight(hBox);
|
||||
switch (id) {
|
||||
case "game":
|
||||
imageUrl = "/assets/img/grass.png";
|
||||
break;
|
||||
case "fabric":
|
||||
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 InstallerItem game = new InstallerItem(MINECRAFT);
|
||||
public InstallerItem fabric = new InstallerItem(FABRIC);
|
||||
public InstallerItem forge = new InstallerItem(FORGE);
|
||||
public InstallerItem liteLoader = new InstallerItem(LITELOADER);
|
||||
public InstallerItem optiFine = new InstallerItem(OPTIFINE);
|
||||
|
||||
public 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));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
public InstallerItem[] getLibraries() {
|
||||
return new InstallerItem[]{game, fabric, forge, liteLoader, optiFine};
|
||||
}
|
||||
}
|
||||
|
||||
public static class InstallerItemSkin extends SkinBase<InstallerItem> {
|
||||
|
||||
InstallerItemSkin(InstallerItem control) {
|
||||
super(control);
|
||||
|
||||
HBox hbox = new HBox();
|
||||
getChildren().setAll(hbox);
|
||||
JFXDepthManager.setDepth(hbox, 1);
|
||||
|
||||
hbox.getStyleClass().add("card");
|
||||
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
if (control.imageUrl != null) {
|
||||
hbox.getChildren().add(FXUtils.limitingSize(new ImageView(new Image(control.imageUrl, 32, 32, true, true)), 32, 32));
|
||||
}
|
||||
|
||||
Label nameLabel = new Label();
|
||||
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();
|
||||
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) {
|
||||
hbox.onMouseClickedProperty().bind(control.action);
|
||||
hbox.setCursor(Cursor.HAND);
|
||||
} else {
|
||||
hbox.onMouseClickedProperty().unbind();
|
||||
hbox.onMouseClickedProperty().set(null);
|
||||
hbox.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 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.application.Platform;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.account.AddAccountPane;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class LeftPaneController extends AdvancedListBox {
|
||||
|
||||
public LeftPaneController() {
|
||||
|
||||
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
|
||||
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
|
||||
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
|
||||
|
||||
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
|
||||
gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull());
|
||||
gameListItem.setOnAction(e -> {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
String version = Profiles.getSelectedVersion();
|
||||
if (version == null) {
|
||||
Controllers.navigate(Controllers.getGameListPage());
|
||||
} else {
|
||||
Versions.modifyGameSettings(profile, version);
|
||||
}
|
||||
});
|
||||
|
||||
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
|
||||
profileListItem.setOnAction(e -> Controllers.navigate(Controllers.getProfileListPage()));
|
||||
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
|
||||
|
||||
AdvancedListItem gameItem = new AdvancedListItem();
|
||||
gameItem.setImage(newImage("/assets/img/bookshelf.png"));
|
||||
gameItem.setTitle(i18n("version.manage"));
|
||||
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
|
||||
|
||||
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
|
||||
launcherSettingsItem.setImage(newImage("/assets/img/command.png"));
|
||||
launcherSettingsItem.setTitle(i18n("settings.launcher"));
|
||||
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
|
||||
|
||||
this
|
||||
.startCategory(i18n("account").toUpperCase())
|
||||
.add(accountListItem)
|
||||
.startCategory(i18n("version").toUpperCase())
|
||||
.add(gameListItem)
|
||||
.add(gameItem)
|
||||
.startCategory(i18n("profile.title").toUpperCase())
|
||||
.add(profileListItem)
|
||||
.startCategory(i18n("launcher").toUpperCase())
|
||||
.add(launcherSettingsItem);
|
||||
|
||||
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
|
||||
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
if (profile != null && profile.getRepository().isLoaded())
|
||||
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
|
||||
}
|
||||
|
||||
// ==== Accounts ====
|
||||
public void checkAccount() {
|
||||
if (Accounts.getAccounts().isEmpty())
|
||||
Platform.runLater(this::addNewAccount);
|
||||
}
|
||||
|
||||
private void addNewAccount() {
|
||||
Controllers.dialog(new AddAccountPane());
|
||||
}
|
||||
// ====
|
||||
|
||||
private boolean checkedModpack = false;
|
||||
|
||||
private void onRefreshedVersions(HMCLGameRepository repository) {
|
||||
runInFX(() -> {
|
||||
if (!checkedModpack) {
|
||||
checkedModpack = true;
|
||||
|
||||
if (repository.getVersionCount() == 0) {
|
||||
File modpackFile = new File("modpack.zip").getAbsoluteFile();
|
||||
if (modpackFile.exists()) {
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
|
||||
.thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
|
||||
.thenApplyAsync(modpack -> ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
|
||||
.withRunAsync(Schedulers.javafx(), this::checkAccount).executor())
|
||||
.thenAcceptAsync(Schedulers.javafx(), executor -> {
|
||||
Controllers.taskDialog(executor, i18n("modpack.installing"));
|
||||
executor.start();
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkAccount();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -17,34 +17,47 @@
|
||||
*/
|
||||
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.WeakChangeListener;
|
||||
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.layout.StackPane;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
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.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;
|
||||
|
||||
/**
|
||||
@@ -53,141 +66,94 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
*/
|
||||
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 WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
|
||||
|
||||
private boolean stopCheckLogCount = false;
|
||||
|
||||
public LogWindow() {
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
|
||||
setTitle(i18n("logwindow.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
}
|
||||
|
||||
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);
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
|
||||
if (!stopCheckLogCount) checkLogCount();
|
||||
}
|
||||
|
||||
public void showGameCrashReport() {
|
||||
stopCheckLogCount = true;
|
||||
for (Log log : impl.listView.getItems()) {
|
||||
if (log.log.contains("Minecraft Crash Report")) {
|
||||
Platform.runLater(() -> {
|
||||
impl.listView.scrollTo(log);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
final 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<>();
|
||||
|
||||
LogWindowImpl() {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/log.fxml");
|
||||
|
||||
engine = webView.getEngine();
|
||||
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/log-window-content.html")))
|
||||
.replace("${FONT}", config().getFontSize() + "px \"" + config().getFontFamily() + "\""));
|
||||
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
|
||||
if (newValue == Worker.State.SUCCEEDED) {
|
||||
document = engine.getDocument();
|
||||
body = document.getElementsByTagName("body").item(0);
|
||||
engine.executeScript("limitedLogs=" + config().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(config().getLogLines()).equals(i)) {
|
||||
cboLines.getSelectionModel().select(i);
|
||||
@@ -196,50 +162,173 @@ public final class LogWindow extends Stage {
|
||||
|
||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
||||
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
|
||||
engine.executeScript("limitedLogs=" + config().getLogLines());
|
||||
});
|
||||
|
||||
if (!flag)
|
||||
cboLines.getSelectionModel().select(0);
|
||||
|
||||
btnFatals.textProperty().bind(Bindings.concat(fatal, " fatals"));
|
||||
btnErrors.textProperty().bind(Bindings.concat(error, " errors"));
|
||||
btnWarns.textProperty().bind(Bindings.concat(warn, " warns"));
|
||||
btnInfos.textProperty().bind(Bindings.concat(info, " infos"));
|
||||
btnDebugs.textProperty().bind(Bindings.concat(debug, " debugs"));
|
||||
|
||||
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) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@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>() {
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
{
|
||||
HBox hBox = new HBox(3);
|
||||
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(hBox);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -37,7 +37,7 @@ public final class SVG {
|
||||
SVGPath path = new SVGPath();
|
||||
path.getStyleClass().add("svg");
|
||||
path.setContent(d);
|
||||
path.fillProperty().bind(fill);
|
||||
if (fill != null) path.fillProperty().bind(fill);
|
||||
|
||||
if (width < 0 || height < 0) {
|
||||
StackPane pane = new StackPane(path);
|
||||
@@ -67,10 +67,18 @@ public final class SVG {
|
||||
return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node copy(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node dotsHorizontal(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node delete(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
|
||||
}
|
||||
@@ -170,4 +178,20 @@ public final class SVG {
|
||||
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node arrowRight(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node wrench(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node upload(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node hanger(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z", fill, width, height);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -44,12 +44,13 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
{
|
||||
List<Node> toolbarButtons = initializeToolbar(skinnable);
|
||||
if (!toolbarButtons.isEmpty()) {
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.getStyleClass().add("jfx-tool-bar-second");
|
||||
JFXDepthManager.setDepth(toolbar, 1);
|
||||
toolbar.setPickOnBounds(false);
|
||||
toolbar.getChildren().setAll(initializeToolbar(skinnable));
|
||||
toolbar.getChildren().setAll(toolbarButtons);
|
||||
root.setTop(toolbar);
|
||||
}
|
||||
|
||||
@@ -92,5 +93,15 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
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);
|
||||
}
|
||||
@@ -1,23 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 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/>.
|
||||
*/
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -39,6 +22,7 @@ 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 static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||
@@ -46,8 +30,6 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class UpgradeDialog extends JFXDialogLayout {
|
||||
private final WebView webView = new WebView();
|
||||
|
||||
public UpgradeDialog(Runnable updateRunnable) {
|
||||
{
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
@@ -55,6 +37,7 @@ public class UpgradeDialog extends JFXDialogLayout {
|
||||
|
||||
{
|
||||
WebView webView = new WebView();
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
WebEngine engine = webView.getEngine();
|
||||
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
|
||||
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -17,20 +17,53 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
|
||||
public class WebStage extends Stage {
|
||||
private final WebView webView = new WebView();
|
||||
protected final StackPane pane = new StackPane();
|
||||
protected final JFXProgressBar progressBar = new JFXProgressBar();
|
||||
protected final WebView webView = new WebView();
|
||||
protected final WebEngine webEngine = webView.getEngine();
|
||||
|
||||
public WebStage() {
|
||||
setScene(new Scene(webView, 800, 480));
|
||||
this(800, 480);
|
||||
}
|
||||
|
||||
public WebStage(int width, int height) {
|
||||
setScene(new Scene(pane, width, height));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
webView.setContextMenuEnabled(false);
|
||||
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
|
||||
|
||||
progressBar.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
switch (webView.getEngine().getLoadWorker().getState()) {
|
||||
case SUCCEEDED:
|
||||
case FAILED:
|
||||
case CANCELLED:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}, webEngine.getLoadWorker().stateProperty()));
|
||||
|
||||
BorderPane borderPane = new BorderPane();
|
||||
borderPane.setPickOnBounds(false);
|
||||
borderPane.setTop(progressBar);
|
||||
progressBar.prefWidthProperty().bind(borderPane.widthProperty());
|
||||
pane.getChildren().setAll(webView, borderPane);
|
||||
}
|
||||
|
||||
public WebView getWebView() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -18,13 +18,17 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
|
||||
@@ -32,6 +36,8 @@ import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
private final Tooltip tooltip;
|
||||
|
||||
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
|
||||
|
||||
@Override
|
||||
@@ -43,16 +49,33 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
setSubtitle(i18n("account.missing.add"));
|
||||
imageProperty().unbind();
|
||||
setImage(newImage("/assets/img/craft_table.png"));
|
||||
tooltip.setText("");
|
||||
} else {
|
||||
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
|
||||
setSubtitle(accountSubtitle(account));
|
||||
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
tooltip.setText(account.getCharacter() + " " + accountTooltip(account));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public AccountAdvancedListItem() {
|
||||
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
|
||||
tooltip = new Tooltip();
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
|
||||
setOnScroll(event -> {
|
||||
Account current = account.get();
|
||||
if (current == null) return;
|
||||
ObservableList<Account> accounts = Accounts.getAccounts();
|
||||
int currentIndex = accounts.indexOf(account.get());
|
||||
if (event.getDeltaY() > 0) { // up
|
||||
currentIndex--;
|
||||
} else { // down
|
||||
currentIndex++;
|
||||
}
|
||||
Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size()));
|
||||
});
|
||||
}
|
||||
|
||||
public ObjectProperty<Account> accountProperty() {
|
||||
@@ -60,11 +83,22 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
}
|
||||
|
||||
private static String accountSubtitle(Account account) {
|
||||
if (account instanceof OfflineAccount)
|
||||
return i18n("account.methods.offline");
|
||||
else if (account instanceof YggdrasilAccount)
|
||||
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
return ((AuthlibInjectorAccount) account).getServer().getName();
|
||||
} else {
|
||||
return loginTypeName;
|
||||
}
|
||||
}
|
||||
|
||||
private static String accountTooltip(Account account) {
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
|
||||
return account.getUsername() + ", " + i18n("account.injector.server") + ": " + server.getName();
|
||||
} else if (account instanceof YggdrasilAccount) {
|
||||
return account.getUsername();
|
||||
else
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -24,11 +24,12 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.ListPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
|
||||
public class AccountList extends ListPage<AccountListItem> implements DecoratorPage {
|
||||
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.manage"));
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage")));
|
||||
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
|
||||
private final ObjectProperty<Account> selectedAccount;
|
||||
|
||||
@@ -51,7 +52,7 @@ public class AccountList extends ListPage<AccountListItem> implements DecoratorP
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyStringProperty titleProperty() {
|
||||
return title.getReadOnlyProperty();
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -18,28 +18,49 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
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;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class AccountListItem extends RadioButton {
|
||||
|
||||
private final Account account;
|
||||
@@ -66,7 +87,9 @@ public class AccountListItem extends RadioButton {
|
||||
if (account instanceof OfflineAccount) {
|
||||
title.bind(characterName);
|
||||
} else {
|
||||
title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
title.bind(
|
||||
account.getUsername().isEmpty() ? characterName :
|
||||
Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
}
|
||||
|
||||
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
@@ -77,9 +100,9 @@ public class AccountListItem extends RadioButton {
|
||||
return new AccountListItemSkin(this);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
account.clearCache();
|
||||
thread(() -> {
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
account.clearCache();
|
||||
try {
|
||||
account.logIn();
|
||||
} catch (CredentialExpiredException e) {
|
||||
@@ -89,13 +112,75 @@ public class AccountListItem extends RadioButton {
|
||||
// ignore cancellation
|
||||
} catch (Exception e1) {
|
||||
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
|
||||
throw e1;
|
||||
}
|
||||
} catch (AuthenticationException e) {
|
||||
LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ObservableBooleanValue canUploadSkin() {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorAccount aiAccount = (AuthlibInjectorAccount) account;
|
||||
ObjectBinding<Optional<CompleteGameProfile>> profile = aiAccount.getYggdrasilService().getProfileRepository().binding(aiAccount.getUUID());
|
||||
return createBooleanBinding(() -> {
|
||||
Set<TextureType> uploadableTextures = profile.get()
|
||||
.map(AuthlibInjectorAccount::getUploadableTextures)
|
||||
.orElse(emptySet());
|
||||
return uploadableTextures.contains(TextureType.SKIN);
|
||||
}, profile);
|
||||
} else {
|
||||
return createBooleanBinding(() -> true);
|
||||
}
|
||||
} else {
|
||||
return createBooleanBinding(() -> false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the skin upload task, null if no file is selected
|
||||
*/
|
||||
@Nullable
|
||||
public Task<?> uploadSkin() {
|
||||
if (!(account instanceof YggdrasilAccount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("account.skin.upload"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return refreshAsync()
|
||||
.thenRunAsync(() -> {
|
||||
BufferedImage skinImg;
|
||||
try {
|
||||
skinImg = ImageIO.read(selectedFile);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidSkinException("Failed to read skin image", e);
|
||||
}
|
||||
if (skinImg == null) {
|
||||
throw new InvalidSkinException("Failed to read skin image");
|
||||
}
|
||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||
String model = skin.isSlim() ? "slim" : "";
|
||||
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
||||
((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath());
|
||||
})
|
||||
.thenComposeAsync(refreshAsync())
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e != null) {
|
||||
Controllers.dialog(AddAccountPane.accountException(e), i18n("account.skin.upload.failed"), MessageType.ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
Accounts.getAccounts().remove(account);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -31,8 +31,11 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
@@ -84,12 +87,40 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
SpinnerPane spinnerRefresh = new SpinnerPane();
|
||||
btnRefresh.setOnMouseClicked(e -> {
|
||||
spinnerRefresh.showSpinner();
|
||||
skinnable.refreshAsync()
|
||||
.whenComplete(ex -> spinnerRefresh.hideSpinner())
|
||||
.start();
|
||||
});
|
||||
btnRefresh.getStyleClass().add("toggle-icon4");
|
||||
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
|
||||
right.getChildren().add(btnRefresh);
|
||||
spinnerRefresh.setContent(btnRefresh);
|
||||
right.getChildren().add(spinnerRefresh);
|
||||
|
||||
JFXButton btnUpload = new JFXButton();
|
||||
SpinnerPane spinnerUpload = new SpinnerPane();
|
||||
btnUpload.setOnMouseClicked(e -> {
|
||||
Task<?> uploadTask = skinnable.uploadSkin();
|
||||
if (uploadTask != null) {
|
||||
spinnerUpload.showSpinner();
|
||||
uploadTask
|
||||
.whenComplete(Schedulers.javafx(), ex -> spinnerUpload.hideSpinner())
|
||||
.start();
|
||||
}
|
||||
});
|
||||
btnUpload.getStyleClass().add("toggle-icon4");
|
||||
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
|
||||
spinnerUpload.managedProperty().bind(spinnerUpload.visibleProperty());
|
||||
spinnerUpload.visibleProperty().bind(skinnable.canUploadSkin());
|
||||
spinnerUpload.setContent(btnUpload);
|
||||
spinnerUpload.getStyleClass().add("small-spinner-pane");
|
||||
right.getChildren().add(spinnerUpload);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnMouseClicked(e -> skinnable.remove());
|
||||
@@ -100,7 +131,8 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
right.getChildren().add(btnRemove);
|
||||
root.setRight(right);
|
||||
|
||||
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
|
||||
root.getStyleClass().add("card");
|
||||
root.setStyle("-fx-padding: 8 8 8 0;");
|
||||
JFXDepthManager.setDepth(root, 1);
|
||||
|
||||
getChildren().setAll(root);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -49,6 +49,7 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -68,6 +69,7 @@ public class AddAccountPane extends StackPane {
|
||||
@FXML private JFXPasswordField txtPassword;
|
||||
@FXML private Label lblCreationWarning;
|
||||
@FXML private Label lblPassword;
|
||||
@FXML private Label lblUsername;
|
||||
@FXML private JFXComboBox<AccountFactory<?>> cboType;
|
||||
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
|
||||
@FXML private Label lblInjectorServer;
|
||||
@@ -88,7 +90,7 @@ public class AddAccountPane extends StackPane {
|
||||
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
|
||||
selectDefaultServer();
|
||||
|
||||
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_MOJANG, Accounts.FACTORY_AUTHLIB_INJECTOR);
|
||||
cboType.getItems().setAll(Accounts.FACTORIES);
|
||||
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
|
||||
// try selecting the preferred login type
|
||||
cboType.getSelectionModel().select(
|
||||
@@ -108,24 +110,26 @@ public class AddAccountPane extends StackPane {
|
||||
// remember the last used login type
|
||||
loginType.addListener((observable, oldValue, newValue) -> config().setPreferredLoginType(Accounts.getLoginType(newValue)));
|
||||
|
||||
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
|
||||
txtUsername.visibleProperty().bind(Bindings.createBooleanBinding(() -> loginType.get().getLoginType().requiresUsername, loginType));
|
||||
lblUsername.visibleProperty().bind(txtUsername.visibleProperty());
|
||||
txtPassword.visibleProperty().bind(Bindings.createBooleanBinding(() -> loginType.get().getLoginType().requiresPassword, loginType));
|
||||
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
|
||||
|
||||
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
|
||||
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
|
||||
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
|
||||
txtUsername.getValidators().add(new Validator(i18n("input.email"), this::validateUsername));
|
||||
|
||||
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> !( // consider the opposite situation: input is valid
|
||||
txtUsername.validate() &&
|
||||
(!txtUsername.isVisible() || txtUsername.validate()) &&
|
||||
// invisible means the field is not needed, neither should it be validated
|
||||
(!txtPassword.isVisible() || txtPassword.validate()) &&
|
||||
(!cboServers.isVisible() || cboServers.getSelectionModel().getSelectedItem() != null)
|
||||
),
|
||||
txtUsername.textProperty(),
|
||||
txtPassword.textProperty(), txtPassword.visibleProperty(),
|
||||
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
|
||||
txtUsername.textProperty(), txtPassword.textProperty(),
|
||||
loginType, cboServers.getSelectionModel().selectedItemProperty(),
|
||||
txtPassword.visibleProperty(), cboServers.visibleProperty()));
|
||||
|
||||
// authlib-injector links
|
||||
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
|
||||
@@ -135,6 +139,20 @@ public class AddAccountPane extends StackPane {
|
||||
linksContainer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
}
|
||||
|
||||
private boolean validateUsername(String username) {
|
||||
AccountFactory<?> loginType = cboType.getSelectionModel().getSelectedItem();
|
||||
if (loginType == Accounts.FACTORY_OFFLINE) {
|
||||
return true;
|
||||
} else if (loginType == Accounts.FACTORY_AUTHLIB_INJECTOR) {
|
||||
AuthlibInjectorServer server = cboServers.getSelectionModel().getSelectedItem();
|
||||
if (server != null && server.isNonEmailLogin()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return username.contains("@");
|
||||
}
|
||||
|
||||
private static final String[] ALLOWED_LINKS = { "register" };
|
||||
|
||||
public static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
|
||||
@@ -241,7 +259,7 @@ public class AddAccountPane extends StackPane {
|
||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||
}
|
||||
|
||||
private class Selector extends BorderPane implements CharacterSelector {
|
||||
private static class Selector extends BorderPane implements CharacterSelector {
|
||||
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final JFXButton cancel = new JFXButton();
|
||||
@@ -317,12 +335,16 @@ public class AddAccountPane extends StackPane {
|
||||
return i18n("account.failed.invalid_token");
|
||||
else if (remoteMessage.contains("Invalid username or password"))
|
||||
return i18n("account.failed.invalid_password");
|
||||
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.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.account;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -28,7 +27,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
@@ -44,7 +43,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class AddAuthlibInjectorServerPane extends StackPane implements DialogAware {
|
||||
|
||||
@FXML private StackPane addServerContainer;
|
||||
@FXML private TransitionPane root;
|
||||
@FXML private Label lblServerUrl;
|
||||
@FXML private Label lblServerName;
|
||||
@FXML private Label lblCreationWarning;
|
||||
@@ -55,8 +54,6 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
@FXML private SpinnerPane nextPane;
|
||||
@FXML private JFXButton btnAddNext;
|
||||
|
||||
private TransitionHandler transitionHandler;
|
||||
|
||||
private AuthlibInjectorServer serverBeingAdded;
|
||||
|
||||
public AddAuthlibInjectorServerPane(String url) {
|
||||
@@ -67,8 +64,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
|
||||
public AddAuthlibInjectorServerPane() {
|
||||
loadFXML(this, "/assets/fxml/authlib-injector-server-add.fxml");
|
||||
transitionHandler = new TransitionHandler(addServerContainer);
|
||||
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
|
||||
root.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
|
||||
|
||||
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
|
||||
nextPane.hideSpinner();
|
||||
@@ -116,7 +112,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
|
||||
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
|
||||
|
||||
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
|
||||
root.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception);
|
||||
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
|
||||
@@ -127,7 +123,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
|
||||
@FXML
|
||||
private void onAddPrev() {
|
||||
transitionHandler.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
|
||||
root.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -18,8 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -31,7 +30,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class AuthlibInjectorServersPage extends ListPage<AuthlibInjectorServerItem> implements DecoratorPage {
|
||||
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.injector.manage.title"));
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.injector.manage.title")));
|
||||
|
||||
private final ObservableList<AuthlibInjectorServerItem> serverItems;
|
||||
|
||||
@@ -50,16 +49,8 @@ public class AuthlibInjectorServersPage extends ListPage<AuthlibInjectorServerIt
|
||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyStringProperty titleProperty() {
|
||||
return title.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
public ReadOnlyObjectWrapper<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.account;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Modality;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.ui.WebStage;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.jackhuang.hmcl.Launcher.COOKIE_MANAGER;
|
||||
|
||||
public class MicrosoftAccountLoginStage extends WebStage implements MicrosoftService.WebViewCallback {
|
||||
public static final MicrosoftAccountLoginStage INSTANCE = new MicrosoftAccountLoginStage();
|
||||
|
||||
CompletableFuture<String> future;
|
||||
Predicate<String> urlTester;
|
||||
|
||||
public MicrosoftAccountLoginStage() {
|
||||
super(600, 600);
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
|
||||
titleProperty().bind(webEngine.titleProperty());
|
||||
|
||||
webEngine.locationProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (urlTester != null && urlTester.test(newValue)) {
|
||||
future.complete(newValue);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
showingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue) {
|
||||
if (future != null) {
|
||||
future.completeExceptionally(new InterruptedException());
|
||||
}
|
||||
future = null;
|
||||
urlTester = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<String> show(MicrosoftService service, Predicate<String> urlTester, String initialURL) {
|
||||
Platform.runLater(() -> {
|
||||
COOKIE_MANAGER.getCookieStore().removeAll();
|
||||
|
||||
webEngine.load(initialURL);
|
||||
show();
|
||||
});
|
||||
this.future = new CompletableFuture<>();
|
||||
this.urlTester = urlTester;
|
||||
return future;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.animation;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -25,4 +26,6 @@ public interface AnimationProducer {
|
||||
void init(AnimationHandler handler);
|
||||
|
||||
List<KeyFrame> animate(AnimationHandler handler);
|
||||
|
||||
@Nullable AnimationProducer opposite();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -21,6 +21,8 @@ import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -41,6 +43,7 @@ public enum ContainerAnimations {
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(1);
|
||||
}, c -> Collections.emptyList()),
|
||||
|
||||
/**
|
||||
* A fade between the old and new view
|
||||
*/
|
||||
@@ -62,6 +65,36 @@ public enum ContainerAnimations {
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH)))),
|
||||
|
||||
/**
|
||||
* A fade between the old and new view
|
||||
*/
|
||||
FADE_IN(c -> {
|
||||
c.getCurrentNode().setTranslateX(0);
|
||||
c.getCurrentNode().setTranslateY(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(0);
|
||||
}, c ->
|
||||
Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)))),
|
||||
|
||||
/**
|
||||
* A fade between the old and new view
|
||||
*/
|
||||
FADE_OUT(c -> {
|
||||
c.getCurrentNode().setTranslateX(0);
|
||||
c.getCurrentNode().setTranslateY(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(1);
|
||||
}, c ->
|
||||
Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)))),
|
||||
/**
|
||||
* A zoom effect
|
||||
*/
|
||||
@@ -143,6 +176,7 @@ public enum ContainerAnimations {
|
||||
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))));
|
||||
|
||||
private final AnimationProducer animationProducer;
|
||||
private ContainerAnimations opposite;
|
||||
|
||||
ContainerAnimations(Consumer<AnimationHandler> init, Function<AnimationHandler, List<KeyFrame>> animationProducer) {
|
||||
this.animationProducer = new AnimationProducer() {
|
||||
@@ -155,10 +189,30 @@ public enum ContainerAnimations {
|
||||
public List<KeyFrame> animate(AnimationHandler handler) {
|
||||
return animationProducer.apply(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable AnimationProducer opposite() {
|
||||
return opposite != null ? opposite.getAnimationProducer() : null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public AnimationProducer getAnimationProducer() {
|
||||
return animationProducer;
|
||||
}
|
||||
|
||||
public ContainerAnimations getOpposite() {
|
||||
return opposite;
|
||||
}
|
||||
|
||||
static {
|
||||
NONE.opposite = NONE;
|
||||
FADE.opposite = FADE;
|
||||
SWIPE_LEFT.opposite = SWIPE_RIGHT;
|
||||
SWIPE_RIGHT.opposite = SWIPE_LEFT;
|
||||
FADE_IN.opposite = FADE_OUT;
|
||||
FADE_OUT.opposite = FADE_IN;
|
||||
ZOOM_IN.opposite = ZOOM_OUT;
|
||||
ZOOM_OUT.opposite = ZOOM_IN;
|
||||
}
|
||||
}
|
||||
+17
-34
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -21,29 +21,17 @@ import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public final class TransitionHandler implements AnimationHandler {
|
||||
private final StackPane view;
|
||||
private Timeline animation;
|
||||
public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
private Duration duration;
|
||||
private Node previousNode, currentNode;
|
||||
|
||||
/**
|
||||
* @param view A stack pane that contains another control that is {@link Parent}
|
||||
*/
|
||||
public TransitionHandler(StackPane view) {
|
||||
this.view = view;
|
||||
currentNode = view.getChildren().stream().findFirst().orElse(null);
|
||||
|
||||
// prevent content overflow
|
||||
Rectangle clip = new Rectangle();
|
||||
clip.widthProperty().bind(view.widthProperty());
|
||||
clip.heightProperty().bind(view.heightProperty());
|
||||
view.setClip(clip);
|
||||
{
|
||||
currentNode = getChildren().stream().findFirst().orElse(null);
|
||||
FXUtils.setOverflowHidden(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,7 +46,7 @@ public final class TransitionHandler implements AnimationHandler {
|
||||
|
||||
@Override
|
||||
public StackPane getCurrentRoot() {
|
||||
return view;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,35 +55,30 @@ public final class TransitionHandler implements AnimationHandler {
|
||||
}
|
||||
|
||||
public void setContent(Node newView, AnimationProducer transition) {
|
||||
setContent(newView, transition, Duration.millis(320));
|
||||
setContent(newView, transition, Duration.millis(160));
|
||||
}
|
||||
|
||||
public void setContent(Node newView, AnimationProducer transition, Duration duration) {
|
||||
this.duration = duration;
|
||||
|
||||
Timeline prev = animation;
|
||||
if (prev != null)
|
||||
prev.stop();
|
||||
|
||||
updateContent(newView);
|
||||
|
||||
transition.init(this);
|
||||
|
||||
// runLater or "init" will not work
|
||||
Platform.runLater(() -> {
|
||||
Timeline nowAnimation = new Timeline();
|
||||
nowAnimation.getKeyFrames().addAll(transition.animate(this));
|
||||
nowAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
|
||||
view.setMouseTransparent(false);
|
||||
view.getChildren().remove(previousNode);
|
||||
Timeline newAnimation = new Timeline();
|
||||
newAnimation.getKeyFrames().addAll(transition.animate(this));
|
||||
newAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
|
||||
setMouseTransparent(false);
|
||||
getChildren().remove(previousNode);
|
||||
}));
|
||||
nowAnimation.play();
|
||||
animation = nowAnimation;
|
||||
FXUtils.playAnimation(this, "transition_pane", newAnimation);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateContent(Node newView) {
|
||||
if (view.getWidth() > 0 && view.getHeight() > 0) {
|
||||
if (getWidth() > 0 && getHeight() > 0) {
|
||||
previousNode = currentNode;
|
||||
if (previousNode == null)
|
||||
previousNode = EMPTY_PANE;
|
||||
@@ -105,11 +88,11 @@ public final class TransitionHandler implements AnimationHandler {
|
||||
if (previousNode == newView)
|
||||
previousNode = EMPTY_PANE;
|
||||
|
||||
view.setMouseTransparent(true);
|
||||
setMouseTransparent(true);
|
||||
|
||||
currentNode = newView;
|
||||
|
||||
view.getChildren().setAll(previousNode, currentNode);
|
||||
getChildren().setAll(previousNode, currentNode);
|
||||
}
|
||||
|
||||
private final StackPane EMPTY_PANE = new StackPane();
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -53,22 +53,37 @@ public class AdvancedListBox extends ScrollPane {
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdvancedListBox remove(Node child) {
|
||||
if (child instanceof Pane)
|
||||
container.getChildren().remove(child);
|
||||
public AdvancedListBox add(int index, Node child) {
|
||||
if (child instanceof Pane || child instanceof AdvancedListItem)
|
||||
container.getChildren().add(index, child);
|
||||
else {
|
||||
StackPane pane = null;
|
||||
for (Node node : container.getChildren())
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("advanced-list-box-item");
|
||||
pane.getChildren().setAll(child);
|
||||
container.getChildren().add(index, pane);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdvancedListBox remove(Node child) {
|
||||
container.getChildren().remove(indexOf(child));
|
||||
return this;
|
||||
}
|
||||
|
||||
public int indexOf(Node child) {
|
||||
if (child instanceof Pane) {
|
||||
return container.getChildren().indexOf(child);
|
||||
} else {
|
||||
for (int i = 0; i < container.getChildren().size(); ++i) {
|
||||
Node node = container.getChildren().get(i);
|
||||
if (node instanceof StackPane) {
|
||||
ObservableList<Node> list = ((StackPane) node).getChildren();
|
||||
if (list.size() == 1 && list.get(0) == child)
|
||||
pane = (StackPane) node;
|
||||
return i;
|
||||
}
|
||||
if (pane == null)
|
||||
throw new Error();
|
||||
container.getChildren().remove(pane);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdvancedListBox startCategory(String category) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -30,10 +30,12 @@ public class AdvancedListItem extends Control {
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
|
||||
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
|
||||
|
||||
public AdvancedListItem() {
|
||||
getStyleClass().add("advanced-list-item");
|
||||
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
|
||||
}
|
||||
|
||||
@@ -73,6 +75,18 @@ public class AdvancedListItem extends Control {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active.get();
|
||||
}
|
||||
|
||||
public BooleanProperty activeProperty() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active.set(active);
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
@@ -30,13 +31,19 @@ import javafx.scene.text.TextAlignment;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
public AdvancedListItemSkin(AdvancedListItem skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getStyleClass().add("container");
|
||||
RipplerContainer container = new RipplerContainer(stackPane);
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.activeProperty(), active -> {
|
||||
skinnable.pseudoClassStateChanged(SELECTED, active);
|
||||
});
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.setPickOnBounds(false);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -34,6 +34,8 @@ import javafx.scene.control.Control;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@DefaultProperty("content")
|
||||
public class ComponentList extends Control {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
|
||||
@@ -41,6 +43,8 @@ public class ComponentList extends Control {
|
||||
private final IntegerProperty depth = new SimpleIntegerProperty(this, "depth", 0);
|
||||
private boolean hasSubtitle = false;
|
||||
public final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||
private boolean expanded = false;
|
||||
private Consumer<ComponentList> lazyInitializer;
|
||||
|
||||
public ComponentList() {
|
||||
getStyleClass().add("options-list");
|
||||
@@ -94,6 +98,18 @@ public class ComponentList extends Control {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setLazyInitializer(Consumer<ComponentList> lazyInitializer) {
|
||||
this.lazyInitializer = lazyInitializer;
|
||||
}
|
||||
|
||||
public void onExpand() {
|
||||
if (!expanded && lazyInitializer != null) {
|
||||
lazyInitializer.accept(this);
|
||||
}
|
||||
|
||||
expanded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected javafx.scene.control.Skin<?> createDefaultSkin() {
|
||||
return new Skin(this);
|
||||
@@ -101,9 +117,11 @@ public class ComponentList extends Control {
|
||||
|
||||
protected static class Skin extends SkinBase<ComponentList> {
|
||||
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
|
||||
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
private final ObservableList<Node> list;
|
||||
private final ObjectBinding<Node> firstItem;
|
||||
private final ObjectBinding<Node> lastItem;
|
||||
|
||||
protected Skin(ComponentList control) {
|
||||
super(control);
|
||||
@@ -124,6 +142,16 @@ public class ComponentList extends Control {
|
||||
if (!list.isEmpty())
|
||||
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
|
||||
|
||||
lastItem = Bindings.valueAt(list, Bindings.subtract(Bindings.size(list), 1));
|
||||
lastItem.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
newValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
||||
if (oldValue != null)
|
||||
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
|
||||
});
|
||||
if (!list.isEmpty())
|
||||
list.get(list.size() - 1).pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
|
||||
|
||||
VBox vbox = new VBox();
|
||||
Bindings.bindContent(vbox.getChildren(), list);
|
||||
getChildren().setAll(vbox);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -126,7 +126,7 @@ class ComponentListCell extends StackPane {
|
||||
VBox container = new VBox();
|
||||
container.setPadding(new Insets(8, 0, 0, 0));
|
||||
FXUtils.setLimitHeight(container, 0);
|
||||
FXUtils.setOverflowHidden(container, true);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
container.getChildren().setAll(content);
|
||||
groupNode.setBottom(container);
|
||||
|
||||
@@ -143,6 +143,7 @@ class ComponentListCell extends StackPane {
|
||||
|
||||
if (isExpanded()) {
|
||||
updateClip(newHeight);
|
||||
list.onExpand();
|
||||
}
|
||||
|
||||
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
|
||||
@@ -161,8 +162,9 @@ class ComponentListCell extends StackPane {
|
||||
expandIcon.setRotate(newValue ? 180 : 0));
|
||||
|
||||
getChildren().setAll(groupNode);
|
||||
} else
|
||||
} else {
|
||||
getChildren().setAll(content);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -23,7 +23,7 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
|
||||
@DefaultProperty("content")
|
||||
public final class ComponentSublist extends ComponentList {
|
||||
public class ComponentSublist extends ComponentList {
|
||||
|
||||
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
|
||||
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -75,7 +75,13 @@ public class FileItem extends BorderPane {
|
||||
* Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}.
|
||||
*/
|
||||
private String processPath(String path) {
|
||||
Path given = Paths.get(path).toAbsolutePath();
|
||||
Path given;
|
||||
try {
|
||||
given = Paths.get(path).toAbsolutePath();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (isConvertToRelativePath()) {
|
||||
try {
|
||||
return Paths.get(".").normalize().toAbsolutePath().relativize(given).normalize().toString();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public abstract class FloatListCell<T> extends ListCell<T> {
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
protected final StackPane pane = new StackPane();
|
||||
|
||||
{
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
|
||||
pane.getStyleClass().add("card");
|
||||
pane.setCursor(Cursor.HAND);
|
||||
setPadding(new Insets(9, 9, 0, 9));
|
||||
JFXDepthManager.setDepth(pane, 1);
|
||||
|
||||
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
|
||||
pane.pseudoClassStateChanged(SELECTED, selected);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
updateControl(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(pane);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void updateControl(T dataItem, boolean empty);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.NumberBinding;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollBar;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
private ScrollBar scrollBar;
|
||||
private Region group;
|
||||
private Rectangle track = new Rectangle();
|
||||
private Rectangle thumb = new Rectangle();
|
||||
|
||||
public FloatScrollBarSkin(final ScrollBar scrollBar) {
|
||||
this.scrollBar = scrollBar;
|
||||
scrollBar.setPrefHeight(1e-18);
|
||||
scrollBar.setPrefWidth(1e-18);
|
||||
|
||||
this.group = new Region() {
|
||||
Point2D dragStart;
|
||||
double preDragThumbPos;
|
||||
|
||||
NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
|
||||
NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
|
||||
|
||||
{
|
||||
// Children are added unmanaged because for some reason the height of the bar keeps changing
|
||||
// if they're managed in certain situations... not sure about the cause.
|
||||
getChildren().addAll(track, thumb);
|
||||
|
||||
track.setManaged(false);
|
||||
track.getStyleClass().add("track");
|
||||
|
||||
thumb.setManaged(false);
|
||||
thumb.getStyleClass().add("thumb");
|
||||
|
||||
scrollBar.orientationProperty().addListener(obs -> setup());
|
||||
|
||||
setup();
|
||||
|
||||
|
||||
thumb.setOnMousePressed(me -> {
|
||||
if (me.isSynthesized()) {
|
||||
// touch-screen events handled by Scroll handler
|
||||
me.consume();
|
||||
return;
|
||||
}
|
||||
/*
|
||||
** if max isn't greater than min then there is nothing to do here
|
||||
*/
|
||||
if (getSkinnable().getMax() > getSkinnable().getMin()) {
|
||||
dragStart = thumb.localToParent(me.getX(), me.getY());
|
||||
double clampedValue = Lang.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax());
|
||||
preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin());
|
||||
me.consume();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
thumb.setOnMouseDragged(me -> {
|
||||
if (me.isSynthesized()) {
|
||||
// touch-screen events handled by Scroll handler
|
||||
me.consume();
|
||||
return;
|
||||
}
|
||||
/*
|
||||
** if max isn't greater than min then there is nothing to do here
|
||||
*/
|
||||
if (getSkinnable().getMax() > getSkinnable().getMin()) {
|
||||
/*
|
||||
** if the tracklength isn't greater then do nothing....
|
||||
*/
|
||||
if (trackLength() > thumbLength()) {
|
||||
Point2D cur = thumb.localToParent(me.getX(), me.getY());
|
||||
if (dragStart == null) {
|
||||
// we're getting dragged without getting a mouse press
|
||||
dragStart = thumb.localToParent(me.getX(), me.getY());
|
||||
}
|
||||
double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX();
|
||||
double position = preDragThumbPos + dragPos / (trackLength() - thumbLength());
|
||||
if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable()) getSkinnable().requestFocus();
|
||||
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
|
||||
if (!Double.isNaN(newValue)) {
|
||||
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
|
||||
}
|
||||
}
|
||||
|
||||
me.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private double trackLength() {
|
||||
return getSkinnable().getOrientation() == Orientation.VERTICAL ? track.getHeight() : track.getWidth();
|
||||
}
|
||||
|
||||
private double thumbLength() {
|
||||
return getSkinnable().getOrientation() == Orientation.VERTICAL ? thumb.getHeight() : thumb.getWidth();
|
||||
}
|
||||
|
||||
private void setup() {
|
||||
track.widthProperty().unbind();
|
||||
track.heightProperty().unbind();
|
||||
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
track.relocate(0, -5);
|
||||
track.widthProperty().bind(scrollBar.widthProperty());
|
||||
track.setHeight(5);
|
||||
} else {
|
||||
track.relocate(-5, 0);
|
||||
track.setWidth(5);
|
||||
track.heightProperty().bind(scrollBar.heightProperty());
|
||||
}
|
||||
|
||||
thumb.xProperty().unbind();
|
||||
thumb.yProperty().unbind();
|
||||
thumb.widthProperty().unbind();
|
||||
thumb.heightProperty().unbind();
|
||||
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
thumb.relocate(0, -5);
|
||||
thumb.widthProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
|
||||
thumb.setHeight(5);
|
||||
thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position));
|
||||
} else {
|
||||
thumb.relocate(-5, 0);
|
||||
thumb.setWidth(5);
|
||||
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
|
||||
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxWidth(double height) {
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxHeight(double width) {
|
||||
if (scrollBar.getOrientation() == Orientation.VERTICAL) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
return 5;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
scrollBar = null;
|
||||
group = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getNode() {
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScrollBar getSkinnable() {
|
||||
return scrollBar;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -19,50 +19,53 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class InputDialogPane extends StackPane {
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
|
||||
@FXML
|
||||
private JFXButton acceptButton;
|
||||
@FXML
|
||||
private JFXButton cancelButton;
|
||||
@FXML
|
||||
private JFXTextField textField;
|
||||
private Label title;
|
||||
@FXML
|
||||
private Label content;
|
||||
private VBox vbox;
|
||||
@FXML
|
||||
private Label lblCreationWarning;
|
||||
@FXML
|
||||
private SpinnerPane acceptPane;
|
||||
|
||||
public InputDialogPane(String text, FutureCallback<String> onResult) {
|
||||
public InputDialogPane(String text, String initialValue, FutureCallback<String> onResult) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
|
||||
content.setText(text);
|
||||
title.setText(text);
|
||||
JFXTextField textField = new JFXTextField();
|
||||
textField.setText(initialValue);
|
||||
vbox.getChildren().setAll(textField);
|
||||
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
|
||||
acceptButton.setOnMouseClicked(e -> {
|
||||
acceptPane.showSpinner();
|
||||
|
||||
onResult.call(textField.getText(), () -> {
|
||||
acceptPane.hideSpinner();
|
||||
future.complete(textField.getText());
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}, msg -> {
|
||||
acceptPane.hideSpinner();
|
||||
lblCreationWarning.setText(msg);
|
||||
});
|
||||
});
|
||||
|
||||
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> !textField.validate(),
|
||||
textField.textProperty()
|
||||
));
|
||||
}
|
||||
|
||||
public void setInitialText(String text) {
|
||||
textField.setText(text);
|
||||
public CompletableFuture<String> getCompletableFuture() {
|
||||
return future;
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 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.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
|
||||
private final StackPane pane;
|
||||
private final CheckBox checkBox;
|
||||
private boolean showLabel;
|
||||
private ObservableValue<Boolean> booleanProperty;
|
||||
|
||||
public JFXCheckBoxTreeTableCell() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
public JFXCheckBoxTreeTableCell(
|
||||
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
|
||||
this(getSelectedProperty, null);
|
||||
}
|
||||
|
||||
public JFXCheckBoxTreeTableCell(
|
||||
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty,
|
||||
final StringConverter<T> converter) {
|
||||
this.getStyleClass().add("check-box-tree-table-cell");
|
||||
this.checkBox = new JFXCheckBox();
|
||||
this.pane = new StackPane(checkBox);
|
||||
this.pane.setAlignment(Pos.CENTER);
|
||||
setGraphic(null);
|
||||
setSelectedStateCallback(getSelectedProperty);
|
||||
setConverter(converter);
|
||||
}
|
||||
|
||||
private ObjectProperty<StringConverter<T>> converter =
|
||||
new SimpleObjectProperty<StringConverter<T>>(this, "converter") {
|
||||
protected void invalidated() {
|
||||
updateShowLabel();
|
||||
}
|
||||
};
|
||||
|
||||
public final ObjectProperty<StringConverter<T>> converterProperty() {
|
||||
return converter;
|
||||
}
|
||||
public final void setConverter(StringConverter<T> value) {
|
||||
converterProperty().set(value);
|
||||
}
|
||||
public final StringConverter<T> getConverter() {
|
||||
return converterProperty().get();
|
||||
}
|
||||
|
||||
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
|
||||
selectedStateCallback = new SimpleObjectProperty<>(this, "selectedStateCallback");
|
||||
|
||||
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
|
||||
return selectedStateCallback;
|
||||
}
|
||||
|
||||
public final void setSelectedStateCallback(Callback<Integer, ObservableValue<Boolean>> value) {
|
||||
selectedStateCallbackProperty().set(value);
|
||||
}
|
||||
|
||||
public final Callback<Integer, ObservableValue<Boolean>> getSelectedStateCallback() {
|
||||
return selectedStateCallbackProperty().get();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
StringConverter<T> c = getConverter();
|
||||
|
||||
if (showLabel) {
|
||||
setText(c.toString(item));
|
||||
}
|
||||
|
||||
setGraphic(pane);
|
||||
|
||||
if (booleanProperty instanceof BooleanProperty) {
|
||||
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
|
||||
}
|
||||
ObservableValue<?> obsValue = getSelectedProperty();
|
||||
if (obsValue instanceof BooleanProperty) {
|
||||
booleanProperty = (ObservableValue<Boolean>) obsValue;
|
||||
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
|
||||
}
|
||||
|
||||
checkBox.disableProperty().bind(Bindings.not(
|
||||
getTreeTableView().editableProperty().and(
|
||||
getTableColumn().editableProperty()).and(
|
||||
editableProperty())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShowLabel() {
|
||||
this.showLabel = converter != null;
|
||||
this.checkBox.setAlignment(showLabel ? Pos.CENTER_LEFT : Pos.CENTER);
|
||||
}
|
||||
|
||||
private ObservableValue<?> getSelectedProperty() {
|
||||
return getSelectedStateCallback() != null ?
|
||||
getSelectedStateCallback().call(getIndex()) :
|
||||
getTableColumn().getCellObservableValue(getIndex());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -38,7 +38,6 @@ public final class MessageDialogPane extends StackPane {
|
||||
WARNING,
|
||||
QUESTION,
|
||||
FINE,
|
||||
PLAIN,
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -47,7 +47,7 @@ import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class MultiFileItem<T> extends ComponentList {
|
||||
public class MultiFileItem<T> extends ComponentSublist {
|
||||
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
|
||||
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
|
||||
private final BooleanProperty directory = new SimpleBooleanProperty(this, "directory", false);
|
||||
@@ -148,6 +148,7 @@ public class MultiFileItem<T> extends ComponentList {
|
||||
pane.setLeft(left);
|
||||
|
||||
Label right = new Label(subtitle);
|
||||
right.setWrapText(true);
|
||||
right.getStyleClass().add("subtitle-label");
|
||||
right.setStyle("-fx-font-size: 10;");
|
||||
pane.setRight(right);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -17,32 +17,35 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.event.EventType;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
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.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Stack;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class Navigator extends StackPane {
|
||||
public class Navigator extends TransitionPane {
|
||||
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Navigator.class.getName() + ".closeListener";
|
||||
|
||||
private final BooleanProperty backable = new SimpleBooleanProperty(this, "backable");
|
||||
private final Stack<Node> stack = new Stack<>();
|
||||
private final TransitionHandler animationHandler = new TransitionHandler(this);
|
||||
private boolean initialized = false;
|
||||
|
||||
public void init(Node init) {
|
||||
stack.push(init);
|
||||
backable.set(canGoBack());
|
||||
getChildren().setAll(init);
|
||||
|
||||
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED));
|
||||
@@ -50,7 +53,7 @@ public class Navigator extends StackPane {
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public void navigate(Node node) {
|
||||
public void navigate(Node node, AnimationProducer animationProducer) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
if (!initialized)
|
||||
@@ -63,15 +66,16 @@ public class Navigator extends StackPane {
|
||||
Logging.LOG.info("Navigate to " + node);
|
||||
|
||||
stack.push(node);
|
||||
backable.set(canGoBack());
|
||||
|
||||
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
|
||||
fireEvent(navigating);
|
||||
node.fireEvent(navigating);
|
||||
|
||||
setContent(node);
|
||||
node.getProperties().put("hmcl.navigator.animation", animationProducer);
|
||||
setContent(node, animationProducer);
|
||||
|
||||
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
|
||||
fireEvent(navigated);
|
||||
node.fireEvent(navigated);
|
||||
|
||||
EventHandler<PageCloseEvent> handler = event -> close(node);
|
||||
@@ -104,16 +108,21 @@ public class Navigator extends StackPane {
|
||||
Logging.LOG.info("Closed page " + from);
|
||||
|
||||
stack.pop();
|
||||
backable.set(canGoBack());
|
||||
Node node = stack.peek();
|
||||
|
||||
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
|
||||
fireEvent(navigating);
|
||||
node.fireEvent(navigating);
|
||||
|
||||
setContent(node);
|
||||
Object obj = from.getProperties().get("hmcl.navigator.animation");
|
||||
if (obj instanceof AnimationProducer) {
|
||||
setContent(node, (AnimationProducer) obj);
|
||||
} else {
|
||||
setContent(node, ContainerAnimations.NONE.getAnimationProducer());
|
||||
}
|
||||
|
||||
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
|
||||
fireEvent(navigated);
|
||||
node.fireEvent(navigated);
|
||||
|
||||
Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
|
||||
@@ -128,12 +137,28 @@ public class Navigator extends StackPane {
|
||||
return stack.size() > 1;
|
||||
}
|
||||
|
||||
private void setContent(Node content) {
|
||||
animationHandler.setContent(content, ContainerAnimations.FADE.getAnimationProducer());
|
||||
public boolean isBackable() {
|
||||
return backable.get();
|
||||
}
|
||||
|
||||
public BooleanProperty backableProperty() {
|
||||
return backable;
|
||||
}
|
||||
|
||||
public void setBackable(boolean backable) {
|
||||
this.backable.set(backable);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return stack.size();
|
||||
}
|
||||
|
||||
public void setContent(Node content, AnimationProducer animationProducer) {
|
||||
super.setContent(content, animationProducer);
|
||||
|
||||
if (content instanceof Region) {
|
||||
((Region) content).setMinSize(0, 0);
|
||||
FXUtils.setOverflowHidden((Region) content, true);
|
||||
FXUtils.setOverflowHidden((Region) content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,14 +204,21 @@ public class Navigator extends StackPane {
|
||||
public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED");
|
||||
public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING");
|
||||
|
||||
private final Navigator source;
|
||||
private final Node node;
|
||||
|
||||
public NavigationEvent(Object source, Node target, EventType<? extends Event> eventType) {
|
||||
public NavigationEvent(Navigator source, Node target, EventType<? extends Event> eventType) {
|
||||
super(source, target, eventType);
|
||||
|
||||
this.source = source;
|
||||
this.node = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Navigator getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public Node getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -38,6 +38,7 @@ public class PopupMenu extends Control {
|
||||
private final BooleanProperty alwaysShowingVBar = new SimpleBooleanProperty();
|
||||
|
||||
public PopupMenu() {
|
||||
getStyleClass().add("popup-menu");
|
||||
}
|
||||
|
||||
public ObservableList<Node> getContent() {
|
||||
@@ -82,7 +83,7 @@ public class PopupMenu extends Control {
|
||||
.otherwise(ScrollPane.ScrollBarPolicy.AS_NEEDED));
|
||||
|
||||
VBox content = new VBox();
|
||||
content.getStyleClass().add("menu");
|
||||
content.getStyleClass().add("content");
|
||||
Bindings.bindContent(content.getChildren(), PopupMenu.this.getContent());
|
||||
scrollPane.setContent(content);
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class PromptDialogPane extends StackPane {
|
||||
private final CompletableFuture<List<Builder.Question<?>>> future = new CompletableFuture<>();
|
||||
|
||||
@FXML
|
||||
private JFXButton acceptButton;
|
||||
@FXML
|
||||
private JFXButton cancelButton;
|
||||
@FXML
|
||||
private VBox vbox;
|
||||
@FXML
|
||||
private Label title;
|
||||
@FXML
|
||||
private Label lblCreationWarning;
|
||||
@FXML
|
||||
private SpinnerPane acceptPane;
|
||||
|
||||
public PromptDialogPane(Builder builder) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
|
||||
this.title.setText(builder.title);
|
||||
|
||||
List<BooleanBinding> bindings = new ArrayList<>();
|
||||
for (Builder.Question<?> question : builder.questions) {
|
||||
if (question instanceof Builder.StringQuestion) {
|
||||
JFXTextField textField = new JFXTextField();
|
||||
textField.textProperty().addListener((a, b, newValue) -> ((Builder.StringQuestion) question).value = textField.getText());
|
||||
textField.setText(((Builder.StringQuestion) question).value);
|
||||
textField.setValidators(((Builder.StringQuestion) question).validators.toArray(new ValidatorBase[0]));
|
||||
bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty()));
|
||||
|
||||
if (StringUtils.isNotBlank(question.question)) {
|
||||
vbox.getChildren().add(new Label(question.question));
|
||||
}
|
||||
VBox.setMargin(textField, new Insets(0, 0, 20, 0));
|
||||
vbox.getChildren().add(textField);
|
||||
} else if (question instanceof Builder.BooleanQuestion) {
|
||||
HBox hBox = new HBox();
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
hBox.getChildren().setAll(checkBox);
|
||||
HBox.setMargin(checkBox, new Insets(0, 0, 0, -10));
|
||||
checkBox.setSelected(((Builder.BooleanQuestion) question).value);
|
||||
checkBox.selectedProperty().addListener((a, b, newValue) -> ((Builder.BooleanQuestion) question).value = newValue);
|
||||
checkBox.setText(question.question);
|
||||
vbox.getChildren().add(hBox);
|
||||
} else if (question instanceof Builder.CandidatesQuestion) {
|
||||
HBox hBox = new HBox();
|
||||
JFXComboBox<String> comboBox = new JFXComboBox<>();
|
||||
hBox.getChildren().setAll(comboBox);
|
||||
comboBox.getItems().setAll(((Builder.CandidatesQuestion) question).candidates);
|
||||
comboBox.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) ->
|
||||
((Builder.CandidatesQuestion) question).value = newValue.intValue());
|
||||
comboBox.getSelectionModel().select(0);
|
||||
if (StringUtils.isNotBlank(question.question)) {
|
||||
vbox.getChildren().add(new Label(question.question));
|
||||
}
|
||||
vbox.getChildren().add(hBox);
|
||||
}
|
||||
}
|
||||
|
||||
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
|
||||
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> bindings.stream().map(BooleanBinding::get).anyMatch(x -> !x),
|
||||
bindings.toArray(new BooleanBinding[0])
|
||||
));
|
||||
|
||||
acceptButton.setOnMouseClicked(e -> {
|
||||
acceptPane.showSpinner();
|
||||
|
||||
builder.callback.call(builder.questions, () -> {
|
||||
acceptPane.hideSpinner();
|
||||
future.complete(builder.questions);
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}, msg -> {
|
||||
acceptPane.hideSpinner();
|
||||
lblCreationWarning.setText(msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Builder.Question<?>>> getCompletableFuture() {
|
||||
return future;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final List<Question<?>> questions = new ArrayList<>();
|
||||
private final String title;
|
||||
private final FutureCallback<List<Question<?>>> callback;
|
||||
|
||||
public Builder(String title, FutureCallback<List<Question<?>>> callback) {
|
||||
this.title = title;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public <T> Builder addQuestion(Question<T> question) {
|
||||
questions.add(question);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static class Question<T> {
|
||||
public final String question;
|
||||
protected T value;
|
||||
|
||||
public Question(String question) {
|
||||
this.question = question;
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringQuestion extends Question<String> {
|
||||
protected final List<ValidatorBase> validators;
|
||||
|
||||
public StringQuestion(String question, String defaultValue, ValidatorBase... validators) {
|
||||
super(question);
|
||||
this.value = defaultValue;
|
||||
this.validators = Arrays.asList(validators);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CandidatesQuestion extends Question<Integer> {
|
||||
protected final List<String> candidates;
|
||||
|
||||
public CandidatesQuestion(String question, String... candidates) {
|
||||
super(question);
|
||||
this.value = null;
|
||||
if (candidates == null || candidates.length == 0) {
|
||||
throw new IllegalArgumentException("At least one candidate required");
|
||||
}
|
||||
this.candidates = new ArrayList<>(Arrays.asList(candidates));
|
||||
}
|
||||
}
|
||||
|
||||
public static class BooleanQuestion extends Question<Boolean> {
|
||||
|
||||
public BooleanQuestion(String question, boolean defaultValue) {
|
||||
super(question);
|
||||
this.value = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class RequiredValidator extends ValidatorBase {
|
||||
|
||||
public RequiredValidator() {
|
||||
}
|
||||
|
||||
public RequiredValidator(@NamedArg("message") String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void eval() {
|
||||
if (srcControl.get() instanceof TextInputControl) {
|
||||
evalTextInputField();
|
||||
}
|
||||
}
|
||||
|
||||
private void evalTextInputField() {
|
||||
TextInputControl textField = ((TextInputControl) srcControl.get());
|
||||
|
||||
hasErrors.set(StringUtils.isBlank(textField.getText()));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -18,38 +18,28 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationHandler;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
|
||||
|
||||
@DefaultProperty("content")
|
||||
public class SpinnerPane extends StackPane {
|
||||
private final TransitionHandler transitionHandler = new TransitionHandler(this);
|
||||
private final JFXSpinner spinner = new JFXSpinner();
|
||||
private final StackPane contentPane = new StackPane();
|
||||
public class SpinnerPane extends Control {
|
||||
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading") {
|
||||
protected void invalidated() {
|
||||
if (get())
|
||||
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
|
||||
else
|
||||
transitionHandler.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
|
||||
}
|
||||
};
|
||||
|
||||
public SpinnerPane() {
|
||||
getStyleClass().add("spinner-pane");
|
||||
|
||||
getChildren().setAll(contentPane);
|
||||
|
||||
content.addListener((a, b, newValue) -> contentPane.getChildren().setAll(newValue));
|
||||
}
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
|
||||
|
||||
public void showSpinner() {
|
||||
setLoading(true);
|
||||
@@ -82,4 +72,72 @@ public class SpinnerPane extends StackPane {
|
||||
public void setLoading(boolean loading) {
|
||||
this.loading.set(loading);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin createDefaultSkin() {
|
||||
return new Skin(this);
|
||||
}
|
||||
|
||||
private static class Skin extends SkinBase<SpinnerPane> {
|
||||
private final JFXSpinner spinner = new JFXSpinner();
|
||||
private final StackPane contentPane = new StackPane();
|
||||
private final StackPane topPane = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
private Timeline animation;
|
||||
|
||||
protected Skin(SpinnerPane control) {
|
||||
super(control);
|
||||
|
||||
root.getStyleClass().add("spinner-pane");
|
||||
topPane.getChildren().setAll(spinner);
|
||||
root.getChildren().setAll(contentPane, topPane);
|
||||
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> contentPane.getChildren().setAll(newValue));
|
||||
getChildren().setAll(root);
|
||||
|
||||
FXUtils.onChangeAndOperate(getSkinnable().loadingProperty(), newValue -> {
|
||||
Timeline prev = animation;
|
||||
if (prev != null) prev.stop();
|
||||
|
||||
AnimationProducer transition;
|
||||
topPane.setMouseTransparent(true);
|
||||
topPane.setVisible(true);
|
||||
topPane.getStyleClass().add("gray-background");
|
||||
if (newValue)
|
||||
transition = ContainerAnimations.FADE_IN.getAnimationProducer();
|
||||
else
|
||||
transition = ContainerAnimations.FADE_OUT.getAnimationProducer();
|
||||
|
||||
AnimationHandler handler = new AnimationHandler() {
|
||||
@Override
|
||||
public Duration getDuration() {
|
||||
return Duration.millis(160);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pane getCurrentRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getPreviousNode() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getCurrentNode() {
|
||||
return topPane;
|
||||
}
|
||||
};
|
||||
|
||||
Timeline now = new Timeline();
|
||||
now.getKeyFrames().addAll(transition.animate(handler));
|
||||
now.getKeyFrames().add(new KeyFrame(handler.getDuration(), e -> {
|
||||
topPane.setMouseTransparent(!newValue);
|
||||
topPane.setVisible(newValue);
|
||||
}));
|
||||
now.play();
|
||||
animation = now;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.AccessibleAttribute;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SingleSelectionModel;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface TabControl {
|
||||
ObservableList<Tab> getTabs();
|
||||
|
||||
class TabControlSelectionModel extends SingleSelectionModel<Tab> {
|
||||
private final TabControl tabHeader;
|
||||
|
||||
public TabControlSelectionModel(final TabControl t) {
|
||||
if (t == null) {
|
||||
throw new NullPointerException("TabPane can not be null");
|
||||
}
|
||||
this.tabHeader = t;
|
||||
|
||||
// watching for changes to the items list content
|
||||
final ListChangeListener<Tab> itemsContentObserver = c -> {
|
||||
while (c.next()) {
|
||||
for (Tab tab : c.getRemoved()) {
|
||||
if (tab != null && !tabHeader.getTabs().contains(tab)) {
|
||||
if (tab.isSelected()) {
|
||||
tab.setSelected(false);
|
||||
final int tabIndex = c.getFrom();
|
||||
|
||||
// we always try to select the nearest, non-disabled
|
||||
// tab from the position of the closed tab.
|
||||
findNearestAvailableTab(tabIndex, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c.wasAdded() || c.wasRemoved()) {
|
||||
// The selected tab index can be out of sync with the list of tab if
|
||||
// we add or remove tabs before the selected tab.
|
||||
if (getSelectedIndex() != tabHeader.getTabs().indexOf(getSelectedItem())) {
|
||||
clearAndSelect(tabHeader.getTabs().indexOf(getSelectedItem()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (getSelectedIndex() == -1 && getSelectedItem() == null && tabHeader.getTabs().size() > 0) {
|
||||
// we go looking for the first non-disabled tab, as opposed to
|
||||
// just selecting the first tab (fix for RT-36908)
|
||||
findNearestAvailableTab(0, true);
|
||||
} else if (tabHeader.getTabs().isEmpty()) {
|
||||
clearSelection();
|
||||
}
|
||||
};
|
||||
if (this.tabHeader.getTabs() != null) {
|
||||
this.tabHeader.getTabs().addListener(itemsContentObserver);
|
||||
}
|
||||
}
|
||||
|
||||
// API Implementation
|
||||
@Override public void select(int index) {
|
||||
if (index < 0 || (getItemCount() > 0 && index >= getItemCount()) ||
|
||||
(index == getSelectedIndex() && getModelItem(index).isSelected())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unselect the old tab
|
||||
if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) {
|
||||
tabHeader.getTabs().get(getSelectedIndex()).setSelected(false);
|
||||
}
|
||||
|
||||
setSelectedIndex(index);
|
||||
|
||||
Tab tab = getModelItem(index);
|
||||
if (tab != null) {
|
||||
setSelectedItem(tab);
|
||||
}
|
||||
|
||||
// Select the new tab
|
||||
if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) {
|
||||
tabHeader.getTabs().get(getSelectedIndex()).setSelected(true);
|
||||
}
|
||||
|
||||
/* Does this get all the change events */
|
||||
((Node) tabHeader).notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
|
||||
}
|
||||
|
||||
@Override public void select(Tab tab) {
|
||||
final int itemCount = getItemCount();
|
||||
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
final Tab value = getModelItem(i);
|
||||
if (value != null && value.equals(tab)) {
|
||||
select(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (tab != null) {
|
||||
setSelectedItem(tab);
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected Tab getModelItem(int index) {
|
||||
final ObservableList<Tab> items = tabHeader.getTabs();
|
||||
if (items == null) return null;
|
||||
if (index < 0 || index >= items.size()) return null;
|
||||
return items.get(index);
|
||||
}
|
||||
|
||||
@Override protected int getItemCount() {
|
||||
final ObservableList<Tab> items = tabHeader.getTabs();
|
||||
return items == null ? 0 : items.size();
|
||||
}
|
||||
|
||||
private Tab findNearestAvailableTab(int tabIndex, boolean doSelect) {
|
||||
// we always try to select the nearest, non-disabled
|
||||
// tab from the position of the closed tab.
|
||||
final int tabCount = getItemCount();
|
||||
int i = 1;
|
||||
Tab bestTab = null;
|
||||
while (true) {
|
||||
// look leftwards
|
||||
int downPos = tabIndex - i;
|
||||
if (downPos >= 0) {
|
||||
Tab _tab = getModelItem(downPos);
|
||||
if (_tab != null) {
|
||||
bestTab = _tab;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// look rightwards. We subtract one as we need
|
||||
// to take into account that a tab has been removed
|
||||
// and if we don't do this we'll miss the tab
|
||||
// to the right of the tab (as it has moved into
|
||||
// the removed tabs position).
|
||||
int upPos = tabIndex + i - 1;
|
||||
if (upPos < tabCount) {
|
||||
Tab _tab = getModelItem(upPos);
|
||||
if (_tab != null) {
|
||||
bestTab = _tab;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (downPos < 0 && upPos >= tabCount) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (doSelect && bestTab != null) {
|
||||
select(bestTab);
|
||||
}
|
||||
|
||||
return bestTab;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Tab {
|
||||
private final StringProperty id = new SimpleStringProperty(this, "id");
|
||||
private final StringProperty text = new SimpleStringProperty(this, "text");
|
||||
private final ReadOnlyBooleanWrapper selected = new ReadOnlyBooleanWrapper(this, "selected");
|
||||
private final ObjectProperty<Node> node = new SimpleObjectProperty<>(this, "node");
|
||||
private Supplier<Node> nodeSupplier;
|
||||
|
||||
public Tab(String id) {
|
||||
setId(id);
|
||||
}
|
||||
|
||||
public Tab(String id, String text) {
|
||||
setId(id);
|
||||
setText(text);
|
||||
}
|
||||
|
||||
public Supplier<Node> getNodeSupplier() {
|
||||
return nodeSupplier;
|
||||
}
|
||||
|
||||
public void setNodeSupplier(Supplier<Node> nodeSupplier) {
|
||||
this.nodeSupplier = nodeSupplier;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id.get();
|
||||
}
|
||||
|
||||
public StringProperty idProperty() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id.set(id);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text.get();
|
||||
}
|
||||
|
||||
public StringProperty textProperty() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text.set(text);
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty selectedProperty() {
|
||||
return selected.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private void setSelected(boolean selected) {
|
||||
this.selected.set(selected);
|
||||
}
|
||||
|
||||
public Node getNode() {
|
||||
return node.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> nodeProperty() {
|
||||
return node;
|
||||
}
|
||||
|
||||
public void setNode(Node node) {
|
||||
this.node.set(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
public class TabHeader extends Control implements TabControl {
|
||||
|
||||
public TabHeader(Tab... tabs) {
|
||||
getStyleClass().setAll("tab-header");
|
||||
if (tabs != null) {
|
||||
getTabs().addAll(tabs);
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableList<Tab> tabs = FXCollections.observableArrayList();
|
||||
|
||||
@Override
|
||||
public ObservableList<Tab> getTabs() {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
private final ObjectProperty<SingleSelectionModel<Tab>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControlSelectionModel(this));
|
||||
|
||||
public SingleSelectionModel<Tab> getSelectionModel() {
|
||||
return selectionModel.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<SingleSelectionModel<Tab>> selectionModelProperty() {
|
||||
return selectionModel;
|
||||
}
|
||||
|
||||
public void setSelectionModel(SingleSelectionModel<Tab> selectionModel) {
|
||||
this.selectionModel.set(selectionModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new TabHeaderSkin(this);
|
||||
}
|
||||
|
||||
public static class TabHeaderSkin extends SkinBase<TabHeader> {
|
||||
|
||||
private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private final Color ripplerColor = Color.valueOf("#FFFF8D");
|
||||
|
||||
private final HeaderContainer header;
|
||||
private boolean isSelectingTab = false;
|
||||
private Tab selectedTab;
|
||||
|
||||
protected TabHeaderSkin(TabHeader control) {
|
||||
super(control);
|
||||
|
||||
header = new HeaderContainer();
|
||||
getChildren().setAll(header);
|
||||
|
||||
FXUtils.onChangeAndOperate(control.getSelectionModel().selectedItemProperty(), item -> {
|
||||
isSelectingTab = true;
|
||||
selectedTab = item;
|
||||
Platform.runLater(() -> {
|
||||
header.setNeedsLayout2(true);
|
||||
header.layout();
|
||||
});
|
||||
});
|
||||
|
||||
this.selectedTab = control.getSelectionModel().getSelectedItem();
|
||||
if (this.selectedTab == null && control.getSelectionModel().getSelectedIndex() != -1) {
|
||||
control.getSelectionModel().select(control.getSelectionModel().getSelectedIndex());
|
||||
this.selectedTab = control.getSelectionModel().getSelectedItem();
|
||||
}
|
||||
|
||||
if (this.selectedTab == null) {
|
||||
control.getSelectionModel().selectFirst();
|
||||
}
|
||||
|
||||
this.selectedTab = control.getSelectionModel().getSelectedItem();
|
||||
}
|
||||
|
||||
protected class HeaderContainer extends StackPane {
|
||||
private Timeline timeline;
|
||||
private StackPane selectedTabLine;
|
||||
private StackPane headersRegion;
|
||||
private Scale scale = new Scale(1, 1, 0, 0);
|
||||
private Rotate rotate = new Rotate(0, 0, 1);
|
||||
private double selectedTabLineOffset;
|
||||
private ObservableList<Node> binding;
|
||||
|
||||
public HeaderContainer() {
|
||||
getStyleClass().add("tab-header-area");
|
||||
setPickOnBounds(false);
|
||||
|
||||
headersRegion = new StackPane() {
|
||||
@Override
|
||||
protected double computePrefWidth(double height) {
|
||||
double width = 0;
|
||||
for (Node child : getChildren()) {
|
||||
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
|
||||
width += child.prefWidth(height);
|
||||
}
|
||||
return snapSize(width) + snappedLeftInset() + snappedRightInset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width) {
|
||||
double height = 0;
|
||||
for (Node child : getChildren()) {
|
||||
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
|
||||
height = Math.max(height, child.prefHeight(width));
|
||||
}
|
||||
return snapSize(height) + snappedTopInset() + snappedBottomInset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
if (isSelectingTab) {
|
||||
animateSelectionLine();
|
||||
isSelectingTab = false;
|
||||
}
|
||||
|
||||
double headerHeight = snapSize(prefHeight(-1));
|
||||
double tabStartX = 0;
|
||||
for (Node node : getChildren()) {
|
||||
if (!(node instanceof TabHeaderContainer)) continue;
|
||||
TabHeaderContainer child = (TabHeaderContainer) node;
|
||||
double w = snapSize(child.prefWidth(-1));
|
||||
double h = snapSize(child.prefHeight(-1));
|
||||
child.resize(w, h);
|
||||
|
||||
child.relocate(tabStartX, headerHeight - h - snappedBottomInset());
|
||||
tabStartX += w;
|
||||
}
|
||||
|
||||
selectedTabLine.resizeRelocate(0,
|
||||
headerHeight - selectedTabLine.prefHeight(-1),
|
||||
snapSize(selectedTabLine.prefWidth(-1)),
|
||||
snapSize(selectedTabLine.prefHeight(-1)));
|
||||
}
|
||||
};
|
||||
|
||||
selectedTabLine = new StackPane();
|
||||
selectedTabLine.setManaged(false);
|
||||
selectedTabLine.getTransforms().addAll(scale, rotate);
|
||||
selectedTabLine.setCache(true);
|
||||
selectedTabLine.getStyleClass().addAll("tab-selected-line");
|
||||
selectedTabLine.setPrefHeight(2);
|
||||
selectedTabLine.setPrefWidth(1);
|
||||
selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
getChildren().setAll(headersRegion, selectedTabLine);
|
||||
headersRegion.setPickOnBounds(false);
|
||||
headersRegion.prefHeightProperty().bind(heightProperty());
|
||||
prefWidthProperty().bind(headersRegion.widthProperty());
|
||||
|
||||
Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> {
|
||||
TabHeaderContainer container = new TabHeaderContainer(tab);
|
||||
container.setVisible(true);
|
||||
return container;
|
||||
}));
|
||||
}
|
||||
|
||||
public void setNeedsLayout2(boolean value) {
|
||||
setNeedsLayout(value);
|
||||
}
|
||||
|
||||
private void runTimeline(double newTransX, double newWidth) {
|
||||
double tempScaleX = 0.0D;
|
||||
double tempWidth = 0.0D;
|
||||
double lineWidth = this.selectedTabLine.prefWidth(-1.0D);
|
||||
if (this.isAnimating()) {
|
||||
this.timeline.stop();
|
||||
tempScaleX = this.scale.getX();
|
||||
if (this.rotate.getAngle() != 0.0D) {
|
||||
this.rotate.setAngle(0.0D);
|
||||
tempWidth = tempScaleX * lineWidth;
|
||||
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - tempWidth);
|
||||
}
|
||||
}
|
||||
|
||||
double oldScaleX = this.scale.getX();
|
||||
double oldWidth = lineWidth * oldScaleX;
|
||||
double oldTransX = this.selectedTabLine.getTranslateX();
|
||||
double newScaleX = newWidth * oldScaleX / oldWidth;
|
||||
this.selectedTabLineOffset = newTransX;
|
||||
// newTransX += offsetStart * (double)this.direction;
|
||||
double transDiff = newTransX - oldTransX;
|
||||
double midScaleX = tempScaleX != 0.0D ? tempScaleX : (Math.abs(transDiff) / 1.3D + oldWidth) * oldScaleX / oldWidth;
|
||||
if (transDiff < 0.0D) {
|
||||
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() + oldWidth);
|
||||
newTransX += newWidth;
|
||||
this.rotate.setAngle(180.0D);
|
||||
}
|
||||
|
||||
this.timeline = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.12D), new KeyValue(this.scale.xProperty(), midScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.24D), new KeyValue(this.scale.xProperty(), newScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH)));
|
||||
this.timeline.setOnFinished((finish) -> {
|
||||
if (this.rotate.getAngle() != 0.0D) {
|
||||
this.rotate.setAngle(0.0D);
|
||||
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - newWidth);
|
||||
}
|
||||
|
||||
});
|
||||
this.timeline.play();
|
||||
}
|
||||
|
||||
private boolean isAnimating() {
|
||||
return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
|
||||
if (isSelectingTab) {
|
||||
animateSelectionLine();
|
||||
isSelectingTab = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void animateSelectionLine() {
|
||||
double offset = 0.0D;
|
||||
double selectedTabOffset = 0.0D;
|
||||
double selectedTabWidth = 0.0D;
|
||||
Side side = Side.TOP;
|
||||
|
||||
for (Node node : headersRegion.getChildren()) {
|
||||
if (node instanceof TabHeaderContainer) {
|
||||
TabHeaderContainer tabHeader = (TabHeaderContainer)node;
|
||||
double tabHeaderPrefWidth = this.snapSize(tabHeader.prefWidth(-1.0D));
|
||||
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
|
||||
selectedTabOffset = side != Side.LEFT && side != Side.BOTTOM ? offset : -offset - tabHeaderPrefWidth;
|
||||
selectedTabWidth = tabHeaderPrefWidth;
|
||||
break;
|
||||
}
|
||||
|
||||
offset += tabHeaderPrefWidth;
|
||||
}
|
||||
}
|
||||
|
||||
this.runTimeline(selectedTabOffset, selectedTabWidth);
|
||||
}
|
||||
}
|
||||
|
||||
protected class TabHeaderContainer extends StackPane {
|
||||
|
||||
private final Tab tab;
|
||||
private final Label tabText;
|
||||
private final BorderPane inner;
|
||||
private final JFXRippler rippler;
|
||||
|
||||
public TabHeaderContainer(Tab tab) {
|
||||
this.tab = tab;
|
||||
|
||||
tabText = new Label();
|
||||
tabText.textProperty().bind(tab.textProperty());
|
||||
tabText.getStyleClass().add("tab-label");
|
||||
inner = new BorderPane();
|
||||
inner.setCenter(tabText);
|
||||
inner.getStyleClass().add("tab-container");
|
||||
rippler = new JFXRippler(inner, JFXRippler.RipplerPos.FRONT);
|
||||
rippler.setRipplerFill(ripplerColor);
|
||||
getChildren().setAll(rippler);
|
||||
|
||||
FXUtils.onChangeAndOperate(tab.selectedProperty(), selected -> inner.pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, selected));
|
||||
|
||||
this.setOnMouseClicked(event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
this.setOpacity(1);
|
||||
getSkinnable().getSelectionModel().select(tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -18,14 +18,13 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -38,14 +37,11 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
public class TaskExecutorDialogPane extends StackPane {
|
||||
private TaskExecutor executor;
|
||||
private Consumer<Region> onCancel;
|
||||
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
|
||||
|
||||
@FXML
|
||||
private JFXProgressBar progressBar;
|
||||
@FXML
|
||||
private Label lblTitle;
|
||||
@FXML
|
||||
private Label lblSubtitle;
|
||||
@FXML
|
||||
private Label lblProgress;
|
||||
@FXML
|
||||
private JFXButton btnCancel;
|
||||
@@ -62,10 +58,24 @@ public class TaskExecutorDialogPane extends StackPane {
|
||||
onCancel.accept(this);
|
||||
});
|
||||
|
||||
lblProgress.textProperty().bind(Bindings.createStringBinding(
|
||||
() -> taskListPane.finishedTasksProperty().get() + "/" + taskListPane.totTasksProperty().get(),
|
||||
taskListPane.finishedTasksProperty(), taskListPane.totTasksProperty()
|
||||
));
|
||||
speedEventHandler = speedEvent -> {
|
||||
String unit = "B/s";
|
||||
double speed = speedEvent.getSpeed();
|
||||
if (speed > 1024) {
|
||||
speed /= 1024;
|
||||
unit = "KB/s";
|
||||
}
|
||||
if (speed > 1024) {
|
||||
speed /= 1024;
|
||||
unit = "MB/s";
|
||||
}
|
||||
double finalSpeed = speed;
|
||||
String finalUnit = unit;
|
||||
Platform.runLater(() -> {
|
||||
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
|
||||
});
|
||||
};
|
||||
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
}
|
||||
|
||||
public void setExecutor(TaskExecutor executor) {
|
||||
@@ -100,25 +110,6 @@ public class TaskExecutorDialogPane extends StackPane {
|
||||
lblTitle.setText(currentState);
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return lblSubtitle.textProperty();
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return lblSubtitle.getText();
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
lblSubtitle.setText(subtitle);
|
||||
}
|
||||
|
||||
public void setProgress(double progress) {
|
||||
if (progress == Double.MAX_VALUE)
|
||||
progressBar.setVisible(false);
|
||||
else
|
||||
progressBar.setProgress(progress);
|
||||
}
|
||||
|
||||
public void setCancel(Consumer<Region> onCancel) {
|
||||
this.onCancel = onCancel;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -19,38 +19,55 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameInstallTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.mod.ModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class TaskListPane extends StackPane {
|
||||
private TaskExecutor executor;
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
|
||||
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
|
||||
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
|
||||
private final List<StageNode> stageNodes = new ArrayList<>();
|
||||
private final ObjectProperty<Insets> progressNodePadding = new SimpleObjectProperty<>(Insets.EMPTY);
|
||||
|
||||
public TaskListPane() {
|
||||
listBox.setSpacing(0);
|
||||
@@ -58,33 +75,35 @@ public final class TaskListPane extends StackPane {
|
||||
getChildren().setAll(listBox);
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty finishedTasksProperty() {
|
||||
return finishedTasks.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty totTasksProperty() {
|
||||
return totTasks.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public void setExecutor(TaskExecutor executor) {
|
||||
List<String> stages = Lang.removingDuplicates(executor.getStages());
|
||||
this.executor = executor;
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void onStart() {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.clear();
|
||||
listBox.clear();
|
||||
finishedTasks.set(0);
|
||||
totTasks.set(0);
|
||||
stageNodes.addAll(stages.stream().map(StageNode::new).collect(Collectors.toList()));
|
||||
stageNodes.forEach(listBox::add);
|
||||
|
||||
if (stages.isEmpty()) progressNodePadding.setValue(new Insets(0, 0, 8, 0));
|
||||
else progressNodePadding.setValue(new Insets(0, 0, 8, 26));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReady(Task<?> task) {
|
||||
Platform.runLater(() -> totTasks.set(totTasks.getValue() + 1));
|
||||
if (task instanceof Task.StageTask) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::begin);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRunning(Task<?> task) {
|
||||
if (!task.getSignificance().shouldShow())
|
||||
if (!task.getSignificance().shouldShow() || task.getName() == null)
|
||||
return;
|
||||
|
||||
if (task instanceof GameAssetDownloadTask) {
|
||||
@@ -97,6 +116,8 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.liteloader")));
|
||||
} else if (task instanceof OptiFineInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.optifine")));
|
||||
} else if (task instanceof FabricInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.fabric")));
|
||||
} else if (task instanceof CurseCompletionTask) {
|
||||
task.setName(i18n("modpack.type.curse.completion"));
|
||||
} else if (task instanceof ModpackInstallTask) {
|
||||
@@ -109,46 +130,128 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
|
||||
} else if (task instanceof HMCLModpackInstallTask) {
|
||||
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
|
||||
} else if (task instanceof HMCLModpackExportTask) {
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
|
||||
task.setName(i18n("modpack.export"));
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
}
|
||||
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
Platform.runLater(() -> listBox.add(node));
|
||||
Platform.runLater(() -> {
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().orElse(null);
|
||||
listBox.add(listBox.indexOf(stageNode) + 1, node);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Task<?> task) {
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
node.unbind();
|
||||
if (task instanceof Task.StageTask) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::succeed);
|
||||
});
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
node.unbind();
|
||||
listBox.remove(node);
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailed(Task<?> task, Throwable throwable) {
|
||||
if (task instanceof Task.StageTask) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::fail);
|
||||
});
|
||||
}
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
Platform.runLater(() -> {
|
||||
node.setThrowable(throwable);
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPropertiesUpdate(Map<String, Map<String, Object>> stageProperties) {
|
||||
stageProperties.forEach((stage, properties) -> {
|
||||
int count = tryCast(properties.get("count"), Integer.class).orElse(0),
|
||||
total = tryCast(properties.get("total"), Integer.class).orElse(0);
|
||||
if (total > 0)
|
||||
Platform.runLater(() ->
|
||||
stageNodes.stream().filter(x -> x.stage.equals(stage)).findAny().ifPresent(stageNode -> stageNode.updateCounter(count, total)));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class ProgressListNode extends BorderPane {
|
||||
private static class StageNode extends BorderPane {
|
||||
private final String stage;
|
||||
private final Label title = new Label();
|
||||
private final String message;
|
||||
private boolean started = false;
|
||||
|
||||
public StageNode(String stage) {
|
||||
this.stage = stage;
|
||||
|
||||
String stageKey = StringUtils.substringBefore(stage, ':');
|
||||
String stageValue = StringUtils.substringAfter(stage, ':');
|
||||
|
||||
// @formatter:off
|
||||
switch (stageKey) {
|
||||
case "hmcl.modpack": message = i18n("install.modpack"); break;
|
||||
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
|
||||
case "hmcl.install.assets": message = i18n("assets.download"); break;
|
||||
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
|
||||
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
|
||||
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
|
||||
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
|
||||
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
|
||||
default: message = i18n(stageKey); break;
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
title.setText(message);
|
||||
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
|
||||
setPadding(new Insets(0, 0, 8, 4));
|
||||
setCenter(title);
|
||||
setLeft(FXUtils.limitingSize(SVG.dotsHorizontal(Theme.blackFillBinding(), 14, 14), 14, 14));
|
||||
}
|
||||
|
||||
public void begin() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
setLeft(FXUtils.limitingSize(SVG.arrowRight(Theme.blackFillBinding(), 14, 14), 14, 14));
|
||||
}
|
||||
|
||||
public void fail() {
|
||||
setLeft(FXUtils.limitingSize(SVG.close(Theme.blackFillBinding(), 14, 14), 14, 14));
|
||||
}
|
||||
|
||||
public void succeed() {
|
||||
setLeft(FXUtils.limitingSize(SVG.check(Theme.blackFillBinding(), 14, 14), 14, 14));
|
||||
}
|
||||
|
||||
public void updateCounter(int count, int total) {
|
||||
if (total > 0)
|
||||
title.setText(String.format("%s - %d/%d", message, count, total));
|
||||
else
|
||||
title.setText(message);
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgressListNode extends BorderPane {
|
||||
private final JFXProgressBar bar = new JFXProgressBar();
|
||||
private final Label title = new Label();
|
||||
private final Label state = new Label();
|
||||
private final DoubleBinding binding = Bindings.createDoubleBinding(() ->
|
||||
getWidth() - getPadding().getLeft() - getPadding().getRight(),
|
||||
paddingProperty(), widthProperty());
|
||||
|
||||
public ProgressListNode(Task<?> task) {
|
||||
bar.progressProperty().bind(task.progressProperty());
|
||||
@@ -159,9 +262,11 @@ public final class TaskListPane extends StackPane {
|
||||
setRight(state);
|
||||
setBottom(bar);
|
||||
|
||||
bar.minWidthProperty().bind(widthProperty());
|
||||
bar.prefWidthProperty().bind(widthProperty());
|
||||
bar.maxWidthProperty().bind(widthProperty());
|
||||
bar.minWidthProperty().bind(binding);
|
||||
bar.prefWidthProperty().bind(binding);
|
||||
bar.maxWidthProperty().bind(binding);
|
||||
|
||||
paddingProperty().bind(progressNodePadding);
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
|
||||
Loaded 100 of 511 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user