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 | ||
|
|
191b88ab2a |
No files matched your search
@@ -0,0 +1 @@
|
||||
*.exe filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -17,17 +17,20 @@ 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
|
||||
|
||||
+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
|
||||
@@ -23,38 +23,45 @@ import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.CrashReporter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.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()));
|
||||
}
|
||||
|
||||
@@ -65,6 +72,8 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
initIcon();
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
@@ -74,7 +83,23 @@ 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);
|
||||
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
||||
|
||||
@@ -106,7 +131,6 @@ public final class Launcher extends Application {
|
||||
Schedulers.shutdown();
|
||||
Controllers.shutdown();
|
||||
Platform.exit();
|
||||
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,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;
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
*/
|
||||
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.*;
|
||||
import javax.swing.*;
|
||||
@@ -48,7 +48,6 @@ 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
|
||||
@@ -59,9 +58,10 @@ public final class Main {
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -77,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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +132,10 @@ public final class Main {
|
||||
try {
|
||||
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
|
||||
defaultKeyStore.load(Files.newInputStream(ksPath), "changeit".toCharArray());
|
||||
|
||||
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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,24 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.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;
|
||||
@@ -32,6 +44,7 @@ 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 {
|
||||
@@ -62,10 +75,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
@Override
|
||||
public File getRunDirectory(String id) {
|
||||
switch (getGameDirectoryType(id)) {
|
||||
case VERSION_FOLDER: return getVersionRoot(id);
|
||||
case ROOT_FOLDER: return super.getRunDirectory(id);
|
||||
case CUSTOM: return new File(getVersionSetting(id).getGameDir());
|
||||
default: throw new Error();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +93,11 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
localVersionSettings.clear();
|
||||
super.refreshVersionsImpl();
|
||||
versions.keySet().forEach(this::loadLocalVersionSetting);
|
||||
versions.keySet().forEach(version -> {
|
||||
if (isModpack(version)) {
|
||||
specializeVersionSetting(version);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
File file = new File(getBaseDirectory(), "launcher_profiles.json");
|
||||
@@ -212,9 +236,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
if (iconFile.exists())
|
||||
return new Image("file:" + iconFile.getAbsolutePath());
|
||||
else if (version.getMainClass() != null &&
|
||||
("net.minecraft.launchwrapper.Launch".equals(version.getMainClass())
|
||||
(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");
|
||||
@@ -261,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");
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import 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.io.IOException;
|
||||
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 -> Modpack.acceptFile(path, blackList, whitelist));
|
||||
|
||||
Version mv = repository.getResolvedPreservingPatchesVersion(version);
|
||||
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
|
||||
.orElseThrow(() -> new IOException("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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,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
|
||||
@@ -103,8 +104,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
libraryTask = libraryTask.thenComposeAsync(version -> dependency.installLibraryAsync(modpack.getGameVersion(), version, mark.getLibraryId(), mark.getLibraryVersion()));
|
||||
}
|
||||
|
||||
dependencies.add(libraryTask.thenComposeAsync(repository::save));
|
||||
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
|
||||
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,9 +23,12 @@ import org.jackhuang.hmcl.Launcher;
|
||||
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;
|
||||
@@ -33,14 +36,15 @@ 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.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;
|
||||
@@ -49,20 +53,21 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
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.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
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.RejectedExecutionException;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -91,7 +96,6 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
|
||||
private CountDownLatch launchingLatch;
|
||||
|
||||
public void setTestMode() {
|
||||
launcherVisibility = LauncherVisibility.KEEP;
|
||||
@@ -108,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) {
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -126,26 +130,34 @@ 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.allOf(
|
||||
Task.composeAsync(() -> {
|
||||
if (setting.isNotCheckGame())
|
||||
return null;
|
||||
else
|
||||
return dependencyManager.checkGameCompletionAsync(version, repository.unmarkVersionLaunchedAbnormally(selectedVersion));
|
||||
}), Task.composeAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
|
||||
if ("Curse".equals(configuration.getType()))
|
||||
return new CurseCompletionTask(dependencyManager, selectedVersion);
|
||||
else if ("Server".equals(configuration.getType()))
|
||||
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
|
||||
else
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
})).withStage("launch.state.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(() -> {
|
||||
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version)).orElse(null);
|
||||
})
|
||||
.thenComposeAsync(Task.supplyAsync(() -> {
|
||||
try {
|
||||
return account.logIn();
|
||||
@@ -162,10 +174,10 @@ public final class LauncherHelper {
|
||||
repository,
|
||||
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
|
||||
authInfo,
|
||||
setting.toLaunchOptions(profile.getGameDir(), !setting.isNotCheckJVM()),
|
||||
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, gameVersion.isPresent())
|
||||
: new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent())
|
||||
);
|
||||
}).thenComposeAsync(launcher -> { // launcher is prev task's result
|
||||
if (scriptFile == null) {
|
||||
@@ -192,15 +204,14 @@ public final class LauncherHelper {
|
||||
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
|
||||
});
|
||||
}
|
||||
}).thenRunAsync(Schedulers.defaultScheduler(), () -> {
|
||||
launchingLatch = new CountDownLatch(1);
|
||||
}).thenRunAsync(() -> {
|
||||
launchingLatch.await();
|
||||
}).withStage("launch.state.waiting_launching"))
|
||||
.withStagesHint(Lang.immutableListOf(
|
||||
"launch.state.dependencies",
|
||||
"launch.state.logging_in",
|
||||
"launch.state.waiting_launching"))
|
||||
.cancellableExecutor();
|
||||
.executor();
|
||||
launchingStepsPane.setExecutor(executor, false);
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
|
||||
@@ -239,6 +250,20 @@ public final class LauncherHelper {
|
||||
} 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) {
|
||||
@@ -292,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()
|
||||
@@ -314,8 +355,8 @@ 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_AND_LATER
|
||||
&& version.getMainClass().contains("launchwrapper")
|
||||
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
|
||||
&& 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)) {
|
||||
@@ -347,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.
|
||||
@@ -383,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;
|
||||
}
|
||||
|
||||
@@ -401,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;
|
||||
}
|
||||
}
|
||||
@@ -410,6 +463,36 @@ public final class LauncherHelper {
|
||||
onAccept.run();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
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() {
|
||||
switch (launcherVisibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
@@ -449,11 +532,13 @@ public final class LauncherHelper {
|
||||
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(HMCLGameRepository repository, String version, 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)
|
||||
@@ -482,7 +567,7 @@ public final class LauncherHelper {
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
logWindow.show();
|
||||
latch.countDown();
|
||||
logWindowLatch.countDown();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -502,9 +587,7 @@ public final class LauncherHelper {
|
||||
// Never come to here.
|
||||
break;
|
||||
case KEEP:
|
||||
Platform.runLater(() -> {
|
||||
launchingLatch.countDown();
|
||||
});
|
||||
Platform.runLater(launchingLatch::countDown);
|
||||
break;
|
||||
case HIDE:
|
||||
launchingLatch.countDown();
|
||||
@@ -539,7 +622,7 @@ public final class LauncherHelper {
|
||||
|
||||
if (showLogs) {
|
||||
try {
|
||||
latch.await();
|
||||
logWindowLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
@@ -548,7 +631,7 @@ public final class LauncherHelper {
|
||||
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
|
||||
}
|
||||
|
||||
if (!lwjgl && (filteredLog.toLowerCase().contains("lwjgl version") || !detectWindow)) {
|
||||
if (!lwjgl && (filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal") || !detectWindow)) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ 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.mod.server.ServerModpackLocalInstallTask;
|
||||
@@ -43,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) {
|
||||
@@ -148,15 +158,28 @@ 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));
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
else if (modpack.getManifest() instanceof ServerModpackManifest)
|
||||
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, ((ServerModpackManifest) modpack.getManifest()), name)
|
||||
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 {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
|
||||
|
||||
@@ -176,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();
|
||||
}
|
||||
@@ -218,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) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -134,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);
|
||||
@@ -142,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,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 ====
|
||||
@@ -205,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;
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ public final class ConfigHolder {
|
||||
}
|
||||
|
||||
configLocation = locateConfig();
|
||||
|
||||
LOG.log(Level.INFO, "Config location: " + configLocation);
|
||||
|
||||
configInstance = loadConfig();
|
||||
configInstance.addListener(source -> markConfigDirty());
|
||||
|
||||
@@ -88,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;
|
||||
@@ -97,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 {
|
||||
|
||||
@@ -17,19 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
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.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -40,39 +35,61 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
public final class DownloadProviders {
|
||||
private DownloadProviders() {}
|
||||
|
||||
public static final Map<String, DownloadProvider> providersById = mapOf(
|
||||
pair("mojang", new MojangDownloadProvider()),
|
||||
pair("bmclapi", new BMCLAPIDownloadProvider("https://bmclapi2.bangbang93.com")),
|
||||
pair("mcbbs", new BMCLAPIDownloadProvider("https://download.mcbbs.net")));
|
||||
private static final AdaptedDownloadProvider DOWNLOAD_PROVIDER = new AdaptedDownloadProvider();
|
||||
|
||||
public static final Map<String, DownloadProvider> providersById;
|
||||
|
||||
public static final String DEFAULT_PROVIDER_ID = "mcbbs";
|
||||
|
||||
private static ObjectBinding<DownloadProvider> downloadProviderProperty;
|
||||
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 String getPrimaryDownloadProviderId() {
|
||||
String downloadType = config().getDownloadType();
|
||||
if (providersById.containsKey(downloadType))
|
||||
return downloadType;
|
||||
else
|
||||
return DEFAULT_PROVIDER_ID;
|
||||
}
|
||||
|
||||
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 DownloadProvider getDownloadProvider() {
|
||||
return downloadProviderProperty.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preferred download providers have the primary one first, the official one next.
|
||||
* @return the preferred download providers
|
||||
*/
|
||||
public static List<DownloadProvider> getPreferredDownloadProviders() {
|
||||
DownloadProvider provider = getDownloadProvider();
|
||||
return Stream.concat(Stream.of(provider), providersById.values().stream().filter(x -> x != provider)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static ObservableObjectValue<DownloadProvider> downloadProviderProperty() {
|
||||
return downloadProviderProperty;
|
||||
public static AdaptedDownloadProvider getDownloadProvider() {
|
||||
return DOWNLOAD_PROVIDER;
|
||||
}
|
||||
}
|
||||
@@ -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,7 +163,11 @@ public final class Profile implements Observable {
|
||||
}
|
||||
|
||||
public DefaultDependencyManager getDependency() {
|
||||
return new DefaultDependencyManager(repository, DownloadProviders.getDownloadProvider(), DownloadProviders.getPreferredDownloadProviders(), HMCLCacheRepository.REPOSITORY);
|
||||
return getDependency(DownloadProviders.getDownloadProvider());
|
||||
}
|
||||
|
||||
public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) {
|
||||
return new DefaultDependencyManager(repository, downloadProvider, HMCLCacheRepository.REPOSITORY);
|
||||
}
|
||||
|
||||
public VersionSetting getVersionSetting(String id) {
|
||||
|
||||
@@ -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,6 +46,9 @@ public class Settings {
|
||||
}
|
||||
|
||||
private Settings() {
|
||||
config().localizationProperty().addListener(unused -> updateSystemLocale());
|
||||
updateSystemLocale();
|
||||
|
||||
DownloadProviders.init();
|
||||
ProxyManager.init();
|
||||
Accounts.init();
|
||||
@@ -74,4 +79,8 @@ public class Settings {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateSystemLocale() {
|
||||
Locale.setDefault(config().getLocalization().getLocale());
|
||||
}
|
||||
}
|
||||
@@ -87,9 +87,10 @@ public class Theme {
|
||||
File temp = File.createTempFile("hmcl", ".css");
|
||||
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
|
||||
.replace("%base-color%", color)
|
||||
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int)Math.ceil(paint.getRed() * 256), (int)Math.ceil(paint.getGreen() * 256), (int)Math.ceil(paint.getBlue() * 256)))
|
||||
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int)Math.ceil(textFill.getRed() * 256), (int)Math.ceil(textFill.getGreen() * 256), (int)Math.ceil(textFill.getBlue() * 256)))
|
||||
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
|
||||
.replace("%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);
|
||||
|
||||
@@ -21,16 +21,13 @@ import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
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;
|
||||
@@ -39,8 +36,6 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@@ -533,37 +528,6 @@ public final class VersionSetting implements Cloneable {
|
||||
defaultJavaPathProperty.addListener(listener);
|
||||
}
|
||||
|
||||
public LaunchOptions toLaunchOptions(File gameDir, boolean checkJava) throws InterruptedException {
|
||||
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion(checkJava)).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();
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,22 @@
|
||||
*/
|
||||
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.download.java.JavaRepository;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
@@ -50,6 +56,8 @@ import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Controllers {
|
||||
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
|
||||
|
||||
private static Scene scene;
|
||||
private static Stage stage;
|
||||
@@ -92,11 +100,21 @@ public final class Controllers {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
@@ -108,15 +126,19 @@ 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, 480);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.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;
|
||||
import javafx.beans.property.Property;
|
||||
@@ -26,7 +29,6 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -34,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;
|
||||
@@ -43,7 +43,6 @@ import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -61,12 +60,9 @@ import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
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;
|
||||
|
||||
@@ -443,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(Lang.clamp(0, bar.getValue() + dy / height, 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.
|
||||
|
||||
@@ -19,95 +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.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class InstallerItem extends BorderPane {
|
||||
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 libraryId, String libraryVersion, @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());
|
||||
}
|
||||
|
||||
String[] urls = new String[]{"/assets/img/grass.png", "/assets/img/fabric.png", "/assets/img/forge.png", "/assets/img/chicken.png", "/assets/img/command.png"};
|
||||
String[] libraryIds = new String[]{"game", "fabric", "forge", "liteloader", "optifine"};
|
||||
public InstallerItem(String id) {
|
||||
this.id = id;
|
||||
|
||||
boolean regularLibrary = false;
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
if (libraryIds[i].equals(libraryId)) {
|
||||
setLeft(FXUtils.limitingSize(new ImageView(new Image(urls[i], 32, 32, true, true)), 32, 32));
|
||||
Label label = new Label();
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
|
||||
if (libraryVersion == null) {
|
||||
label.setText(i18n("install.installer.not_installed", i18n("install.installer." + libraryId)));
|
||||
} else {
|
||||
label.setText(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + libraryVersion);
|
||||
}
|
||||
setCenter(label);
|
||||
regularLibrary = true;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (!regularLibrary) {
|
||||
String title = I18n.hasKey("install.installer." + libraryId) ? i18n("install.installer." + libraryId) : libraryId;
|
||||
if (libraryVersion != null) {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.setTitle(title);
|
||||
item.setSubtitle(i18n("archive.version") + ": " + libraryVersion);
|
||||
setCenter(item);
|
||||
} else {
|
||||
Label label = new Label();
|
||||
label.setStyle("-fx-font-size: 15px;");
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
setCenter(label);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
HBox hBox = new HBox();
|
||||
|
||||
if (upgrade != null) {
|
||||
JFXButton upgradeButton = new JFXButton();
|
||||
if (libraryVersion == null) {
|
||||
upgradeButton.setGraphic(SVG.arrowRight(Theme.blackFillBinding(), -1, -1));
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||
Pane placeholder = new Pane();
|
||||
VBox list = new VBox();
|
||||
spinnerPane.getStyleClass().add("content-background");
|
||||
|
||||
StackPane contentPane = new StackPane();
|
||||
{
|
||||
|
||||
@@ -150,6 +150,8 @@ public final class LogWindow extends Stage {
|
||||
private JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
boolean flag = false;
|
||||
cboLines.getItems().setAll("500", "2000", "5000");
|
||||
for (String i : cboLines.getItems())
|
||||
@@ -178,6 +180,7 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
private void onClear() {
|
||||
impl.listView.getItems().clear();
|
||||
logs.clear();
|
||||
}
|
||||
|
||||
@@ -195,7 +198,7 @@ public final class LogWindow extends Stage {
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException ignored) {
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -279,7 +282,6 @@ public final class LogWindow extends Stage {
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
}
|
||||
setPadding(new Insets(2));
|
||||
getStyleClass().add("log");
|
||||
setWrapText(true);
|
||||
setGraphic(null);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -186,4 +186,12 @@ public final class SVG {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -93,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);
|
||||
}
|
||||
@@ -22,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;
|
||||
@@ -29,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")));
|
||||
@@ -38,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) -> {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
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;
|
||||
@@ -54,7 +54,7 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
|
||||
setSubtitle(accountSubtitle(account));
|
||||
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
tooltip.setText(account.getCharacter() + " " + accountSubtitle(account));
|
||||
tooltip.setText(account.getCharacter() + " " + accountTooltip(account));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -83,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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.account;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
@@ -32,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;
|
||||
@@ -85,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());
|
||||
@@ -102,7 +132,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
root.setRight(right);
|
||||
|
||||
root.getStyleClass().add("card");
|
||||
root.setPadding(new Insets(8, 8, 8, 0));
|
||||
root.setStyle("-fx-padding: 8 8 8 0;");
|
||||
JFXDepthManager.setDepth(root, 1);
|
||||
|
||||
getChildren().setAll(root);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -36,8 +36,7 @@ public abstract class FloatListCell<T> extends ListCell<T> {
|
||||
|
||||
pane.getStyleClass().add("card");
|
||||
pane.setCursor(Cursor.HAND);
|
||||
pane.setPadding(new Insets(8));
|
||||
setPadding(new Insets(5));
|
||||
setPadding(new Insets(9, 9, 0, 9));
|
||||
JFXDepthManager.setDepth(pane, 1);
|
||||
|
||||
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
|
||||
|
||||
@@ -121,10 +121,6 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
return getSkinnable().getOrientation() == Orientation.VERTICAL ? thumb.getHeight() : thumb.getWidth();
|
||||
}
|
||||
|
||||
private double boundedSize(double min, double value, double max) {
|
||||
return Math.min(Math.max(value, min), Math.max(min, max));
|
||||
}
|
||||
|
||||
private void setup() {
|
||||
track.widthProperty().unbind();
|
||||
track.heightProperty().unbind();
|
||||
@@ -146,13 +142,13 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
thumb.relocate(0, -5);
|
||||
thumb.widthProperty().bind(Bindings.max(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
|
||||
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(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
|
||||
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
|
||||
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ public final class MessageDialogPane extends StackPane {
|
||||
WARNING,
|
||||
QUESTION,
|
||||
FINE,
|
||||
PLAIN,
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -148,6 +148,7 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ 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;
|
||||
@@ -81,6 +82,18 @@ public class PromptDialogPane extends StackPane {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +159,19 @@ public class PromptDialogPane extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -34,14 +34,16 @@ 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;
|
||||
@@ -128,7 +130,7 @@ 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"));
|
||||
|
||||
@@ -17,15 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final StringProperty tag = new SimpleStringProperty(this, "tag");
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
public TwoLineListItem(String titleString, String subtitleString) {
|
||||
@@ -37,15 +41,28 @@ public class TwoLineListItem extends VBox {
|
||||
|
||||
public TwoLineListItem() {
|
||||
setMouseTransparent(true);
|
||||
|
||||
HBox firstLine = new HBox();
|
||||
|
||||
Label lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.textProperty().bind(title);
|
||||
|
||||
Label lblTag = new Label();
|
||||
lblTag.getStyleClass().add("tag");
|
||||
lblTag.textProperty().bind(tag);
|
||||
|
||||
lblTag.visibleProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> StringUtils.isNotBlank(tag.getValue()),
|
||||
tag));
|
||||
|
||||
firstLine.getChildren().addAll(lblTitle, lblTag);
|
||||
|
||||
Label lblSubtitle = new Label();
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
lblSubtitle.textProperty().bind(subtitle);
|
||||
|
||||
getChildren().setAll(lblTitle, lblSubtitle);
|
||||
getChildren().setAll(firstLine, lblSubtitle);
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
@@ -73,6 +90,18 @@ public class TwoLineListItem extends VBox {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public String getTag() {
|
||||
return tag.get();
|
||||
}
|
||||
|
||||
public StringProperty tagProperty() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public void setTag(String tag) {
|
||||
this.tag.set(tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getTitle();
|
||||
|
||||
@@ -22,11 +22,16 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
|
||||
@@ -48,9 +53,14 @@ public class Decorator extends Control {
|
||||
private final Stage primaryStage;
|
||||
private StackPane drawerWrapper;
|
||||
|
||||
private final ReadOnlyBooleanWrapper allowMove = new ReadOnlyBooleanWrapper();
|
||||
private final ReadOnlyBooleanWrapper dragging = new ReadOnlyBooleanWrapper();
|
||||
|
||||
public Decorator(Stage primaryStage) {
|
||||
this.primaryStage = primaryStage;
|
||||
|
||||
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
|
||||
primaryStage.initStyle(StageStyle.UNDECORATED);
|
||||
}
|
||||
|
||||
@@ -62,7 +72,7 @@ public class Decorator extends Control {
|
||||
return drawerWrapper;
|
||||
}
|
||||
|
||||
void setDrawerWrapper(StackPane drawerWrapper) {
|
||||
public void setDrawerWrapper(StackPane drawerWrapper) {
|
||||
this.drawerWrapper = drawerWrapper;
|
||||
}
|
||||
|
||||
@@ -166,6 +176,30 @@ public class Decorator extends Control {
|
||||
return showCloseAsHome;
|
||||
}
|
||||
|
||||
public boolean isAllowMove() {
|
||||
return allowMove.get();
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty allowMoveProperty() {
|
||||
return allowMove.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
void setAllowMove(boolean allowMove) {
|
||||
this.allowMove.set(allowMove);
|
||||
}
|
||||
|
||||
public boolean isDragging() {
|
||||
return dragging.get();
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty draggingProperty() {
|
||||
return dragging.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
void setDragging(boolean dragging) {
|
||||
this.dragging.set(dragging);
|
||||
}
|
||||
|
||||
public ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonActionProperty() {
|
||||
return onBackNavButtonAction;
|
||||
}
|
||||
@@ -190,4 +224,18 @@ public class Decorator extends Control {
|
||||
public void close() {
|
||||
onCloseButtonAction.get().run();
|
||||
}
|
||||
|
||||
public void capableDraggingWindow(Node node) {
|
||||
node.addEventHandler(MouseEvent.MOUSE_MOVED, e -> allowMove.set(true));
|
||||
node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
|
||||
if (!isDragging()) allowMove.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
public void forbidDraggingWindow(Node node) {
|
||||
node.addEventHandler(MouseEvent.MOUSE_MOVED, e -> {
|
||||
allowMove.set(false);
|
||||
e.consume();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
@@ -80,7 +81,6 @@ public class DecoratorController {
|
||||
decorator.setOnCloseButtonAction(Launcher::stopApplication);
|
||||
|
||||
navigator = new Navigator();
|
||||
navigator.setOnNavigating(this::onNavigating);
|
||||
navigator.setOnNavigated(this::onNavigated);
|
||||
navigator.init(mainPage);
|
||||
|
||||
@@ -133,7 +133,7 @@ public class DecoratorController {
|
||||
// ==== Background ====
|
||||
|
||||
private void setupBackground() {
|
||||
decorator.backgroundProperty().bind(
|
||||
decorator.contentBackgroundProperty().bind(
|
||||
Bindings.createObjectBinding(
|
||||
() -> {
|
||||
Image image = null;
|
||||
@@ -172,9 +172,9 @@ public class DecoratorController {
|
||||
}
|
||||
|
||||
List<Path> candidates;
|
||||
try {
|
||||
candidates = Files.list(imageDir)
|
||||
.filter(Files::isRegularFile)
|
||||
try (Stream<Path> stream = Files.list(imageDir)) {
|
||||
candidates = stream
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(it -> {
|
||||
String filename = it.getFileName().toString();
|
||||
return filename.endsWith(".png") || filename.endsWith(".jpg");
|
||||
@@ -302,16 +302,16 @@ public class DecoratorController {
|
||||
Platform.runLater(() -> showDialog(node));
|
||||
return;
|
||||
}
|
||||
|
||||
dialog = new JFXDialog();
|
||||
dialogPane = new StackContainerPane();
|
||||
|
||||
dialog.setContent(dialogPane);
|
||||
decorator.capableDraggingWindow(dialog);
|
||||
decorator.forbidDraggingWindow(dialogPane);
|
||||
dialog.setDialogContainer(decorator.getDrawerWrapper());
|
||||
dialog.setOverlayClose(false);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
dialogPane.push(node);
|
||||
|
||||
EventHandler<DialogCloseEvent> handler = event -> closeDialog(node);
|
||||
|
||||
@@ -30,6 +30,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -49,14 +50,13 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE),
|
||||
glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); });
|
||||
|
||||
private final BorderPane root;
|
||||
private final StackPane root, parent;
|
||||
private final StackPane titleContainer;
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
private final StackPane leftPane;
|
||||
|
||||
private double xOffset, yOffset, newX, newY, initX, initY;
|
||||
private boolean allowMove, isDragging;
|
||||
private boolean titleBarTransparent = true;
|
||||
|
||||
/**
|
||||
@@ -72,14 +72,20 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
minus.fillProperty().bind(Theme.foregroundFillBinding());
|
||||
|
||||
Decorator skinnable = getSkinnable();
|
||||
StackPane parent = new StackPane();
|
||||
parent.backgroundProperty().bind(skinnable.backgroundProperty());
|
||||
root = new StackPane();
|
||||
root.getStyleClass().add("window");
|
||||
|
||||
parent = new StackPane();
|
||||
parent.getStyleClass().add("body");
|
||||
parent.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
parent.setPickOnBounds(false);
|
||||
parent.prefHeightProperty().bind(control.prefHeightProperty());
|
||||
parent.prefWidthProperty().bind(control.prefWidthProperty());
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, this::onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, this::onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
|
||||
|
||||
root = new BorderPane();
|
||||
root.getStyleClass().addAll("jfx-decorator", "resize-border");
|
||||
root.getChildren().setAll(parent);
|
||||
|
||||
// animation layer at bottom
|
||||
{
|
||||
@@ -89,12 +95,18 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
leftPane.getStyleClass().add("jfx-decorator-drawer");
|
||||
layer.getChildren().setAll(leftPane);
|
||||
parent.getChildren().add(layer);
|
||||
parent.getChildren().add(root);
|
||||
}
|
||||
|
||||
StackPane wrapper = new StackPane();
|
||||
BorderPane frame = new BorderPane();
|
||||
frame.getStyleClass().addAll("jfx-decorator");
|
||||
wrapper.getChildren().setAll(frame);
|
||||
skinnable.setDrawerWrapper(wrapper);
|
||||
|
||||
parent.getChildren().add(wrapper);
|
||||
|
||||
// center node with a animation layer at bottom, a container layer at middle and a "welcome" layer at top.
|
||||
StackPane container = new StackPane();
|
||||
skinnable.setDrawerWrapper(container);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
|
||||
// content layer at middle
|
||||
@@ -125,19 +137,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
container.getChildren().add(floatLayer);
|
||||
}
|
||||
|
||||
root.setCenter(container);
|
||||
frame.setCenter(container);
|
||||
|
||||
titleContainer = new StackPane();
|
||||
root.setOnMouseReleased(this::onMouseReleased);
|
||||
root.setOnMouseDragged(this::onMouseDragged);
|
||||
root.setOnMouseMoved(this::onMouseMoved);
|
||||
titleContainer.setPickOnBounds(false);
|
||||
titleContainer.setMinHeight(40);
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar");
|
||||
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true);
|
||||
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
|
||||
if (!isDragging) allowMove = false;
|
||||
});
|
||||
control.capableDraggingWindow(titleContainer);
|
||||
|
||||
StackPane titleBarBackground = new StackPane();
|
||||
titleBarBackground.getStyleClass().add("background");
|
||||
@@ -151,6 +156,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
rectangle.widthProperty().bind(titleContainer.widthProperty());
|
||||
rectangle.heightProperty().bind(titleContainer.heightProperty().add(100));
|
||||
titleContainer.setClip(rectangle);
|
||||
|
||||
Rectangle buttonsContainerPlaceHolder = new Rectangle();
|
||||
{
|
||||
navBarPane = new TransitionPane();
|
||||
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
|
||||
@@ -170,11 +177,14 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, s.getLeftPaneWidth(), FXUtils.SINE);
|
||||
});
|
||||
titleBar.setCenter(navBarPane);
|
||||
titleBar.setRight(buttonsContainerPlaceHolder);
|
||||
}
|
||||
frame.setTop(titleContainer);
|
||||
|
||||
{
|
||||
HBox buttonsContainer = new HBox();
|
||||
buttonsContainer.setStyle("-fx-background-color: transparent;");
|
||||
buttonsContainer.setAlignment(Pos.CENTER_RIGHT);
|
||||
buttonsContainer.setPadding(new Insets(4));
|
||||
buttonsContainer.setAlignment(Pos.TOP_RIGHT);
|
||||
buttonsContainer.setMaxHeight(40);
|
||||
{
|
||||
JFXButton btnMin = new JFXButton();
|
||||
StackPane pane = new StackPane(minus);
|
||||
@@ -190,11 +200,16 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
buttonsContainer.getChildren().setAll(btnMin, btnClose);
|
||||
}
|
||||
titleBar.setRight(buttonsContainer);
|
||||
AnchorPane layer = new AnchorPane();
|
||||
layer.setPickOnBounds(false);
|
||||
layer.getChildren().add(buttonsContainer);
|
||||
AnchorPane.setTopAnchor(buttonsContainer, 0.0);
|
||||
AnchorPane.setRightAnchor(buttonsContainer, 0.0);
|
||||
buttonsContainerPlaceHolder.widthProperty().bind(buttonsContainer.widthProperty());
|
||||
parent.getChildren().add(layer);
|
||||
}
|
||||
root.setTop(titleContainer);
|
||||
|
||||
getChildren().add(parent);
|
||||
getChildren().add(root);
|
||||
}
|
||||
|
||||
private Node createNavBar(Decorator skinnable, boolean titleBarTransparent, double leftPaneWidth, boolean canBack, boolean canClose, boolean showCloseAsHome, boolean canRefresh, String title, Node titleNode) {
|
||||
@@ -203,32 +218,32 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
HBox navLeft = new HBox();
|
||||
navLeft.setAlignment(Pos.CENTER_LEFT);
|
||||
navLeft.setPadding(new Insets(0, 5, 0, 5));
|
||||
{
|
||||
JFXButton backNavButton = new JFXButton();
|
||||
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
|
||||
backNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
|
||||
backNavButton.visibleProperty().set(canBack);
|
||||
|
||||
JFXButton closeNavButton = new JFXButton();
|
||||
JFXButton backNavButton = new JFXButton();
|
||||
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
|
||||
backNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
|
||||
backNavButton.visibleProperty().set(canBack);
|
||||
|
||||
JFXButton closeNavButton = new JFXButton();
|
||||
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
|
||||
closeNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
|
||||
|
||||
if (canBack) navLeft.getChildren().add(backNavButton);
|
||||
if (canClose) navLeft.getChildren().add(closeNavButton);
|
||||
if (showCloseAsHome)
|
||||
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
|
||||
else
|
||||
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
|
||||
closeNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
|
||||
|
||||
if (canBack) navLeft.getChildren().add(backNavButton);
|
||||
if (canClose) navLeft.getChildren().add(closeNavButton);
|
||||
if (showCloseAsHome)
|
||||
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
|
||||
else
|
||||
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
|
||||
}
|
||||
navBar.setLeft(navLeft);
|
||||
|
||||
BorderPane center = new BorderPane();
|
||||
if (title != null) {
|
||||
Label titleLabel = new Label();
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
if (titleBarTransparent) titleLabel.pseudoClassStateChanged(TRANSPARENT, true);
|
||||
if (!canClose && !canBack) {
|
||||
@@ -237,6 +252,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
// we should have width 2 * 10 reduced
|
||||
titleLabel.setPrefWidth(leftPaneWidth - 20);
|
||||
}
|
||||
if (titleNode != null) {
|
||||
titleLabel.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||
// 18 = 10 (horizontal padding of navLeft) + 8 (margin-left)
|
||||
return leftPaneWidth - 18 - backNavButton.getWidth() - closeNavButton.getWidth();
|
||||
}, backNavButton.widthProperty(), closeNavButton.widthProperty()));
|
||||
}
|
||||
titleLabel.setText(title);
|
||||
center.setLeft(titleLabel);
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
@@ -277,19 +298,19 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
}
|
||||
|
||||
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
|
||||
return x < root.getWidth() && x > root.getWidth() - root.snappedLeftInset();
|
||||
return x < root.getWidth() && x >= root.getWidth() - root.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean isTopEdge(double x, double y, Bounds boundsInParent) {
|
||||
return y >= 0 && y < root.snappedLeftInset();
|
||||
return y >= 0 && y <= root.snappedTopInset();
|
||||
}
|
||||
|
||||
private boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
|
||||
return y < root.getHeight() && y > root.getHeight() - root.snappedLeftInset();
|
||||
return y < root.getHeight() && y >= root.getHeight() - root.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
|
||||
return x >= 0 && x < root.snappedLeftInset();
|
||||
return x >= 0 && x <= root.snappedLeftInset();
|
||||
}
|
||||
|
||||
private boolean setStageWidth(double width) {
|
||||
@@ -326,43 +347,41 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
if (primaryStage.isResizable()) {
|
||||
double x = mouseEvent.getX(), y = mouseEvent.getY();
|
||||
Bounds boundsInParent = root.getBoundsInParent();
|
||||
if (root.getBorder() != null && root.getBorder().getStrokes().size() > 0) {
|
||||
double diagonalSize = root.snappedLeftInset() + 10;
|
||||
if (this.isRightEdge(x, y, boundsInParent)) {
|
||||
if (y < diagonalSize) {
|
||||
root.setCursor(Cursor.NE_RESIZE);
|
||||
} else if (y > root.getHeight() - diagonalSize) {
|
||||
root.setCursor(Cursor.SE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.E_RESIZE);
|
||||
}
|
||||
} else if (this.isLeftEdge(x, y, boundsInParent)) {
|
||||
if (y < diagonalSize) {
|
||||
root.setCursor(Cursor.NW_RESIZE);
|
||||
} else if (y > root.getHeight() - diagonalSize) {
|
||||
root.setCursor(Cursor.SW_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.W_RESIZE);
|
||||
}
|
||||
} else if (this.isTopEdge(x, y, boundsInParent)) {
|
||||
if (x < diagonalSize) {
|
||||
root.setCursor(Cursor.NW_RESIZE);
|
||||
} else if (x > root.getWidth() - diagonalSize) {
|
||||
root.setCursor(Cursor.NE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.N_RESIZE);
|
||||
}
|
||||
} else if (this.isBottomEdge(x, y, boundsInParent)) {
|
||||
if (x < diagonalSize) {
|
||||
root.setCursor(Cursor.SW_RESIZE);
|
||||
} else if (x > root.getWidth() - diagonalSize) {
|
||||
root.setCursor(Cursor.SE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.S_RESIZE);
|
||||
}
|
||||
double diagonalSize = root.snappedLeftInset() + 10;
|
||||
if (this.isRightEdge(x, y, boundsInParent)) {
|
||||
if (y < diagonalSize) {
|
||||
root.setCursor(Cursor.NE_RESIZE);
|
||||
} else if (y > root.getHeight() - diagonalSize) {
|
||||
root.setCursor(Cursor.SE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.DEFAULT);
|
||||
root.setCursor(Cursor.E_RESIZE);
|
||||
}
|
||||
} else if (this.isLeftEdge(x, y, boundsInParent)) {
|
||||
if (y < diagonalSize) {
|
||||
root.setCursor(Cursor.NW_RESIZE);
|
||||
} else if (y > root.getHeight() - diagonalSize) {
|
||||
root.setCursor(Cursor.SW_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.W_RESIZE);
|
||||
}
|
||||
} else if (this.isTopEdge(x, y, boundsInParent)) {
|
||||
if (x < diagonalSize) {
|
||||
root.setCursor(Cursor.NW_RESIZE);
|
||||
} else if (x > root.getWidth() - diagonalSize) {
|
||||
root.setCursor(Cursor.NE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.N_RESIZE);
|
||||
}
|
||||
} else if (this.isBottomEdge(x, y, boundsInParent)) {
|
||||
if (x < diagonalSize) {
|
||||
root.setCursor(Cursor.SW_RESIZE);
|
||||
} else if (x > root.getWidth() - diagonalSize) {
|
||||
root.setCursor(Cursor.SE_RESIZE);
|
||||
} else {
|
||||
root.setCursor(Cursor.S_RESIZE);
|
||||
}
|
||||
} else {
|
||||
root.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -371,11 +390,11 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
}
|
||||
|
||||
protected void onMouseReleased(MouseEvent mouseEvent) {
|
||||
isDragging = false;
|
||||
getSkinnable().setDragging(false);
|
||||
}
|
||||
|
||||
protected void onMouseDragged(MouseEvent mouseEvent) {
|
||||
this.isDragging = true;
|
||||
getSkinnable().setDragging(true);
|
||||
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
|
||||
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress()) {
|
||||
this.newX = mouseEvent.getScreenX();
|
||||
@@ -429,7 +448,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
}
|
||||
|
||||
mouseEvent.consume();
|
||||
} else if (this.allowMove) {
|
||||
} else if (getSkinnable().isAllowMove()) {
|
||||
this.primaryStage.setX(mouseEvent.getScreenX() - this.xOffset);
|
||||
this.primaryStage.setY(mouseEvent.getScreenY() - this.yOffset);
|
||||
mouseEvent.consume();
|
||||
|
||||
@@ -19,16 +19,28 @@ package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.wizard.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements TaskExecutorDialogWizardDisplayer {
|
||||
public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements WizardDisplayer {
|
||||
private final WizardController wizardController = new WizardController(this);
|
||||
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
|
||||
private final WizardDisplayer displayer = new TaskExecutorDialogWizardDisplayer(new ConcurrentLinkedQueue<>()) {
|
||||
@Override
|
||||
public void onEnd() {
|
||||
super.onEnd();
|
||||
fireEvent(new PageCloseEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
|
||||
}
|
||||
};
|
||||
|
||||
private final String category;
|
||||
|
||||
@@ -45,28 +57,24 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WizardController getWizardController() {
|
||||
return wizardController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Queue<Object> getCancelQueue() {
|
||||
return cancelQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
displayer.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
displayer.onCancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnd() {
|
||||
fireEvent(new PageCloseEvent());
|
||||
displayer.onEnd();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
|
||||
displayer.navigateTo(page, nav);
|
||||
navigate(page, nav.getAnimation().getAnimationProducer());
|
||||
|
||||
String prefix = category == null ? "" : category + " - ";
|
||||
@@ -86,6 +94,11 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTask(Map<String, Object> settings, Task<?> task) {
|
||||
displayer.handleTask(settings, task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPageCloseable() {
|
||||
return true;
|
||||
|
||||
@@ -20,11 +20,11 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
@@ -40,7 +40,7 @@ class AdditionalInstallersPage extends InstallersPage {
|
||||
protected final String gameVersion;
|
||||
protected final Version version;
|
||||
|
||||
public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, GameRepository repository, DownloadProvider downloadProvider) {
|
||||
public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, GameRepository repository, InstallerWizardDownloadProvider downloadProvider) {
|
||||
super(controller, repository, gameVersion, downloadProvider);
|
||||
this.gameVersion = gameVersion;
|
||||
this.version = version;
|
||||
@@ -54,10 +54,8 @@ class AdditionalInstallersPage extends InstallersPage {
|
||||
() -> compatible.get() && txtName.validate(),
|
||||
txtName.textProperty(), compatible));
|
||||
|
||||
InstallerPageItem[] libraries = new InstallerPageItem[]{game, fabric, forge, liteLoader, optiFine};
|
||||
|
||||
for (InstallerPageItem library : libraries) {
|
||||
String libraryId = library.id;
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals("game")) continue;
|
||||
library.removeAction.set(e -> {
|
||||
controller.getSettings().put(libraryId, new UpdateInstallerWizardProvider.RemoveVersionAction(libraryId));
|
||||
@@ -91,28 +89,25 @@ class AdditionalInstallersPage extends InstallersPage {
|
||||
String liteLoader = analyzer.getVersion(LITELOADER).orElse(null);
|
||||
String optiFine = analyzer.getVersion(OPTIFINE).orElse(null);
|
||||
|
||||
InstallerPageItem[] libraries = new InstallerPageItem[]{this.game, this.fabric, this.forge, this.liteLoader, this.optiFine};
|
||||
InstallerItem[] libraries = group.getLibraries();
|
||||
String[] versions = new String[]{game, fabric, forge, liteLoader, optiFine};
|
||||
|
||||
String currentGameVersion = Lang.nonNull(getVersion("game"), game);
|
||||
|
||||
boolean compatible = true;
|
||||
for (int i = 0; i < libraries.length; ++i) {
|
||||
String libraryId = libraries[i].id;
|
||||
String libraryId = libraries[i].getLibraryId();
|
||||
String libraryVersion = Lang.nonNull(getVersion(libraryId), versions[i]);
|
||||
boolean alreadyInstalled = versions[i] != null && !(controller.getSettings().get(libraryId) instanceof UpdateInstallerWizardProvider.RemoveVersionAction);
|
||||
if (!"game".equals(libraryId) && currentGameVersion != null && !currentGameVersion.equals(game) && getVersion(libraryId) == null && alreadyInstalled) {
|
||||
// For third-party libraries, if game version is being changed, and the library is not being reinstalled,
|
||||
// warns the user that we should update the library.
|
||||
libraries[i].label.set(i18n("install.installer.change_version", i18n("install.installer." + libraryId), libraryVersion));
|
||||
libraries[i].removable.set(true);
|
||||
libraries[i].setState(libraryVersion, /* incompatibleWithGame */ true, /* removable */ true);
|
||||
compatible = false;
|
||||
} else if (alreadyInstalled || getVersion(libraryId) != null) {
|
||||
libraries[i].label.set(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + libraryVersion);
|
||||
libraries[i].removable.set(true);
|
||||
libraries[i].setState(libraryVersion, /* incompatibleWithGame */ false, /* removable */ true);
|
||||
} else {
|
||||
libraries[i].label.set(i18n("install.installer.not_installed", i18n("install.installer." + libraryId)));
|
||||
libraries[i].removable.set(false);
|
||||
libraries[i].setState(/* libraryVersion */ null, /* incompatibleWithGame */ false, /* removable */ false);
|
||||
}
|
||||
}
|
||||
this.compatible.set(compatible);
|
||||
|
||||
+72
@@ -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.download;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
public class InstallerWizardDownloadProvider implements DownloadProvider {
|
||||
|
||||
private DownloadProvider fallback;
|
||||
|
||||
public InstallerWizardDownloadProvider(DownloadProvider fallback) {
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
public void setDownloadProvider(DownloadProvider downloadProvider) {
|
||||
fallback = downloadProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersionListURL() {
|
||||
return fallback.getVersionListURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAssetBaseURL() {
|
||||
return fallback.getAssetBaseURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<URL> getAssetObjectCandidates(String assetObjectLocation) {
|
||||
return fallback.getAssetObjectCandidates(assetObjectLocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String injectURL(String baseURL) {
|
||||
return fallback.injectURL(baseURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<URL> injectURLWithCandidates(String baseURL) {
|
||||
return fallback.injectURLWithCandidates(baseURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionList<?> getVersionListById(String id) {
|
||||
return fallback.getVersionListById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getConcurrency() {
|
||||
return fallback.getConcurrency();
|
||||
}
|
||||
}
|
||||
@@ -19,30 +19,21 @@ package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
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.VBox;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
@@ -56,15 +47,11 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public class InstallersPage extends Control implements WizardPage {
|
||||
protected final WizardController controller;
|
||||
|
||||
protected InstallerPageItem game = new InstallerPageItem("game");
|
||||
protected InstallerPageItem fabric = new InstallerPageItem("fabric");
|
||||
protected InstallerPageItem forge = new InstallerPageItem("forge");
|
||||
protected InstallerPageItem liteLoader = new InstallerPageItem("liteloader");
|
||||
protected InstallerPageItem optiFine = new InstallerPageItem("optifine");
|
||||
protected InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup();
|
||||
protected JFXTextField txtName = new JFXTextField();
|
||||
protected BooleanProperty installable = new SimpleBooleanProperty();
|
||||
|
||||
public InstallersPage(WizardController controller, GameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
|
||||
public InstallersPage(WizardController controller, GameRepository repository, String gameVersion, InstallerWizardDownloadProvider downloadProvider) {
|
||||
this.controller = controller;
|
||||
|
||||
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
|
||||
@@ -76,13 +63,15 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
txtName.textProperty()));
|
||||
txtName.setText(gameVersion);
|
||||
|
||||
InstallerPageItem[] libraries = new InstallerPageItem[]{game, fabric, forge, liteLoader, optiFine};
|
||||
group.game.installable.setValue(false);
|
||||
|
||||
for (InstallerPageItem library : libraries) {
|
||||
String libraryId = library.id;
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals("game")) continue;
|
||||
library.action.set(e ->
|
||||
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false))));
|
||||
library.action.set(e -> {
|
||||
if (library.incompatibleLibraryName.get() == null)
|
||||
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false)));
|
||||
});
|
||||
library.removeAction.set(e -> {
|
||||
controller.getSettings().remove(libraryId);
|
||||
reload();
|
||||
@@ -100,15 +89,13 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
}
|
||||
|
||||
protected void reload() {
|
||||
InstallerPageItem[] libraries = new InstallerPageItem[]{game, fabric, forge, liteLoader, optiFine};
|
||||
|
||||
for (InstallerPageItem library : libraries) {
|
||||
String libraryId = library.id;
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (controller.getSettings().containsKey(libraryId)) {
|
||||
library.label.set(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + getVersion(libraryId));
|
||||
library.libraryVersion.set(getVersion(libraryId));
|
||||
library.removable.set(true);
|
||||
} else {
|
||||
library.label.setValue(i18n("install.installer.not_installed", i18n("install.installer." + libraryId)));
|
||||
library.libraryVersion.set(null);
|
||||
library.removable.set(false);
|
||||
}
|
||||
}
|
||||
@@ -134,55 +121,8 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
return new InstallersPageSkin(this);
|
||||
}
|
||||
|
||||
protected static class InstallerPageItem {
|
||||
String id;
|
||||
StringProperty label = new SimpleStringProperty();
|
||||
BooleanProperty removable = new SimpleBooleanProperty();
|
||||
ObjectProperty<EventHandler<? super MouseEvent>> removeAction = new SimpleObjectProperty<>();
|
||||
ObjectProperty<EventHandler<? super MouseEvent>> action = new SimpleObjectProperty<>();
|
||||
|
||||
public InstallerPageItem(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class InstallersPageSkin extends SkinBase<InstallersPage> {
|
||||
|
||||
protected static class InstallersPageItemSkin extends BorderPane {
|
||||
final ImageView imageView;
|
||||
final Label label;
|
||||
|
||||
InstallersPageItemSkin(String imageUrl, InstallerPageItem item, boolean clickable) {
|
||||
setPadding(new Insets(8));
|
||||
getStyleClass().add("card");
|
||||
|
||||
setLeft(FXUtils.limitingSize(imageView = new ImageView(new Image(imageUrl, 32, 32, true, true)), 32, 32));
|
||||
setCenter(label = new Label());
|
||||
label.textProperty().bind(item.label);
|
||||
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
|
||||
if (clickable) {
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
setRight(right);
|
||||
JFXButton closeButton = new JFXButton();
|
||||
closeButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
|
||||
right.getChildren().add(closeButton);
|
||||
closeButton.getStyleClass().add("toggle-icon4");
|
||||
closeButton.visibleProperty().bind(item.removable);
|
||||
closeButton.onMouseClickedProperty().bind(item.removeAction);
|
||||
onMouseClickedProperty().bind(item.action);
|
||||
JFXButton arrowButton = new JFXButton();
|
||||
arrowButton.setGraphic(SVG.arrowRight(Theme.blackFillBinding(), -1, -1));
|
||||
arrowButton.onMouseClickedProperty().bind(item.action);
|
||||
arrowButton.getStyleClass().add("toggle-icon4");
|
||||
right.getChildren().add(arrowButton);
|
||||
setCursor(Cursor.HAND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for all SkinBase instances.
|
||||
*
|
||||
@@ -200,7 +140,7 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
HBox versionNamePane = new HBox(8);
|
||||
versionNamePane.setAlignment(Pos.CENTER_LEFT);
|
||||
versionNamePane.getStyleClass().add("card");
|
||||
versionNamePane.setPadding(new Insets(20, 8, 20, 16));
|
||||
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
|
||||
|
||||
versionNamePane.getChildren().add(new Label(i18n("archive.name")));
|
||||
|
||||
@@ -209,13 +149,7 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
list.getChildren().add(versionNamePane);
|
||||
}
|
||||
|
||||
InstallersPageItemSkin game = new InstallersPageItemSkin("/assets/img/grass.png", control.game, false);
|
||||
InstallersPageItemSkin fabric = new InstallersPageItemSkin("/assets/img/fabric.png", control.fabric, true);
|
||||
InstallersPageItemSkin forge = new InstallersPageItemSkin("/assets/img/forge.png", control.forge, true);
|
||||
InstallersPageItemSkin liteLoader = new InstallersPageItemSkin("/assets/img/chicken.png", control.liteLoader, true);
|
||||
InstallersPageItemSkin optiFine = new InstallersPageItemSkin("/assets/img/command.png", control.optiFine, true);
|
||||
list.getChildren().addAll(game, fabric, forge, liteLoader, optiFine);
|
||||
list.getChildren().forEach(node -> JFXDepthManager.setDepth(node, 1));
|
||||
list.getChildren().addAll(control.group.getLibraries());
|
||||
|
||||
{
|
||||
JFXButton installButton = new JFXButton(i18n("button.install"));
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
@@ -90,8 +91,17 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
if (name.isPresent()) {
|
||||
txtModpackName.setText(name.get());
|
||||
txtModpackName.setDisable(true);
|
||||
} else {
|
||||
txtModpackName.getValidators().addAll(
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
|
||||
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
|
||||
);
|
||||
}
|
||||
|
||||
btnInstall.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> !txtModpackName.validate(),
|
||||
txtModpackName.textProperty()));
|
||||
|
||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
||||
if (filePath.isPresent()) {
|
||||
selectedFile = filePath.get();
|
||||
@@ -101,7 +111,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile == null) {
|
||||
Platform.runLater(controller::onEnd);
|
||||
controller.onEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,12 +131,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
lblModpackLocation.setText(selectedFile.getAbsolutePath());
|
||||
|
||||
if (!name.isPresent()) {
|
||||
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
|
||||
txtModpackName.getValidators().addAll(
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
|
||||
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
|
||||
);
|
||||
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
|
||||
txtModpackName.setText(manifest.getName());
|
||||
}
|
||||
}, e -> {
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
|
||||
|
||||
@@ -85,7 +85,11 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
|
||||
if (serverModpackManifest != null) {
|
||||
return ModpackHelper.getUpdateTask(profile, serverModpackManifest, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
|
||||
} else {
|
||||
return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
|
||||
}
|
||||
} catch (UnsupportedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
} catch (MismatchedModpackTypeException e) {
|
||||
|
||||
@@ -24,13 +24,8 @@ import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WebStage;
|
||||
@@ -40,10 +35,6 @@ import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -110,7 +101,7 @@ public class RemoteModpackPage extends StackPane implements WizardPage {
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
|
||||
if (!name.isPresent()) {
|
||||
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
|
||||
txtModpackName.setText(manifest.getName());
|
||||
txtModpackName.getValidators().addAll(
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
|
||||
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
|
||||
|
||||
+16
-10
@@ -18,12 +18,16 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionMismatchException;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -53,14 +57,16 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
private final Version version;
|
||||
private final String libraryId;
|
||||
private final String oldLibraryVersion;
|
||||
private final InstallerWizardDownloadProvider downloadProvider;
|
||||
|
||||
public UpdateInstallerWizardProvider(@NotNull Profile profile, @NotNull String gameVersion, @NotNull Version version, @NotNull String libraryId, @Nullable String oldLibraryVersion) {
|
||||
this.profile = profile;
|
||||
this.dependencyManager = profile.getDependency();
|
||||
this.gameVersion = gameVersion;
|
||||
this.version = version;
|
||||
this.libraryId = libraryId;
|
||||
this.oldLibraryVersion = oldLibraryVersion;
|
||||
this.downloadProvider = new InstallerWizardDownloadProvider(DownloadProviders.getDownloadProvider());
|
||||
this.dependencyManager = profile.getDependency(downloadProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,20 +96,19 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
}
|
||||
}
|
||||
|
||||
return ret.thenComposeAsync(profile.getRepository()::save).thenComposeAsync(profile.getRepository().refreshVersionsAsync()).withStagesHint(stages);
|
||||
return ret.thenComposeAsync(profile.getRepository()::saveAsync).thenComposeAsync(profile.getRepository().refreshVersionsAsync()).withStagesHint(stages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
DownloadProvider provider = profile.getDependency().getPrimaryDownloadProvider();
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, provider, libraryId, () -> {
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> {
|
||||
if (oldLibraryVersion == null) {
|
||||
controller.onFinish();
|
||||
} else if ("game".equals(libraryId)) {
|
||||
String newGameVersion = ((RemoteVersion) settings.get(libraryId)).getSelfVersion();
|
||||
controller.onNext(new AdditionalInstallersPage(newGameVersion, version, controller, profile.getRepository(), provider));
|
||||
controller.onNext(new AdditionalInstallersPage(newGameVersion, version, controller, profile.getRepository(), downloadProvider));
|
||||
} else {
|
||||
Controllers.confirm(i18n("install.change_version.confirm", i18n("install.installer." + libraryId), oldLibraryVersion, ((RemoteVersion) settings.get(libraryId)).getSelfVersion()),
|
||||
i18n("install.change_version"), controller::onFinish, controller::onCancel);
|
||||
@@ -142,17 +147,18 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
}
|
||||
Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
Controllers.dialog(i18n("install.failed.downloading.timeout", ((DownloadException) exception).getUrl()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.timeout", url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), ((DownloadException) exception).getUrl()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else if (exception instanceof OptiFineInstallTask.UnsupportedOptiFineInstallationException ||
|
||||
exception instanceof FabricInstallTask.UnsupportedFabricInstallationException) {
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -33,9 +34,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
private final Profile profile;
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final InstallerWizardDownloadProvider downloadProvider;
|
||||
|
||||
public VanillaInstallWizardProvider(Profile profile) {
|
||||
this.profile = profile;
|
||||
this.downloadProvider = new InstallerWizardDownloadProvider(DownloadProviders.getDownloadProvider());
|
||||
this.dependencyManager = profile.getDependency(downloadProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,7 +49,7 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
private Task<Void> finishVersionDownloadingAsync(Map<String, Object> settings) {
|
||||
GameBuilder builder = profile.getDependency().gameBuilder();
|
||||
GameBuilder builder = dependencyManager.gameBuilder();
|
||||
|
||||
String name = (String) settings.get("name");
|
||||
builder.name(name);
|
||||
@@ -69,11 +74,10 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
DownloadProvider provider = profile.getDependency().getPrimaryDownloadProvider();
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", provider, "game",
|
||||
() -> controller.onNext(new InstallersPage(controller, profile.getRepository(), ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), provider)));
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", downloadProvider, "game",
|
||||
() -> controller.onNext(new InstallersPage(controller, profile.getRepository(), ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider)));
|
||||
default:
|
||||
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.application.Platform;
|
||||
@@ -31,7 +32,6 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
||||
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
@@ -54,6 +55,7 @@ import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -83,23 +85,34 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
private HBox checkPane;
|
||||
@FXML
|
||||
private VBox centrePane;
|
||||
@FXML
|
||||
private JFXComboBox<String> downloadSourceComboBox;
|
||||
|
||||
private final VersionList<?> versionList;
|
||||
private VersionList<?> versionList;
|
||||
private TaskExecutor executor;
|
||||
|
||||
public VersionsPage(WizardController controller, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
|
||||
public VersionsPage(WizardController controller, String title, String gameVersion, InstallerWizardDownloadProvider downloadProvider, String libraryId, Runnable callback) {
|
||||
this.title = title;
|
||||
this.gameVersion = gameVersion;
|
||||
this.libraryId = libraryId;
|
||||
this.controller = controller;
|
||||
this.versionList = downloadProvider.getVersionListById(libraryId);
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
|
||||
|
||||
if (versionList.hasType()) {
|
||||
centrePane.getChildren().setAll(checkPane, list);
|
||||
} else
|
||||
centrePane.getChildren().setAll(list);
|
||||
downloadSourceComboBox.getItems().setAll(DownloadProviders.providersById.keySet());
|
||||
downloadSourceComboBox.setConverter(stringConverter(key -> i18n("download.provider." + key)));
|
||||
downloadSourceComboBox.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
||||
controller.getSettings().put("downloadProvider", newValue);
|
||||
downloadProvider.setDownloadProvider(DownloadProviders.getDownloadProviderByPrimaryId(newValue));
|
||||
versionList = downloadProvider.getVersionListById(libraryId);
|
||||
if (versionList.hasType()) {
|
||||
centrePane.getChildren().setAll(checkPane, list);
|
||||
} else {
|
||||
centrePane.getChildren().setAll(list);
|
||||
}
|
||||
refresh();
|
||||
});
|
||||
downloadSourceComboBox.getSelectionModel().select((String)controller.getSettings().getOrDefault("downloadProvider", DownloadProviders.getPrimaryDownloadProviderId()));
|
||||
|
||||
InvalidationListener listener = o -> list.getItems().setAll(loadVersions());
|
||||
chkRelease.selectedProperty().addListener(listener);
|
||||
@@ -184,13 +197,15 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
VersionList<?> currentVersionList = versionList;
|
||||
root.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
|
||||
executor = versionList.refreshAsync(gameVersion).whenComplete(exception -> {
|
||||
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(exception -> {
|
||||
if (exception == null) {
|
||||
List<RemoteVersion> items = loadVersions();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if (versionList.getVersions(gameVersion).isEmpty()) {
|
||||
if (versionList != currentVersionList) return;
|
||||
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
|
||||
root.setContent(emptyPane, ContainerAnimations.FADE.getAnimationProducer());
|
||||
} else {
|
||||
if (items.isEmpty()) {
|
||||
@@ -206,6 +221,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Failed to fetch versions list", exception);
|
||||
Platform.runLater(() -> {
|
||||
if (versionList != currentVersionList) return;
|
||||
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
@@ -36,7 +36,6 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -63,72 +62,97 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
|
||||
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
|
||||
String modpackName = (String) settings.get(ModpackInfoPage.MODPACK_NAME);
|
||||
String modpackAuthor = (String) settings.get(ModpackInfoPage.MODPACK_AUTHOR);
|
||||
String modpackFileApi = (String) settings.get(ModpackInfoPage.MODPACK_FILE_API);
|
||||
String modpackVersion = (String) settings.get(ModpackInfoPage.MODPACK_VERSION);
|
||||
String modpackDescription = (String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION);
|
||||
ModpackExportInfo exportInfo = (ModpackExportInfo) settings.get(ModpackInfoPage.MODPACK_INFO);
|
||||
exportInfo.setWhitelist(whitelist);
|
||||
String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
|
||||
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER);
|
||||
|
||||
switch (modpackType) {
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_HMCL:
|
||||
return exportAsHMCL(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, includeLauncher);
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
|
||||
return exportAsMultiMC(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription);
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
|
||||
return exportAsServer(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi);
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
|
||||
}
|
||||
return exportWithLauncher(modpackType, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
private Task<?> exportAsHMCL(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, boolean includeLauncherRaw) {
|
||||
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, File modpackFile) {
|
||||
List<File> launcherJar = Launcher.getCurrentJarFiles();
|
||||
boolean includeLauncher = includeLauncherRaw && launcherJar != null;
|
||||
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
|
||||
return new Task<Object>() {
|
||||
File tempModpack;
|
||||
Task<?> exportTask;
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
File dest;
|
||||
if (packWithLauncher) {
|
||||
dest = tempModpack = Files.createTempFile("hmcl", ".zip").toFile();
|
||||
} else {
|
||||
dest = modpackFile;
|
||||
}
|
||||
|
||||
switch (modpackType) {
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MCBBS:
|
||||
exportTask = exportAsMcbbs(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
|
||||
exportTask = exportAsMultiMC(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
|
||||
exportTask = exportAsServer(exportInfo, dest);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return Collections.singleton(exportTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!packWithLauncher) return;
|
||||
try (Zipper zip = new Zipper(modpackFile.toPath())) {
|
||||
Config exported = new Config();
|
||||
|
||||
exported.setBackgroundImageType(config().getBackgroundImageType());
|
||||
exported.setBackgroundImage(config().getBackgroundImage());
|
||||
exported.setTheme(config().getTheme());
|
||||
exported.setDownloadType(config().getDownloadType());
|
||||
exported.setPreferredLoginType(config().getPreferredLoginType());
|
||||
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
|
||||
|
||||
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
|
||||
zip.putFile(tempModpack, "modpack.zip");
|
||||
|
||||
File bg = new File("bg").getAbsoluteFile();
|
||||
if (bg.isDirectory())
|
||||
zip.putDirectory(bg.toPath(), "bg");
|
||||
|
||||
File background_png = new File("background.png").getAbsoluteFile();
|
||||
if (background_png.isFile())
|
||||
zip.putFile(background_png, "background.png");
|
||||
|
||||
File background_jpg = new File("background.jpg").getAbsoluteFile();
|
||||
if (background_jpg.isFile())
|
||||
zip.putFile(background_jpg, "background.jpg");
|
||||
|
||||
for (File jar : launcherJar)
|
||||
zip.putFile(jar, jar.getName());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency = null;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
File tempModpack = includeLauncher ? Files.createTempFile("hmcl", ".zip").toFile() : modpackFile;
|
||||
|
||||
dependency = new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
|
||||
new Modpack(modpackName, modpackAuthor, modpackVersion, null, modpackDescription, StandardCharsets.UTF_8, null), tempModpack);
|
||||
|
||||
if (includeLauncher) {
|
||||
dependency = dependency.thenRunAsync(() -> {
|
||||
try (Zipper zip = new Zipper(modpackFile.toPath())) {
|
||||
Config exported = new Config();
|
||||
|
||||
exported.setBackgroundImageType(config().getBackgroundImageType());
|
||||
exported.setBackgroundImage(config().getBackgroundImage());
|
||||
exported.setTheme(config().getTheme());
|
||||
exported.setDownloadType(config().getDownloadType());
|
||||
exported.setPreferredLoginType(config().getPreferredLoginType());
|
||||
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
|
||||
|
||||
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
|
||||
zip.putFile(tempModpack, "modpack.zip");
|
||||
|
||||
File bg = new File("bg").getAbsoluteFile();
|
||||
if (bg.isDirectory())
|
||||
zip.putDirectory(bg.toPath(), "bg");
|
||||
|
||||
File background_png = new File("background.png").getAbsoluteFile();
|
||||
if (background_png.isFile())
|
||||
zip.putFile(background_png, "background.png");
|
||||
|
||||
File background_jpg = new File("background.jpg").getAbsoluteFile();
|
||||
if (background_jpg.isFile())
|
||||
zip.putFile(background_jpg, "background.jpg");
|
||||
|
||||
for (File jar : launcherJar)
|
||||
zip.putFile(jar, jar.getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
public void execute() {
|
||||
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -138,30 +162,30 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsMultiMC(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription) {
|
||||
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
VersionSetting vs = profile.getVersionSetting(version);
|
||||
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, whitelist,
|
||||
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, exportInfo.getWhitelist(),
|
||||
new MultiMCInstanceConfiguration(
|
||||
"OneSix",
|
||||
modpackName + "-" + modpackVersion,
|
||||
exportInfo.getName() + "-" + exportInfo.getVersion(),
|
||||
null,
|
||||
Lang.toIntOrNull(vs.getPermSize()),
|
||||
vs.getWrapper(),
|
||||
vs.getPreLaunchCommand(),
|
||||
null,
|
||||
modpackDescription,
|
||||
exportInfo.getDescription(),
|
||||
null,
|
||||
vs.getJavaArgs(),
|
||||
exportInfo.getJavaArguments(),
|
||||
vs.isFullscreen(),
|
||||
vs.getWidth(),
|
||||
vs.getHeight(),
|
||||
vs.getMaxMemory(),
|
||||
vs.getMinMemory(),
|
||||
exportInfo.getMinMemory(),
|
||||
vs.isShowLogs(),
|
||||
/* showConsoleOnError */ true,
|
||||
/* autoCloseConsole */ false,
|
||||
@@ -181,13 +205,13 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsServer(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, String modpackFileApi) {
|
||||
private Task<?> exportAsServer(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ServerModpackExportTask(profile.getRepository(), version, whitelist, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi, modpackFile);
|
||||
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -203,7 +227,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
case 0:
|
||||
return new ModpackTypeSelectionPage(controller);
|
||||
case 1:
|
||||
return new ModpackInfoPage(controller, version);
|
||||
return new ModpackInfoPage(controller, profile.getRepository(), version);
|
||||
case 2:
|
||||
return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
|
||||
default:
|
||||
|
||||
@@ -17,76 +17,96 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextArea;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.controls.JFXToggleButton;
|
||||
import com.jfoenix.validation.RequiredFieldValidator;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.*;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
private final WizardController controller;
|
||||
private final HMCLGameRepository gameRepository;
|
||||
private final ModpackExportInfo.Options options;
|
||||
private final String versionName;
|
||||
private final boolean canIncludeLauncher;
|
||||
private final boolean showFileApi;
|
||||
|
||||
private SimpleStringProperty versionName = new SimpleStringProperty();
|
||||
private SimpleStringProperty modpackName = new SimpleStringProperty();
|
||||
private SimpleStringProperty modpackFileApi = new SimpleStringProperty();
|
||||
private SimpleStringProperty modpackAuthor = new SimpleStringProperty();
|
||||
private SimpleStringProperty modpackVersion = new SimpleStringProperty("1.0");
|
||||
private SimpleStringProperty modpackDescription = new SimpleStringProperty();
|
||||
private SimpleBooleanProperty includingLauncher = new SimpleBooleanProperty();
|
||||
private ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
|
||||
private final ModpackExportInfo exportInfo = new ModpackExportInfo();
|
||||
|
||||
public ModpackInfoPage(WizardController controller, String version) {
|
||||
private final SimpleStringProperty name = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty author = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty version = new SimpleStringProperty("1.0");
|
||||
private final SimpleStringProperty description = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty url = new SimpleStringProperty("");
|
||||
private final SimpleBooleanProperty forceUpdate = new SimpleBooleanProperty();
|
||||
private final SimpleBooleanProperty packWithLauncher = new SimpleBooleanProperty();
|
||||
private final SimpleStringProperty fileApi = new SimpleStringProperty();
|
||||
private final SimpleIntegerProperty minMemory = new SimpleIntegerProperty(0);
|
||||
private final SimpleStringProperty authlibInjectorServer = new SimpleStringProperty();
|
||||
private final SimpleStringProperty launchArguments = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty javaArguments = new SimpleStringProperty("");
|
||||
private final ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
|
||||
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty();
|
||||
|
||||
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
|
||||
this.controller = controller;
|
||||
modpackName.set(version);
|
||||
modpackAuthor.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
|
||||
versionName.set(version);
|
||||
this.gameRepository = gameRepository;
|
||||
this.options = tryCast(controller.getSettings().get(MODPACK_INFO_OPTION), ModpackExportInfo.Options.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required"));
|
||||
this.versionName = version;
|
||||
|
||||
name.set(version);
|
||||
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
|
||||
|
||||
VersionSetting versionSetting = gameRepository.getVersionSetting(versionName);
|
||||
minMemory.set(Optional.ofNullable(versionSetting.getMinMemory()).orElse(0));
|
||||
launchArguments.set(versionSetting.getMinecraftArgs());
|
||||
javaArguments.set(versionSetting.getJavaArgs());
|
||||
|
||||
List<File> launcherJar = Launcher.getCurrentJarFiles();
|
||||
canIncludeLauncher = launcherJar != null;
|
||||
showFileApi = controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER;
|
||||
|
||||
next.set(e -> onNext());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onNext() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
|
||||
@@ -96,24 +116,35 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
controller.onEnd();
|
||||
return;
|
||||
}
|
||||
controller.getSettings().put(MODPACK_NAME, modpackName.get());
|
||||
controller.getSettings().put(MODPACK_FILE_API, modpackFileApi.get());
|
||||
controller.getSettings().put(MODPACK_VERSION, modpackVersion.get());
|
||||
controller.getSettings().put(MODPACK_AUTHOR, modpackAuthor.get());
|
||||
|
||||
exportInfo.setName(name.get());
|
||||
exportInfo.setFileApi(fileApi.get());
|
||||
exportInfo.setVersion(version.get());
|
||||
exportInfo.setAuthor(author.get());
|
||||
exportInfo.setDescription(description.get());
|
||||
exportInfo.setPackWithLauncher(packWithLauncher.get());
|
||||
exportInfo.setUrl(url.get());
|
||||
exportInfo.setForceUpdate(forceUpdate.get());
|
||||
exportInfo.setPackWithLauncher(packWithLauncher.get());
|
||||
exportInfo.setMinMemory(minMemory.get());
|
||||
exportInfo.setLaunchArguments(launchArguments.get());
|
||||
exportInfo.setJavaArguments(javaArguments.get());
|
||||
exportInfo.setAuthlibInjectorServer(authlibInjectorServer.get());
|
||||
|
||||
if (StringUtils.isNotBlank(mcbbsThreadId.get())) {
|
||||
exportInfo.setOrigins(Collections.singletonList(new McbbsModpackManifest.Origin(
|
||||
"mcbbs", Integer.parseInt(mcbbsThreadId.get())
|
||||
)));
|
||||
}
|
||||
|
||||
controller.getSettings().put(MODPACK_INFO, exportInfo);
|
||||
controller.getSettings().put(MODPACK_FILE, file);
|
||||
controller.getSettings().put(MODPACK_DESCRIPTION, modpackDescription.get());
|
||||
controller.getSettings().put(MODPACK_INCLUDE_LAUNCHER, includingLauncher.get());
|
||||
controller.onNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
controller.getSettings().remove(MODPACK_NAME);
|
||||
controller.getSettings().remove(MODPACK_VERSION);
|
||||
controller.getSettings().remove(MODPACK_AUTHOR);
|
||||
controller.getSettings().remove(MODPACK_DESCRIPTION);
|
||||
controller.getSettings().remove(MODPACK_INCLUDE_LAUNCHER);
|
||||
controller.getSettings().remove(MODPACK_FILE);
|
||||
controller.getSettings().remove(MODPACK_INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -126,19 +157,12 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
return new ModpackInfoPageSkin(this);
|
||||
}
|
||||
|
||||
public static final String MODPACK_NAME = "modpack.name";
|
||||
public static final String MODPACK_FILE_API = "modpack.file_api";
|
||||
public static final String MODPACK_VERSION = "modpack.version";
|
||||
public static final String MODPACK_AUTHOR = "archive.author";
|
||||
public static final String MODPACK_DESCRIPTION = "modpack.description";
|
||||
public static final String MODPACK_INCLUDE_LAUNCHER = "modpack.include_launcher";
|
||||
public static final String MODPACK_INFO = "modpack.info";
|
||||
public static final String MODPACK_FILE = "modpack.file";
|
||||
public static final String MODPACK_INFO_OPTION = "modpack.info.option";
|
||||
|
||||
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
|
||||
private final JFXTextField txtModpackName;
|
||||
private final JFXTextField txtModpackFileApi;
|
||||
private final JFXTextField txtModpackAuthor;
|
||||
private final JFXTextField txtModpackVersion;
|
||||
private ObservableList<Node> originList;
|
||||
|
||||
public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
|
||||
super(skinnable);
|
||||
@@ -151,6 +175,8 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
scroll.setFitToHeight(true);
|
||||
getChildren().setAll(scroll);
|
||||
|
||||
List<JFXTextField> validatingFields = new ArrayList<>();
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
borderPane.setStyle("-fx-padding: 16;");
|
||||
@@ -179,31 +205,37 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
|
||||
|
||||
Label versionNameLabel = new Label();
|
||||
versionNameLabel.textProperty().bind(skinnable.versionName);
|
||||
versionNameLabel.setText(skinnable.versionName);
|
||||
borderPane1.setRight(versionNameLabel);
|
||||
list.getContent().add(borderPane1);
|
||||
}
|
||||
|
||||
{
|
||||
txtModpackName = new JFXTextField();
|
||||
txtModpackName.textProperty().bindBidirectional(skinnable.modpackName);
|
||||
JFXTextField txtModpackName = new JFXTextField();
|
||||
txtModpackName.textProperty().bindBidirectional(skinnable.name);
|
||||
txtModpackName.setLabelFloat(true);
|
||||
txtModpackName.setPromptText(i18n("modpack.name"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
validator.setMessage(i18n("modpack.not_a_valid_name"));
|
||||
txtModpackName.getValidators().add(validator);
|
||||
txtModpackName.getValidators().add(new RequiredValidator(i18n("modpack.not_a_valid_name")));
|
||||
StackPane.setMargin(txtModpackName, insets);
|
||||
list.getContent().add(txtModpackName);
|
||||
|
||||
validatingFields.add(txtModpackName);
|
||||
}
|
||||
|
||||
if (skinnable.showFileApi) {
|
||||
txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.modpackFileApi);
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
JFXTextField txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
|
||||
txtModpackFileApi.setLabelFloat(true);
|
||||
txtModpackFileApi.setPromptText(i18n("modpack.file_api"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
txtModpackFileApi.getValidators().add(validator);
|
||||
|
||||
if (skinnable.options.isValidateFileApi()) {
|
||||
txtModpackFileApi.getValidators().add(new RequiredValidator());
|
||||
}
|
||||
|
||||
txtModpackFileApi.getValidators().add(new Validator(s -> {
|
||||
if (s.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
new URL(s).toURI();
|
||||
return true;
|
||||
@@ -213,35 +245,37 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
}));
|
||||
StackPane.setMargin(txtModpackFileApi, insets);
|
||||
list.getContent().add(txtModpackFileApi);
|
||||
} else {
|
||||
txtModpackFileApi = null;
|
||||
|
||||
validatingFields.add(txtModpackFileApi);
|
||||
}
|
||||
|
||||
{
|
||||
txtModpackAuthor = new JFXTextField();
|
||||
txtModpackAuthor.textProperty().bindBidirectional(skinnable.modpackAuthor);
|
||||
JFXTextField txtModpackAuthor = new JFXTextField();
|
||||
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
|
||||
txtModpackAuthor.setLabelFloat(true);
|
||||
txtModpackAuthor.setPromptText(i18n("archive.author"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
txtModpackAuthor.getValidators().add(validator);
|
||||
txtModpackAuthor.getValidators().add(new RequiredValidator());
|
||||
StackPane.setMargin(txtModpackAuthor, insets);
|
||||
list.getContent().add(txtModpackAuthor);
|
||||
|
||||
validatingFields.add(txtModpackAuthor);
|
||||
}
|
||||
|
||||
{
|
||||
txtModpackVersion = new JFXTextField();
|
||||
txtModpackVersion.textProperty().bindBidirectional(skinnable.modpackVersion);
|
||||
JFXTextField txtModpackVersion = new JFXTextField();
|
||||
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
|
||||
txtModpackVersion.setLabelFloat(true);
|
||||
txtModpackVersion.setPromptText(i18n("archive.version"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
txtModpackVersion.getValidators().add(validator);
|
||||
txtModpackVersion.getValidators().add(new RequiredValidator());
|
||||
StackPane.setMargin(txtModpackVersion, insets);
|
||||
list.getContent().add(txtModpackVersion);
|
||||
|
||||
validatingFields.add(txtModpackVersion);
|
||||
}
|
||||
|
||||
{
|
||||
JFXTextArea area = new JFXTextArea();
|
||||
area.textProperty().bindBidirectional(skinnable.modpackDescription);
|
||||
area.textProperty().bindBidirectional(skinnable.description);
|
||||
area.setLabelFloat(true);
|
||||
area.setPromptText(i18n("modpack.desc"));
|
||||
area.setMinHeight(400);
|
||||
@@ -249,18 +283,123 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
list.getContent().add(area);
|
||||
}
|
||||
|
||||
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_HMCL) {
|
||||
BorderPane borderPane1 = new BorderPane();
|
||||
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
|
||||
list.getContent().add(borderPane1);
|
||||
if (skinnable.options.isRequireForceUpdate()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
|
||||
list.getContent().add(pane);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.setDisable(!skinnable.canIncludeLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.includingLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
borderPane1.setRight(button);
|
||||
pane.setRight(button);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireAuthlibInjectorServer()) {
|
||||
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
|
||||
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
|
||||
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
|
||||
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
|
||||
|
||||
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() ->
|
||||
Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem())
|
||||
.map(AuthlibInjectorServer::getUrl)
|
||||
.orElse(null)));
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
Label left = new Label(i18n("account.injector.server"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
pane.setLeft(left);
|
||||
pane.setRight(cboServers);
|
||||
|
||||
list.getContent().add(pane);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireMinMemory()) {
|
||||
JFXTextField txtMinMemory = new JFXTextField();
|
||||
FXUtils.bindInt(txtMinMemory, skinnable.minMemory);
|
||||
txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false));
|
||||
FXUtils.setLimitWidth(txtMinMemory, 300);
|
||||
validatingFields.add(txtMinMemory);
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
Label label = new Label(i18n("settings.min_memory"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
pane.setLeft(label);
|
||||
BorderPane.setAlignment(txtMinMemory, Pos.CENTER_RIGHT);
|
||||
pane.setRight(txtMinMemory);
|
||||
|
||||
list.getContent().add(pane);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireLaunchArguments()) {
|
||||
JFXTextField txtLaunchArguments = new JFXTextField();
|
||||
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
|
||||
txtLaunchArguments.setLabelFloat(true);
|
||||
txtLaunchArguments.setPromptText(i18n("settings.advanced.minecraft_arguments"));
|
||||
StackPane.setMargin(txtLaunchArguments, insets);
|
||||
list.getContent().add(txtLaunchArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireJavaArguments()) {
|
||||
JFXTextField txtJavaArguments = new JFXTextField();
|
||||
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
|
||||
txtJavaArguments.setLabelFloat(true);
|
||||
txtJavaArguments.setPromptText(i18n("settings.advanced.jvm_args"));
|
||||
StackPane.setMargin(txtJavaArguments, insets);
|
||||
list.getContent().add(txtJavaArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins() || skinnable.options.isRequireUrl()) {
|
||||
BorderPane originPane = new BorderPane();
|
||||
Label title = new Label(i18n("modpack.origin"));
|
||||
originPane.setTop(title);
|
||||
VBox container = new VBox();
|
||||
BorderPane.setMargin(container, new Insets(0, 0, 0, 16));
|
||||
originPane.setCenter(container);
|
||||
list.getContent().add(originPane);
|
||||
|
||||
if (skinnable.options.isRequireUrl()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8, 0, 8, 0));
|
||||
Label left = new Label(i18n("modpack.origin.url"));
|
||||
pane.setLeft(left);
|
||||
JFXTextField txtModpackUrl = new JFXTextField();
|
||||
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
|
||||
pane.setRight(txtModpackUrl);
|
||||
container.getChildren().add(pane);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8, 0, 8, 0));
|
||||
Label left = new Label(i18n("modpack.origin.mcbbs"));
|
||||
pane.setLeft(left);
|
||||
JFXTextField txtMcbbs = new JFXTextField();
|
||||
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
|
||||
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
|
||||
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
|
||||
pane.setRight(txtMcbbs);
|
||||
container.getChildren().add(pane);
|
||||
|
||||
validatingFields.add(txtMcbbs);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
|
||||
list.getContent().add(pane);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.setDisable(!skinnable.canIncludeLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,15 +415,13 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
nextButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
nextButton.setText(i18n("wizard.next"));
|
||||
nextButton.getStyleClass().add("jfx-button-raised");
|
||||
if (skinnable.showFileApi) {
|
||||
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
|
||||
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate() || !txtModpackFileApi.validate(),
|
||||
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty(), txtModpackFileApi.textProperty()));
|
||||
} else {
|
||||
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
|
||||
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate(),
|
||||
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty()));
|
||||
}
|
||||
nextButton.disableProperty().bind(
|
||||
// Disable nextButton if any text of JFXTextFields in validatingFields does not fulfill
|
||||
// our requirement.
|
||||
Bindings.createBooleanBinding(() -> validatingFields.stream()
|
||||
.map(field -> !field.validate())
|
||||
.reduce(false, (left, right) -> left || right),
|
||||
validatingFields.stream().map(JFXTextField::textProperty).toArray(StringProperty[]::new)));
|
||||
hbox.getChildren().add(nextButton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,18 +20,23 @@ package org.jackhuang.hmcl.ui.export;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackInfoPage.MODPACK_INFO_OPTION;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackTypeSelectionPage extends StackPane implements WizardPage {
|
||||
private final WizardController controller;
|
||||
@FXML
|
||||
private JFXButton btnHMCL;
|
||||
private JFXButton btnMCBBS;
|
||||
@FXML
|
||||
private JFXButton btnMultiMC;
|
||||
@FXML
|
||||
@@ -41,12 +46,15 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
|
||||
this.controller = controller;
|
||||
FXUtils.loadFXML(this, "/assets/fxml/modpack/type.fxml");
|
||||
|
||||
JFXButton[] buttons = new JFXButton[]{btnHMCL, btnMultiMC, btnServer};
|
||||
String[] types = new String[]{MODPACK_TYPE_HMCL, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
|
||||
JFXButton[] buttons = new JFXButton[]{btnMCBBS, btnMultiMC, btnServer};
|
||||
String[] types = new String[]{MODPACK_TYPE_MCBBS, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
|
||||
ModpackExportInfo.Options[] options = new ModpackExportInfo.Options[]{McbbsModpackExportTask.OPTION, MultiMCModpackExportTask.OPTION, ServerModpackExportTask.OPTION};
|
||||
for (int i = 0; i < types.length; ++i) {
|
||||
String type = types[i];
|
||||
ModpackExportInfo.Options option = options[i];
|
||||
buttons[i].setOnMouseClicked(e -> {
|
||||
controller.getSettings().put(MODPACK_TYPE, type);
|
||||
controller.getSettings().put(MODPACK_INFO_OPTION, option);
|
||||
controller.onNext();
|
||||
});
|
||||
}
|
||||
@@ -63,7 +71,7 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
|
||||
|
||||
public static final String MODPACK_TYPE = "modpack.type";
|
||||
|
||||
public static final String MODPACK_TYPE_HMCL = "hmcl";
|
||||
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
|
||||
public static final String MODPACK_TYPE_MULTIMC = "multimc";
|
||||
public static final String MODPACK_TYPE_SERVER = "server";
|
||||
}
|
||||
@@ -115,6 +115,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
StackPane launchPane = new StackPane();
|
||||
launchPane.getStyleClass().add("launch-pane");
|
||||
launchPane.setMaxWidth(230);
|
||||
launchPane.setMaxHeight(55);
|
||||
launchPane.setOnScroll(event -> {
|
||||
@@ -134,8 +135,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
JFXButton launchButton = new JFXButton();
|
||||
launchButton.setPrefWidth(230);
|
||||
launchButton.setPrefHeight(55);
|
||||
launchButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
launchButton.getStyleClass().add("jfx-button-raised");
|
||||
//launchButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
launchButton.setOnAction(e -> launch());
|
||||
launchButton.setDefaultButton(true);
|
||||
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
|
||||
@@ -161,7 +161,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
separator.getStyleClass().add("darker-fill");
|
||||
separator.setWidth(1);
|
||||
separator.setHeight(57);
|
||||
separator.setTranslateX(95);
|
||||
@@ -170,13 +169,12 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
menuButton = new JFXButton();
|
||||
menuButton.setPrefHeight(55);
|
||||
menuButton.setPrefWidth(230);
|
||||
menuButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
menuButton.getStyleClass().add("jfx-button-raised");
|
||||
//menuButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
menuButton.setStyle("-fx-font-size: 15px;");
|
||||
menuButton.setOnMouseClicked(e -> onMenu());
|
||||
menuButton.setClip(new Rectangle(211, -100, 100, 200));
|
||||
StackPane graphic = new StackPane();
|
||||
Node svg = SVG.triangle(Theme.whiteFillBinding(), 10, 10);
|
||||
Node svg = SVG.triangle(Theme.foregroundFillBinding(), 10, 10);
|
||||
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
|
||||
graphic.getChildren().setAll(svg);
|
||||
graphic.setTranslateX(12);
|
||||
|
||||
@@ -18,11 +18,9 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
@@ -182,6 +180,34 @@ public class RootPage extends DecoratorTabPage {
|
||||
return profileListPage;
|
||||
}
|
||||
|
||||
public Tab getMainTab() {
|
||||
return mainTab;
|
||||
}
|
||||
|
||||
public Tab getSettingsTab() {
|
||||
return settingsTab;
|
||||
}
|
||||
|
||||
public Tab getGameTab() {
|
||||
return gameTab;
|
||||
}
|
||||
|
||||
public Tab getAccountTab() {
|
||||
return accountTab;
|
||||
}
|
||||
|
||||
public Tab getProfileTab() {
|
||||
return profileTab;
|
||||
}
|
||||
|
||||
private void selectPage(Tab tab) {
|
||||
if (getSelectionModel().getSelectedItem() == tab) {
|
||||
getSelectionModel().select(getMainTab());
|
||||
} else {
|
||||
getSelectionModel().select(tab);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Skin extends SkinBase<RootPage> {
|
||||
|
||||
protected Skin(RootPage control) {
|
||||
@@ -190,7 +216,7 @@ public class RootPage extends DecoratorTabPage {
|
||||
// first item in left sidebar
|
||||
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
|
||||
accountListItem.activeProperty().bind(control.accountTab.selectedProperty());
|
||||
accountListItem.setOnAction(e -> control.getSelectionModel().select(control.accountTab));
|
||||
accountListItem.setOnAction(e -> control.selectPage(control.accountTab));
|
||||
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
|
||||
|
||||
// second item in left sidebar
|
||||
@@ -200,7 +226,7 @@ public class RootPage extends DecoratorTabPage {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
String version = Profiles.getSelectedVersion();
|
||||
if (version == null) {
|
||||
control.getSelectionModel().select(control.gameTab);
|
||||
control.selectPage(control.gameTab);
|
||||
} else {
|
||||
Versions.modifyGameSettings(profile, version);
|
||||
}
|
||||
@@ -211,12 +237,12 @@ public class RootPage extends DecoratorTabPage {
|
||||
gameItem.activeProperty().bind(control.gameTab.selectedProperty());
|
||||
gameItem.setImage(newImage("/assets/img/bookshelf.png"));
|
||||
gameItem.setTitle(i18n("version.manage"));
|
||||
gameItem.setOnAction(e -> control.getSelectionModel().select(control.gameTab));
|
||||
gameItem.setOnAction(e -> control.selectPage(control.gameTab));
|
||||
|
||||
// forth item in left sidebar
|
||||
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
|
||||
profileListItem.activeProperty().bind(control.profileTab.selectedProperty());
|
||||
profileListItem.setOnAction(e -> control.getSelectionModel().select(control.profileTab));
|
||||
profileListItem.setOnAction(e -> control.selectPage(control.profileTab));
|
||||
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
|
||||
|
||||
// fifth item in left sidebar
|
||||
@@ -224,7 +250,7 @@ public class RootPage extends DecoratorTabPage {
|
||||
launcherSettingsItem.activeProperty().bind(control.settingsTab.selectedProperty());
|
||||
launcherSettingsItem.setImage(newImage("/assets/img/command.png"));
|
||||
launcherSettingsItem.setTitle(i18n("settings.launcher"));
|
||||
launcherSettingsItem.setOnAction(e -> control.getSelectionModel().select(control.settingsTab));
|
||||
launcherSettingsItem.setOnAction(e -> control.selectPage(control.settingsTab));
|
||||
|
||||
// the left sidebar
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
@@ -247,9 +273,7 @@ public class RootPage extends DecoratorTabPage {
|
||||
control.transitionPane.getStyleClass().add("jfx-decorator-content-container");
|
||||
control.transitionPane.getChildren().setAll(getSkinnable().getMainPage());
|
||||
FXUtils.setOverflowHidden(control.transitionPane, 8);
|
||||
StackPane wrapper = new StackPane(control.transitionPane);
|
||||
wrapper.setPadding(new Insets(4));
|
||||
root.setCenter(wrapper);
|
||||
root.setCenter(control.transitionPane);
|
||||
}
|
||||
|
||||
getChildren().setAll(root);
|
||||
@@ -258,7 +282,12 @@ public class RootPage extends DecoratorTabPage {
|
||||
}
|
||||
|
||||
// ==== Accounts ====
|
||||
|
||||
private boolean checkedAccont = false;
|
||||
|
||||
public void checkAccount() {
|
||||
if (checkedAccont) return;
|
||||
checkedAccont = true;
|
||||
if (Accounts.getAccounts().isEmpty())
|
||||
Platform.runLater(this::addNewAccount);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
@@ -63,7 +62,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
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 static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reservedSelectedPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reversedSelectedPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
|
||||
public final class SettingsPage extends SettingsView implements DecoratorPage {
|
||||
@@ -73,7 +72,6 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
|
||||
|
||||
public SettingsPage() {
|
||||
FXUtils.smoothScrolling(scroll);
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
|
||||
|
||||
// ==== Download sources ====
|
||||
cboDownloadSource.getItems().setAll(DownloadProviders.providersById.keySet());
|
||||
@@ -112,7 +110,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
|
||||
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
|
||||
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
|
||||
|
||||
reservedSelectedPropertyFor(chkDisableProxy).bindBidirectional(config().hasProxyProperty());
|
||||
reversedSelectedPropertyFor(chkDisableProxy).bindBidirectional(config().hasProxyProperty());
|
||||
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
|
||||
|
||||
ToggleGroup proxyConfigurationGroup = new ToggleGroup();
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.profile;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -75,7 +74,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
|
||||
root.setRight(right);
|
||||
|
||||
root.getStyleClass().add("card");
|
||||
root.setPadding(new Insets(8, 8, 8, 0));
|
||||
root.setStyle("-fx-padding: 8 8 8 0");
|
||||
JFXDepthManager.setDepth(root, 1);
|
||||
item.titleProperty().bind(skinnable.titleProperty());
|
||||
item.subtitleProperty().bind(skinnable.subtitleProperty());
|
||||
|
||||
@@ -132,7 +132,7 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
}
|
||||
|
||||
String getSubtitle() {
|
||||
return StringUtils.parseColorEscapes(packInfo.getDescription());
|
||||
return StringUtils.parseColorEscapes(packInfo.getDescription().toString());
|
||||
}
|
||||
|
||||
Datapack.Pack getPackInfo() {
|
||||
|
||||
@@ -32,16 +32,17 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
|
||||
public GameAdvancedListItem() {
|
||||
tooltip = new Tooltip();
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
|
||||
if (version != null && Profiles.getSelectedProfile() != null &&
|
||||
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
setTitle(version);
|
||||
setSubtitle(null);
|
||||
setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
|
||||
tooltip.setText(version);
|
||||
} else {
|
||||
Tooltip.uninstall(this,tooltip);
|
||||
setTitle(i18n("version.empty"));
|
||||
setSubtitle(i18n("version.empty.add"));
|
||||
setImage(newImage("/assets/img/grass.png"));
|
||||
|
||||
@@ -27,16 +27,20 @@ import javafx.scene.control.Skin;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameVersion;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -48,6 +52,7 @@ public class GameItem extends Control {
|
||||
private final Profile profile;
|
||||
private final String version;
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final StringProperty tag = new SimpleStringProperty();
|
||||
private final StringProperty subtitle = new SimpleStringProperty();
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
|
||||
|
||||
@@ -75,6 +80,17 @@ public class GameItem extends Control {
|
||||
}, Platform::runLater)
|
||||
.exceptionally(handleUncaught);
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(version);
|
||||
if (config == null) return;
|
||||
tag.set(config.getVersion());
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to read modpack configuration from ", e);
|
||||
}
|
||||
}, Platform::runLater)
|
||||
.exceptionally(handleUncaught);
|
||||
|
||||
title.set(id);
|
||||
image.set(profile.getRepository().getVersionIconImage(version));
|
||||
}
|
||||
@@ -96,6 +112,10 @@ public class GameItem extends Control {
|
||||
return title;
|
||||
}
|
||||
|
||||
public StringProperty tagProperty() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ public class GameItemSkin extends SkinBase<GameItem> {
|
||||
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.titleProperty().bind(skinnable.titleProperty());
|
||||
item.tagProperty().bind(skinnable.tagProperty());
|
||||
item.subtitleProperty().bind(skinnable.subtitleProperty());
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
center.getChildren().setAll(imageView, item);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.Node;
|
||||
@@ -30,7 +29,6 @@ import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider;
|
||||
@@ -57,7 +55,6 @@ public class GameList extends ListPageBase<GameListItem> implements DecoratorPag
|
||||
});
|
||||
|
||||
Profiles.registerVersionsListener(this::loadVersions);
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
|
||||
}
|
||||
|
||||
private void loadVersions(Profile profile) {
|
||||
@@ -99,21 +96,21 @@ public class GameList extends ListPageBase<GameListItem> implements DecoratorPag
|
||||
return new GameListSkin();
|
||||
}
|
||||
|
||||
void addNewGame() {
|
||||
public static void addNewGame() {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
if (profile.getRepository().isLoaded()) {
|
||||
Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(profile), i18n("install.new_game"));
|
||||
}
|
||||
}
|
||||
|
||||
void importModpack() {
|
||||
public static void importModpack() {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
if (profile.getRepository().isLoaded()) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile), i18n("install.modpack"));
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
public static void refreshList() {
|
||||
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start();
|
||||
}
|
||||
|
||||
@@ -132,9 +129,9 @@ public class GameList extends ListPageBase<GameListItem> implements DecoratorPag
|
||||
super(GameList.this);
|
||||
|
||||
HBox hbox = new HBox(
|
||||
createToolbarButton(i18n("install.new_game"), SVG::plus, GameList.this::addNewGame),
|
||||
createToolbarButton(i18n("install.modpack"), SVG::importIcon, GameList.this::importModpack),
|
||||
createToolbarButton(i18n("button.refresh"), SVG::refresh, GameList.this::refresh),
|
||||
createToolbarButton(i18n("install.new_game"), SVG::plus, GameList::addNewGame),
|
||||
createToolbarButton(i18n("install.modpack"), SVG::importIcon, GameList::importModpack),
|
||||
createToolbarButton(i18n("button.refresh"), SVG::refresh, GameList::refreshList),
|
||||
createToolbarButton(i18n("settings.type.global.manage"), SVG::gear, GameList.this::modifyGlobalGameSettings)
|
||||
);
|
||||
hbox.setPickOnBounds(false);
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.SkinBase;
|
||||
@@ -87,19 +86,31 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||
right.getChildren().add(btnUpgrade);
|
||||
}
|
||||
|
||||
JFXButton btnManage = new JFXButton();
|
||||
btnManage.setOnMouseClicked(e -> {
|
||||
currentSkinnable = skinnable;
|
||||
popup.show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
|
||||
});
|
||||
btnManage.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnManage, Pos.CENTER);
|
||||
btnManage.setGraphic(SVG.dotsVertical(Theme.blackFillBinding(), -1, -1));
|
||||
right.getChildren().add(btnManage);
|
||||
{
|
||||
JFXButton btnLaunch = new JFXButton();
|
||||
btnLaunch.setOnMouseClicked(e -> skinnable.launch());
|
||||
btnLaunch.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
|
||||
btnLaunch.setGraphic(SVG.launch(Theme.blackFillBinding(), 20, 20));
|
||||
right.getChildren().add(btnLaunch);
|
||||
}
|
||||
|
||||
{
|
||||
JFXButton btnManage = new JFXButton();
|
||||
btnManage.setOnMouseClicked(e -> {
|
||||
currentSkinnable = skinnable;
|
||||
popup.show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
|
||||
});
|
||||
btnManage.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnManage, Pos.CENTER);
|
||||
btnManage.setGraphic(SVG.dotsVertical(Theme.blackFillBinding(), -1, -1));
|
||||
right.getChildren().add(btnManage);
|
||||
}
|
||||
|
||||
root.setRight(right);
|
||||
|
||||
root.getStyleClass().add("card");
|
||||
root.setPadding(new Insets(8, 8, 8, 0));
|
||||
root.setStyle("-fx-padding: 8 8 8 0");
|
||||
JFXDepthManager.setDepth(root, 1);
|
||||
|
||||
getChildren().setAll(root);
|
||||
|
||||
@@ -39,7 +39,6 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
@@ -74,9 +73,9 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
|
||||
return LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(versionId));
|
||||
}).thenAcceptAsync(analyzer -> {
|
||||
Function<String, Consumer<InstallerItem>> removeAction = libraryId -> x -> {
|
||||
Function<String, Runnable> removeAction = libraryId -> () -> {
|
||||
profile.getDependency().removeLibraryAsync(version, libraryId)
|
||||
.thenComposeAsync(profile.getRepository()::save)
|
||||
.thenComposeAsync(profile.getRepository()::saveAsync)
|
||||
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
|
||||
.start();
|
||||
@@ -84,15 +83,28 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
|
||||
itemsProperty().clear();
|
||||
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
String libraryId = type.getPatchId();
|
||||
String libraryVersion = analyzer.getVersion(type).orElse(null);
|
||||
Consumer<InstallerItem> action = "game".equals(libraryId) || libraryVersion == null ? null : removeAction.apply(libraryId);
|
||||
itemsProperty().add(new InstallerItem(libraryId, libraryVersion, () -> {
|
||||
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup();
|
||||
|
||||
// Conventional libraries: game, fabric, forge, liteloader, optifine
|
||||
for (InstallerItem installerItem : group.getLibraries()) {
|
||||
String libraryId = installerItem.getLibraryId();
|
||||
String libraryVersion = analyzer.getVersion(libraryId).orElse(null);
|
||||
installerItem.libraryVersion.set(libraryVersion);
|
||||
installerItem.upgradable.set(libraryVersion != null);
|
||||
installerItem.installable.set(true);
|
||||
installerItem.action.set(e -> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
|
||||
}, action));
|
||||
});
|
||||
boolean removable = !"game".equals(libraryId) && libraryVersion != null;
|
||||
installerItem.removable.set(removable);
|
||||
if (removable) {
|
||||
Runnable action = removeAction.apply(libraryId);
|
||||
installerItem.removeAction.set(e -> action.run());
|
||||
}
|
||||
itemsProperty().add(installerItem);
|
||||
}
|
||||
|
||||
// other third-party libraries which are unable to manage.
|
||||
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||
String libraryId = mark.getLibraryId();
|
||||
String libraryVersion = mark.getLibraryVersion();
|
||||
@@ -101,14 +113,23 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
if (LibraryAnalyzer.LibraryType.fromPatchId(libraryId) != null)
|
||||
continue;
|
||||
|
||||
Consumer<InstallerItem> action = removeAction.apply(libraryId);
|
||||
if (libraryVersion != null && Lang.test(() -> profile.getDependency().getVersionList(libraryId)))
|
||||
itemsProperty().add(
|
||||
new InstallerItem(libraryId, libraryVersion, () -> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
|
||||
}, action));
|
||||
else
|
||||
itemsProperty().add(new InstallerItem(libraryId, libraryVersion, null, action));
|
||||
Runnable action = removeAction.apply(libraryId);
|
||||
|
||||
InstallerItem installerItem = new InstallerItem(libraryId);
|
||||
installerItem.libraryVersion.set(libraryVersion);
|
||||
installerItem.installable.set(false);
|
||||
installerItem.upgradable.bind(installerItem.installable);
|
||||
installerItem.removable.set(true);
|
||||
installerItem.removeAction.set(e -> action.run());
|
||||
|
||||
if (libraryVersion != null && Lang.test(() -> profile.getDependency().getVersionList(libraryId))) {
|
||||
installerItem.installable.set(true);
|
||||
installerItem.action.set(e -> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
|
||||
});
|
||||
}
|
||||
|
||||
itemsProperty().add(installerItem);
|
||||
}
|
||||
}, Platform::runLater);
|
||||
}
|
||||
@@ -122,7 +143,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
|
||||
private void doInstallOffline(File file) {
|
||||
Task<?> task = profile.getDependency().installLibraryAsync(version, file.toPath())
|
||||
.thenComposeAsync(profile.getRepository()::save)
|
||||
.thenComposeAsync(profile.getRepository()::saveAsync)
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
|
||||
task.setName(i18n("install.installer.install_offline"));
|
||||
TaskExecutor executor = task.executor(new TaskListener() {
|
||||
|
||||
@@ -23,7 +23,6 @@ import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
@@ -87,13 +86,12 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
{
|
||||
|
||||
Region clippedContainer = (Region)listView.lookup(".clipped-container");
|
||||
setPrefWidth(0);
|
||||
HBox container = new HBox(8);
|
||||
container.setPadding(new Insets(0, 0, 0, 6));
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
pane.getChildren().add(container);
|
||||
pane.setPadding(new Insets(8, 8, 8, 0));
|
||||
if (clippedContainer != null) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
|
||||
@@ -17,16 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListCell;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Callback;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
@@ -35,6 +36,7 @@ import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.ToolbarListPageSkin;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
@@ -117,9 +119,19 @@ public class VersionPage extends Control implements DecoratorPage {
|
||||
public void setVersion(String version, Profile profile) {
|
||||
this.version = version;
|
||||
this.profile = profile;
|
||||
|
||||
profile.setSelectedVersion(version);
|
||||
}
|
||||
|
||||
public void loadVersion(String version, Profile profile) {
|
||||
// If we jumped to game list page and deleted this version
|
||||
// and back to this page, we should return to main page.
|
||||
if (!this.profile.getRepository().isLoaded() ||
|
||||
!this.profile.getRepository().hasVersion(version)) {
|
||||
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
|
||||
return;
|
||||
}
|
||||
|
||||
setVersion(version, profile);
|
||||
preferredVersionName = version;
|
||||
listView.getSelectionModel().select(version);
|
||||
@@ -244,7 +256,6 @@ public class VersionPage extends Control implements DecoratorPage {
|
||||
}, listViewItemPopup))
|
||||
);
|
||||
|
||||
FXUtils.smoothScrolling(control.listView);
|
||||
control.listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
control.listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
|
||||
@Override
|
||||
@@ -268,15 +279,24 @@ public class VersionPage extends Control implements DecoratorPage {
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
{
|
||||
StackPane drawerContainer = new StackPane();
|
||||
FXUtils.setLimitWidth(drawerContainer, 200);
|
||||
drawerContainer.getStyleClass().add("gray-background");
|
||||
drawerContainer.getChildren().setAll(control.listView);
|
||||
FXUtils.setOverflowHidden(drawerContainer, 8);
|
||||
BorderPane left = new BorderPane();
|
||||
FXUtils.setLimitWidth(left, 200);
|
||||
left.setCenter(control.listView);
|
||||
root.setLeft(left);
|
||||
|
||||
StackPane wrapper = new StackPane(drawerContainer);
|
||||
wrapper.setPadding(new Insets(4, 0, 4, 4));
|
||||
root.setLeft(wrapper);
|
||||
JFXButton btnAddGame = ToolbarListPageSkin.createToolbarButton(null, SVG::plus, GameList::addNewGame);
|
||||
FXUtils.installFastTooltip(btnAddGame, new Tooltip(i18n("install.new_game")));
|
||||
|
||||
JFXButton btnImportModpack = ToolbarListPageSkin.createToolbarButton(null, SVG::importIcon, GameList::importModpack);
|
||||
FXUtils.installFastTooltip(btnImportModpack, new Tooltip(i18n("install.modpack")));
|
||||
|
||||
JFXButton btnRefresh = ToolbarListPageSkin.createToolbarButton(null, SVG::refresh, GameList::refreshList);
|
||||
FXUtils.installFastTooltip(btnRefresh, new Tooltip(i18n("button.refresh")));
|
||||
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.setPickOnBounds(false);
|
||||
toolbar.getChildren().setAll(btnAddGame, btnImportModpack, btnRefresh);
|
||||
left.setTop(toolbar);
|
||||
}
|
||||
|
||||
TabHeader tabPane = new TabHeader();
|
||||
@@ -328,9 +348,7 @@ public class VersionPage extends Control implements DecoratorPage {
|
||||
);
|
||||
|
||||
JFXButton upgradeButton = new JFXButton();
|
||||
FXUtils.setLimitWidth(upgradeButton, 40);
|
||||
FXUtils.setLimitHeight(upgradeButton, 40);
|
||||
upgradeButton.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
|
||||
upgradeButton.setGraphic(SVG.update(null, 20, 20));
|
||||
upgradeButton.getStyleClass().add("jfx-decorator-button");
|
||||
upgradeButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
upgradeButton.setOnAction(event -> control.updateGame());
|
||||
@@ -338,18 +356,14 @@ public class VersionPage extends Control implements DecoratorPage {
|
||||
FXUtils.installFastTooltip(upgradeButton, i18n("version.update"));
|
||||
|
||||
JFXButton testGameButton = new JFXButton();
|
||||
FXUtils.setLimitWidth(testGameButton, 40);
|
||||
FXUtils.setLimitHeight(testGameButton, 40);
|
||||
testGameButton.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20));
|
||||
testGameButton.setGraphic(SVG.launch(null, 20, 20));
|
||||
testGameButton.getStyleClass().add("jfx-decorator-button");
|
||||
testGameButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
testGameButton.setOnAction(event -> control.testGame());
|
||||
FXUtils.installFastTooltip(testGameButton, i18n("version.launch.test"));
|
||||
|
||||
JFXButton browseMenuButton = new JFXButton();
|
||||
FXUtils.setLimitWidth(browseMenuButton, 40);
|
||||
FXUtils.setLimitHeight(browseMenuButton, 40);
|
||||
browseMenuButton.setGraphic(SVG.folderOpen(Theme.whiteFillBinding(), 20, 20));
|
||||
browseMenuButton.setGraphic(SVG.folderOpen(null, 20, 20));
|
||||
browseMenuButton.getStyleClass().add("jfx-decorator-button");
|
||||
browseMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
browseMenuButton.setOnAction(event -> browsePopup.show(browseMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, browseMenuButton.getHeight()));
|
||||
@@ -357,8 +371,8 @@ public class VersionPage extends Control implements DecoratorPage {
|
||||
|
||||
JFXButton managementMenuButton = new JFXButton();
|
||||
FXUtils.setLimitWidth(managementMenuButton, 40);
|
||||
FXUtils.setLimitHeight(managementMenuButton, 40);;
|
||||
managementMenuButton.setGraphic(SVG.wrench(Theme.whiteFillBinding(), 20, 20));
|
||||
FXUtils.setLimitHeight(managementMenuButton, 40);
|
||||
managementMenuButton.setGraphic(SVG.wrench(null, 20, 20));
|
||||
managementMenuButton.getStyleClass().add("jfx-decorator-button");
|
||||
managementMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
managementMenuButton.setOnAction(event -> managementPopup.show(managementMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, managementMenuButton.getHeight()));
|
||||
@@ -370,13 +384,11 @@ public class VersionPage extends Control implements DecoratorPage {
|
||||
BorderPane titleBar = new BorderPane();
|
||||
titleBar.setLeft(tabPane);
|
||||
titleBar.setRight(toolBar);
|
||||
control.state.set(new State(i18n("version.manage.manage"), titleBar, true, false, true));
|
||||
control.state.set(new State(i18n("version.manage.manage"), titleBar, true, false, true, false, 200));
|
||||
|
||||
control.transitionPane.getStyleClass().add("gray-background");
|
||||
FXUtils.setOverflowHidden(control.transitionPane, 8);
|
||||
StackPane wrapper = new StackPane(control.transitionPane);
|
||||
wrapper.setPadding(new Insets(4));
|
||||
root.setCenter(wrapper);
|
||||
//control.transitionPane.getStyleClass().add("gray-background");
|
||||
//FXUtils.setOverflowHidden(control.transitionPane, 8);
|
||||
root.setCenter(control.transitionPane);
|
||||
|
||||
spinnerPane.loadingProperty().bind(control.loading);
|
||||
spinnerPane.setContent(root);
|
||||
|
||||
@@ -19,16 +19,17 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
@@ -59,8 +60,12 @@ public class Versions {
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> renameVersion(Profile profile, String version) {
|
||||
return Controllers.prompt(i18n("version.manage.rename.message"), (res, resolve, reject) -> {
|
||||
if (profile.getRepository().renameVersion(version, res)) {
|
||||
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, resolve, reject) -> {
|
||||
if (!OperatingSystem.isNameValid(newName)) {
|
||||
reject.accept(i18n("install.new_game.malformed"));
|
||||
return;
|
||||
}
|
||||
if (profile.getRepository().renameVersion(version, newName)) {
|
||||
profile.getRepository().refreshVersionsAsync().start();
|
||||
resolve.run();
|
||||
} else {
|
||||
@@ -150,7 +155,9 @@ public class Versions {
|
||||
if (Accounts.getSelectedAccount() == null)
|
||||
Controllers.getRootPage().checkAccount();
|
||||
else if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id))
|
||||
Controllers.dialog(i18n("version.empty.launch"));
|
||||
Controllers.dialog(i18n("version.empty.launch"), i18n("launch.failed"), MessageDialogPane.MessageType.ERROR, () -> {
|
||||
Controllers.getRootPage().getSelectionModel().select(Controllers.getRootPage().getGameTab());
|
||||
});
|
||||
else
|
||||
return true;
|
||||
return false;
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -85,7 +84,7 @@ public class WorldListItemSkin extends SkinBase<WorldListItem> {
|
||||
root.setRight(right);
|
||||
|
||||
root.getStyleClass().add("card");
|
||||
root.setPadding(new Insets(8, 8, 8, 0));
|
||||
root.setStyle("-fx-padding: 8 8 8 0");
|
||||
JFXDepthManager.setDepth(root, 1);
|
||||
|
||||
getChildren().setAll(root);
|
||||
|
||||
@@ -43,6 +43,7 @@ import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -69,7 +70,7 @@ public class WorldListPage extends ListPageBase<WorldListItem> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ToolbarListPageSkin createDefaultSkin() {
|
||||
protected ToolbarListPageSkin<WorldListPage> createDefaultSkin() {
|
||||
return new WorldListPageSkin();
|
||||
}
|
||||
|
||||
@@ -87,7 +88,11 @@ public class WorldListPage extends ListPageBase<WorldListItem> {
|
||||
setLoading(true);
|
||||
return CompletableFuture
|
||||
.runAsync(() -> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse(null))
|
||||
.thenApplyAsync(unused -> World.getWorlds(savesDir).parallel().collect(Collectors.toList()))
|
||||
.thenApplyAsync(unused -> {
|
||||
try (Stream<World> stream = World.getWorlds(savesDir)) {
|
||||
return stream.parallel().collect(Collectors.toList());
|
||||
}
|
||||
})
|
||||
.whenCompleteAsync((result, exception) -> {
|
||||
worlds = result;
|
||||
setLoading(false);
|
||||
|
||||
@@ -26,32 +26,34 @@ import org.jackhuang.hmcl.ui.construct.TaskListPane;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
|
||||
public interface AbstractWizardDisplayer extends WizardDisplayer {
|
||||
WizardController getWizardController();
|
||||
public abstract class AbstractWizardDisplayer implements WizardDisplayer {
|
||||
private final Queue<Object> cancelQueue;
|
||||
|
||||
Queue<Object> getCancelQueue();
|
||||
public AbstractWizardDisplayer(Queue<Object> cancelQueue) {
|
||||
this.cancelQueue = cancelQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
default void handleTask(Map<String, Object> settings, Task<?> task) {
|
||||
public void handleTask(Map<String, Object> settings, Task<?> task) {
|
||||
TaskExecutor executor = task.withRunAsync(Schedulers.javafx(), this::navigateToSuccess).executor();
|
||||
TaskListPane pane = new TaskListPane();
|
||||
pane.setExecutor(executor);
|
||||
navigateTo(pane, Navigation.NavigationDirection.FINISH);
|
||||
getCancelQueue().add(executor);
|
||||
cancelQueue.add(executor);
|
||||
executor.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
default void onCancel() {
|
||||
while (!getCancelQueue().isEmpty()) {
|
||||
Object x = getCancelQueue().poll();
|
||||
public void onCancel() {
|
||||
while (!cancelQueue.isEmpty()) {
|
||||
Object x = cancelQueue.poll();
|
||||
if (x instanceof TaskExecutor) ((TaskExecutor) x).cancel();
|
||||
else if (x instanceof Thread) ((Thread) x).interrupt();
|
||||
else throw new IllegalStateException("Unrecognized cancel queue element: " + x);
|
||||
}
|
||||
}
|
||||
|
||||
default void navigateToSuccess() {
|
||||
void navigateToSuccess() {
|
||||
navigateTo(new Label("Successful"), Navigation.NavigationDirection.FINISH);
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.wizard;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXToolbar;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class DefaultWizardDisplayer extends StackPane implements AbstractWizardDisplayer {
|
||||
|
||||
private final String prefix;
|
||||
private final WizardController wizardController;
|
||||
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private Node nowPage;
|
||||
|
||||
@FXML
|
||||
private TransitionPane root;
|
||||
@FXML
|
||||
private JFXButton backButton;
|
||||
@FXML
|
||||
private JFXToolbar toolbar;
|
||||
@FXML
|
||||
private JFXButton refreshButton;
|
||||
@FXML
|
||||
private Label titleLabel;
|
||||
|
||||
public DefaultWizardDisplayer(String prefix, WizardProvider wizardProvider) {
|
||||
this.prefix = prefix;
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/wizard.fxml");
|
||||
toolbar.setEffect(null);
|
||||
|
||||
wizardController = new WizardController(this);
|
||||
wizardController.setProvider(wizardProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WizardController getWizardController() {
|
||||
return wizardController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Queue<Object> getCancelQueue() {
|
||||
return cancelQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnd() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
|
||||
backButton.setDisable(!wizardController.canPrev());
|
||||
root.setContent(page, nav.getAnimation().getAnimationProducer());
|
||||
String title = StringUtils.isBlank(prefix) ? "" : prefix + " - ";
|
||||
if (page instanceof WizardPage)
|
||||
titleLabel.setText(title + ((WizardPage) page).getTitle());
|
||||
refreshButton.setVisible(page instanceof Refreshable);
|
||||
nowPage = page;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
wizardController.onStart();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void back() {
|
||||
wizardController.onPrev(true);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void close() {
|
||||
wizardController.onCancel();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void refresh() {
|
||||
((Refreshable) nowPage).refresh();
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -28,14 +28,19 @@ import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplayer {
|
||||
public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplayer {
|
||||
|
||||
public TaskExecutorDialogWizardDisplayer(Queue<Object> cancelQueue) {
|
||||
super(cancelQueue);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void handleTask(Map<String, Object> settings, Task<?> task) {
|
||||
public void handleTask(Map<String, Object> settings, Task<?> task) {
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(it -> {
|
||||
it.fireEvent(new DialogCloseEvent());
|
||||
onEnd();
|
||||
@@ -51,7 +56,7 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
|
||||
}
|
||||
|
||||
runInFX(() -> {
|
||||
TaskExecutor executor = task.cancellableExecutor(new TaskListener() {
|
||||
TaskExecutor executor = task.executor(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
runInFX(() -> {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.wizard;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -27,6 +28,7 @@ public class WizardController implements Navigation {
|
||||
private WizardProvider provider = null;
|
||||
private final Map<String, Object> settings = new HashMap<>();
|
||||
private final Stack<Node> pages = new Stack<>();
|
||||
private boolean stopped = false;
|
||||
|
||||
public WizardController(WizardDisplayer displayer) {
|
||||
this.displayer = displayer;
|
||||
@@ -59,10 +61,16 @@ public class WizardController implements Navigation {
|
||||
Node page = navigatingTo(0);
|
||||
pages.push(page);
|
||||
|
||||
if (stopped) { // navigatingTo may stop this wizard.
|
||||
return;
|
||||
}
|
||||
|
||||
if (page instanceof WizardPage)
|
||||
((WizardPage) page).onNavigate(settings);
|
||||
|
||||
displayer.onStart();
|
||||
|
||||
Logging.LOG.info("Navigating to " + page + ", pages: " + pages);
|
||||
displayer.navigateTo(page, NavigationDirection.START);
|
||||
}
|
||||
|
||||
@@ -74,9 +82,14 @@ public class WizardController implements Navigation {
|
||||
public void onNext(Node page) {
|
||||
pages.push(page);
|
||||
|
||||
if (stopped) { // navigatingTo may stop this wizard.
|
||||
return;
|
||||
}
|
||||
|
||||
if (page instanceof WizardPage)
|
||||
((WizardPage) page).onNavigate(settings);
|
||||
|
||||
Logging.LOG.info("Navigating to " + page + ", pages: " + pages);
|
||||
displayer.navigateTo(page, NavigationDirection.NEXT);
|
||||
}
|
||||
|
||||
@@ -99,6 +112,7 @@ public class WizardController implements Navigation {
|
||||
if (prevPage instanceof WizardPage)
|
||||
((WizardPage) prevPage).onNavigate(settings);
|
||||
|
||||
Logging.LOG.info("Navigating to " + prevPage + ", pages: " + pages);
|
||||
displayer.navigateTo(prevPage, NavigationDirection.PREVIOUS);
|
||||
}
|
||||
|
||||
@@ -117,6 +131,7 @@ public class WizardController implements Navigation {
|
||||
|
||||
@Override
|
||||
public void onEnd() {
|
||||
stopped = true;
|
||||
settings.clear();
|
||||
pages.clear();
|
||||
displayer.onEnd();
|
||||
|
||||
@@ -23,9 +23,16 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import java.util.Map;
|
||||
|
||||
public interface WizardDisplayer {
|
||||
void onStart();
|
||||
void onEnd();
|
||||
void onCancel();
|
||||
default void onStart() {
|
||||
}
|
||||
|
||||
default void onEnd() {
|
||||
}
|
||||
|
||||
default void onCancel() {
|
||||
}
|
||||
|
||||
void navigateTo(Node page, Navigation.NavigationDirection nav);
|
||||
|
||||
void handleTask(Map<String, Object> settings, Task<?> task);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
@@ -37,7 +38,7 @@ public class RemoteVersion {
|
||||
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
|
||||
String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null);
|
||||
String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null);
|
||||
if (packXZUrl != null && packXZHash != null) {
|
||||
if (SystemUtils.JRE_CAPABILITY_PACK200 && packXZUrl != null && packXZHash != null) {
|
||||
return new RemoteVersion(version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash));
|
||||
} else if (jarUrl != null && jarHash != null) {
|
||||
return new RemoteVersion(version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash));
|
||||
|
||||
@@ -199,7 +199,7 @@ public final class UpdateHandler {
|
||||
StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
|
||||
for (int i = 0; i < stacktrace.length; i++) {
|
||||
StackTraceElement element = stacktrace[i];
|
||||
if (Main.class.getName().equals(element.getClassName())) {
|
||||
if (Main.class.getName().equals(element.getClassName()) && "main".equals(element.getMethodName())) {
|
||||
// we've reached the main method
|
||||
return i + 1 != stacktrace.length;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utility for Adding JavaFX to module path.
|
||||
*
|
||||
* @author ZekerZhayard
|
||||
*/
|
||||
public class JavaFXPatcher {
|
||||
public static void patch(Set<String> modules, Path... jarPaths) {
|
||||
// Nothing to do with Java 8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.lang.Class.forName;
|
||||
import static org.jackhuang.hmcl.Metadata.HMCL_DIRECTORY;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.platform.JavaVersion.CURRENT_JAVA;
|
||||
|
||||
/**
|
||||
* Utility for patching self when missing dependencies.
|
||||
* Copy from https://github.com/Col-E/Recaf/blob/master/src/main/java/me/coley/recaf/util/self/SelfDependencyPatcher.java
|
||||
*
|
||||
* @author Matt
|
||||
*/
|
||||
public class SelfDependencyPatcher {
|
||||
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies");
|
||||
private static final String DEFAULT_JFX_VERSION = "16";
|
||||
private static final Map<String, String> JFX_DEPENDENCIES = new HashMap<>();
|
||||
|
||||
static {
|
||||
addJfxDependency("base");
|
||||
addJfxDependency("controls");
|
||||
addJfxDependency("fxml");
|
||||
addJfxDependency("graphics");
|
||||
addJfxDependency("media");
|
||||
// Fix #874: Remove the dependency on javafx.swing
|
||||
// addJfxDependency("swing");
|
||||
addJfxDependency("web");
|
||||
}
|
||||
|
||||
private static void addJfxDependency(String name) {
|
||||
JFX_DEPENDENCIES.put("javafx." + name, jfxUrl(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch in any missing dependencies, if any.
|
||||
*/
|
||||
public static void patch() throws PatchException, IncompatibleVersionException {
|
||||
// Do nothing if JavaFX is detected
|
||||
try {
|
||||
try {
|
||||
forName("javafx.application.Application");
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} catch (UnsupportedClassVersionError error) {
|
||||
// Loading the JavaFX class was unsupported.
|
||||
// We are probably on 8 and its on 11
|
||||
throw new IncompatibleVersionException();
|
||||
}
|
||||
// So the problem with Java 8 is that some distributions DO NOT BUNDLE JAVAFX
|
||||
// Why is this a problem? OpenJFX does not come in public bundles prior to Java 11
|
||||
// So you're out of luck unless you change your JDK or update Java.
|
||||
if (CURRENT_JAVA.getParsedVersion() < 11) {
|
||||
throw new IncompatibleVersionException();
|
||||
}
|
||||
|
||||
// We can only self-patch JavaFX on x86-64 platform.
|
||||
// For ARM support, user's manual patch is required.
|
||||
if (Architecture.CURRENT != Architecture.X86_64) {
|
||||
throw new IncompatibleVersionException();
|
||||
}
|
||||
|
||||
// Otherwise we're free to download in Java 11+
|
||||
LOG.info("Missing JavaFX dependencies, attempting to patch in missing classes");
|
||||
// Check if dependencies need to be downloaded
|
||||
if (!hasCachedDependencies()) {
|
||||
LOG.info(" - No local cache, downloading dependencies...");
|
||||
try {
|
||||
fetchDependencies();
|
||||
} catch (Exception ex) {
|
||||
throw new PatchException("Failed to download dependencies", ex);
|
||||
}
|
||||
} else {
|
||||
LOG.info(" - Local cache found!");
|
||||
}
|
||||
// Add the dependencies
|
||||
try {
|
||||
loadFromCache();
|
||||
} catch (IOException ex) {
|
||||
throw new PatchException("Failed to load JavaFX cache", ex);
|
||||
} catch (ReflectiveOperationException | NoClassDefFoundError ex) {
|
||||
throw new PatchException("Failed to add dependencies to classpath!", ex);
|
||||
}
|
||||
LOG.info(" - Done!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject them into the current classpath.
|
||||
*
|
||||
* @throws IOException
|
||||
* When the locally cached dependency urls cannot be resolved.
|
||||
* @throws ReflectiveOperationException
|
||||
* When the call to add these urls to the system classpath failed.
|
||||
*/
|
||||
private static void loadFromCache() throws IOException, ReflectiveOperationException {
|
||||
LOG.info(" - Loading dependencies...");
|
||||
List<Path> jarPaths = new ArrayList<>();
|
||||
List<String> jfxDepFile = JFX_DEPENDENCIES.values().stream().map(SelfDependencyPatcher::getFileName).collect(Collectors.toList());
|
||||
Files.walk(DEPENDENCIES_DIR_PATH).filter(p -> jfxDepFile.contains(p.getFileName().toString())).forEach(jarPaths::add);
|
||||
JavaFXPatcher.patch(JFX_DEPENDENCIES.keySet(), jarPaths.toArray(new Path[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Download dependencies.
|
||||
*
|
||||
* @throws IOException When the files cannot be fetched or saved.
|
||||
*/
|
||||
private static void fetchDependencies() throws Exception {
|
||||
// Get dir to store dependencies in
|
||||
Path dependenciesDir = DEPENDENCIES_DIR_PATH;
|
||||
if (!Files.isDirectory(dependenciesDir)) {
|
||||
Files.createDirectories(dependenciesDir);
|
||||
}
|
||||
ProgressFrame dialog = new ProgressFrame(i18n("download.javafx"));
|
||||
|
||||
ForkJoinTask<Void> task = ForkJoinPool.commonPool().submit(() -> {
|
||||
// Download each dependency
|
||||
Collection<String> dependencies = JFX_DEPENDENCIES.values();
|
||||
int i = 1;
|
||||
for (String dependencyUrlPath : dependencies) {
|
||||
URL depURL = new URL(dependencyUrlPath);
|
||||
Path dependencyFilePath = DEPENDENCIES_DIR_PATH.resolve(getFileName(dependencyUrlPath));
|
||||
int finalI = i;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dialog.setStatus(dependencyUrlPath);
|
||||
dialog.setProgress(finalI, dependencies.size());
|
||||
});
|
||||
Files.copy(depURL.openStream(), dependencyFilePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
checksum(dependencyFilePath, dependencyUrlPath);
|
||||
i++;
|
||||
}
|
||||
|
||||
dialog.dispose();
|
||||
return null;
|
||||
});
|
||||
|
||||
dialog.setVisible(true);
|
||||
|
||||
task.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} when the dependencies directory has files in it.
|
||||
*/
|
||||
private static boolean hasCachedDependencies() {
|
||||
if (!Files.isDirectory(DEPENDENCIES_DIR_PATH))
|
||||
return false;
|
||||
|
||||
for (String url : JFX_DEPENDENCIES.values()) {
|
||||
Path dependencyFilePath = DEPENDENCIES_DIR_PATH.resolve(getFileName(url));
|
||||
if (!Files.exists(dependencyFilePath))
|
||||
return false;
|
||||
|
||||
try {
|
||||
checksum(dependencyFilePath, url);
|
||||
} catch (ChecksumMismatchException e) {
|
||||
return false;
|
||||
} catch (IOException ignored) {
|
||||
// Ignore other situations
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void checksum(Path dependencyFilePath, String dependencyUrlPath) throws IOException {
|
||||
String expectedHash = NetworkUtils.doGet(NetworkUtils.toURL(dependencyUrlPath + ".sha1"));
|
||||
String actualHash = Hex.encodeHex(DigestUtils.digest("SHA-1", dependencyFilePath));
|
||||
if (!expectedHash.equalsIgnoreCase(actualHash)) {
|
||||
throw new ChecksumMismatchException("SHA-1", expectedHash, actualHash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url Full url path.
|
||||
* @return Name of file at url.
|
||||
*/
|
||||
private static String getFileName(String url) {
|
||||
return url.substring(url.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param component Name of the component.
|
||||
* @return Formed URL for the component.
|
||||
*/
|
||||
private static String jfxUrl(String component) {
|
||||
return jfxUrl(component, DEFAULT_JFX_VERSION);
|
||||
}
|
||||
|
||||
private static String jfxUrl(String component, String version) {
|
||||
// https://repo1.maven.org/maven2/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar
|
||||
return String.format("https://maven.aliyun.com/repository/central/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar",
|
||||
component, version, component, version, getMvnName());
|
||||
// return String.format("https://bmclapi.bangbang93.com/maven/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar",
|
||||
// component, version, component, version, getMvnName());
|
||||
}
|
||||
|
||||
private static String getMvnName() {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case LINUX:
|
||||
return "linux";
|
||||
case OSX:
|
||||
return "mac";
|
||||
default:
|
||||
return "win";
|
||||
}
|
||||
}
|
||||
|
||||
public static class PatchException extends Exception {
|
||||
PatchException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IncompatibleVersionException extends Exception {
|
||||
}
|
||||
|
||||
public static class ProgressFrame extends JDialog {
|
||||
private int totalTasks = 0;
|
||||
private int finishedTasks = 0;
|
||||
|
||||
private final JProgressBar progressBar;
|
||||
private final JLabel progressText;
|
||||
|
||||
public ProgressFrame(String title) {
|
||||
super((Dialog) null);
|
||||
|
||||
JPanel panel = new JPanel();
|
||||
|
||||
setResizable(false);
|
||||
setTitle(title);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
setBounds(100, 100, 600, 150);
|
||||
setContentPane(panel);
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
GridBagLayout gridBagLayout = new GridBagLayout();
|
||||
gridBagLayout.columnWidths = new int[]{600, 0};
|
||||
gridBagLayout.rowHeights = new int[]{0, 0, 0, 200};
|
||||
gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
|
||||
gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 1.0};
|
||||
panel.setLayout(gridBagLayout);
|
||||
|
||||
progressText = new JLabel("");
|
||||
GridBagConstraints gbc_lblProgressText = new GridBagConstraints();
|
||||
gbc_lblProgressText.insets = new Insets(10, 0, 5, 0);
|
||||
gbc_lblProgressText.gridx = 0;
|
||||
gbc_lblProgressText.gridy = 0;
|
||||
panel.add(progressText, gbc_lblProgressText);
|
||||
|
||||
progressBar = new JProgressBar();
|
||||
GridBagConstraints gbc_progressBar = new GridBagConstraints();
|
||||
gbc_progressBar.insets = new Insets(0, 25, 5, 25);
|
||||
gbc_progressBar.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc_progressBar.gridx = 0;
|
||||
gbc_progressBar.gridy = 1;
|
||||
panel.add(progressBar, gbc_progressBar);
|
||||
|
||||
JButton btnCancel = new JButton(i18n("button.cancel"));
|
||||
btnCancel.addActionListener(e -> {
|
||||
System.exit(-1);
|
||||
});
|
||||
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
|
||||
gbc_btnCancel.insets = new Insets(0, 25, 5, 25);
|
||||
gbc_btnCancel.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc_btnCancel.gridx = 0;
|
||||
gbc_btnCancel.gridy = 2;
|
||||
panel.add(btnCancel, gbc_btnCancel);
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
progressText.setText(status);
|
||||
}
|
||||
|
||||
public void setProgress(int current, int total) {
|
||||
progressBar.setValue(current);
|
||||
progressBar.setMaximum(total);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public final class I18n {
|
||||
|
||||
private I18n() {}
|
||||
|
||||
private static SupportedLocale getCurrentLocale() {
|
||||
public static SupportedLocale getCurrentLocale() {
|
||||
try {
|
||||
return ConfigHolder.config().getLocalization();
|
||||
} catch (IllegalStateException e) {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.module.Configuration;
|
||||
import java.lang.module.ModuleFinder;
|
||||
import java.lang.module.ModuleReference;
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import jdk.internal.loader.BuiltinClassLoader;
|
||||
|
||||
/**
|
||||
* Utility for Adding JavaFX to module path.
|
||||
*
|
||||
* @author ZekerZhayard
|
||||
*/
|
||||
public class JavaFXPatcher {
|
||||
/**
|
||||
* Add JavaFX to module path at runtime.
|
||||
*
|
||||
* @param modules All module names
|
||||
* @param jarPaths All jar paths
|
||||
* @throws ReflectiveOperationException When the call to add these jars to the system module path failed.
|
||||
*/
|
||||
public static void patch(Set<String> modules, Path... jarPaths) throws ReflectiveOperationException {
|
||||
// Find all modules
|
||||
ModuleFinder finder = ModuleFinder.of(jarPaths);
|
||||
|
||||
// Load all modules as unnamed module
|
||||
for (ModuleReference mref : finder.findAll()) {
|
||||
((BuiltinClassLoader) ClassLoader.getSystemClassLoader()).loadModule(mref);
|
||||
}
|
||||
|
||||
// Define all modules
|
||||
Configuration config = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), finder, modules);
|
||||
ModuleLayer layer = ModuleLayer.defineModules(config, List.of(ModuleLayer.boot()), name -> ClassLoader.getSystemClassLoader()).layer();
|
||||
|
||||
// Add-Exports and Add-Opens
|
||||
try (
|
||||
FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + JavaFXPatcher.class.getProtectionDomain().getCodeSource().getLocation().toURI()), Map.of("create", "true"));
|
||||
InputStream stream = Files.newInputStream(fs.getPath("/META-INF/MANIFEST.MF"))
|
||||
) {
|
||||
// Some hacks
|
||||
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
|
||||
Attributes attributes = new Manifest(stream).getMainAttributes();
|
||||
addExportsOrOpens(attributes.getValue("Add-Exports"), layer, lookup.findVirtual(Module.class, "implAddExportsToAllUnnamed", MethodType.methodType(void.class, String.class)));
|
||||
addExportsOrOpens(attributes.getValue("Add-Opens"), layer, lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class)));
|
||||
} catch (Throwable t) {
|
||||
throw new ReflectiveOperationException(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addExportsOrOpens(String targets, ModuleLayer layer, MethodHandle handle) {
|
||||
for (String target : targets.split("\\s+")) {
|
||||
String[] name = target.split("/", 2); // <module>/<package>
|
||||
if (name[0].startsWith("javafx.")) {
|
||||
layer.findModule(name[0]).ifPresent(m -> {
|
||||
try {
|
||||
handle.invokeWithArguments(m, name[1]);
|
||||
} catch (Throwable throwable) {
|
||||
throw new RuntimeException(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -22,4 +22,6 @@
|
||||
-fx-base-rippler-color: %base-rippler-color%;
|
||||
-fx-base-disabled-text-fill: %disabled-font-color%;
|
||||
-fx-base-text-fill: %font-color%;
|
||||
|
||||
%font%
|
||||
}
|
||||
@@ -18,22 +18,6 @@
|
||||
.root {
|
||||
}
|
||||
|
||||
.jfx-list-view .scroll-bar {
|
||||
-fx-skin: "org.jackhuang.hmcl.ui.construct.FloatScrollBarSkin";
|
||||
}
|
||||
|
||||
.jfx-list-view .scroll-bar .track {
|
||||
-fx-fill: transparent;
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar {
|
||||
-fx-skin: "org.jackhuang.hmcl.ui.construct.FloatScrollBarSkin";
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar .track {
|
||||
-fx-fill: transparent;
|
||||
}
|
||||
|
||||
.scroll-bar .thumb {
|
||||
-fx-fill: rgba(255, 255, 255, 0.5);
|
||||
-fx-arc-width: 5px;
|
||||
@@ -44,13 +28,6 @@
|
||||
-fx-text-fill: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
-fx-padding: 20.0, 10.0;
|
||||
-fx-font-size: 15.0;
|
||||
-fx-font-weight: NORMAL;
|
||||
-fx-text-fill: #444;
|
||||
}
|
||||
|
||||
.title-label {
|
||||
-fx-font-size: 16.0px;
|
||||
-fx-padding: 14.0px;
|
||||
@@ -129,19 +106,34 @@
|
||||
-fx-padding: 8 16 8 16;
|
||||
}
|
||||
|
||||
.menu {
|
||||
.popup-menu .scroll-bar .thumb {
|
||||
-fx-fill: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.popup-menu .content {
|
||||
-fx-padding: 4 0 4 0;
|
||||
}
|
||||
|
||||
.two-line-list-item > .title {
|
||||
.two-line-list-item HBox {
|
||||
-fx-spacing: 8;
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.two-line-list-item .title {
|
||||
-fx-text-fill: #292929;
|
||||
-fx-font-size: 15px;
|
||||
}
|
||||
|
||||
.two-line-list-item > .subtitle {
|
||||
.two-line-list-item .subtitle {
|
||||
-fx-text-fill: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.two-line-list-item .tag {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-background-color: -fx-base-rippler-color;
|
||||
-fx-padding: 2;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
-fx-background-color: rgba(0, 0, 0, 0.5);
|
||||
-fx-background-radius: 2px;
|
||||
@@ -153,10 +145,6 @@
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.darker-fill {
|
||||
-fx-fill: -fx-base-darker-color;
|
||||
}
|
||||
|
||||
.sponsor-pane {
|
||||
-fx-padding: 16;
|
||||
-fx-cursor: hand;
|
||||
@@ -166,6 +154,32 @@
|
||||
-fx-wrap-text: true;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Main Page *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.launch-pane .jfx-button {
|
||||
-fx-background-color: -fx-base-color;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.launch-pane .jfx-button .jfx-rippler {
|
||||
-jfx-rippler-fill: white;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
-fx-padding: 0.0;
|
||||
}
|
||||
|
||||
.launch-pane .jfx-button, .jfx-button * {
|
||||
-fx-text-fill: -fx-base-text-fill;
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.launch-pane Rectangle {
|
||||
-fx-fill: -fx-base-darker-color;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Tab Pane *
|
||||
@@ -368,9 +382,14 @@
|
||||
|
||||
.jfx-tool-bar {
|
||||
-fx-font-size: 13.0;
|
||||
-fx-font-weight: BOLD;
|
||||
-fx-pref-width: 100.0%;
|
||||
-fx-pref-height: 32.0px;
|
||||
-fx-min-height: 40px;
|
||||
-fx-max-height: 40px;
|
||||
-fx-pref-height: 40px;
|
||||
}
|
||||
|
||||
.jfx-tool-bar .jfx-decorator-title {
|
||||
-fx-font-weight: BOLD;
|
||||
}
|
||||
|
||||
.jfx-tool-bar .background {
|
||||
@@ -383,9 +402,19 @@
|
||||
}
|
||||
|
||||
.jfx-tool-bar .jfx-decorator-button {
|
||||
-fx-min-width: 40px;
|
||||
-fx-max-width: 40px;
|
||||
-fx-pref-width: 40px;
|
||||
-fx-min-height: 40px;
|
||||
-fx-max-height: 40px;
|
||||
-fx-pref-height: 40px;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.jfx-tool-bar .jfx-decorator-button .svg {
|
||||
-fx-fill: -fx-base-text-fill;
|
||||
}
|
||||
|
||||
.jfx-tool-bar Label {
|
||||
-fx-text-fill: WHITE;
|
||||
}
|
||||
@@ -436,9 +465,9 @@
|
||||
}
|
||||
|
||||
.jfx-decorator-button {
|
||||
-fx-max-width: 35px;
|
||||
-fx-max-width: 40px;
|
||||
-fx-background-radius: 5px;
|
||||
-fx-max-height: 35px;
|
||||
-fx-max-height: 40px;
|
||||
-fx-background-color: transparent;
|
||||
-jfx-toggle-color: rgba(128, 128, 255, 0.2);
|
||||
-jfx-untoggle-color: transparent;
|
||||
@@ -671,8 +700,15 @@
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.jfx-list-view .scroll-bar {
|
||||
-fx-skin: "org.jackhuang.hmcl.ui.construct.FloatScrollBarSkin";
|
||||
}
|
||||
|
||||
.jfx-list-view .scroll-bar .track {
|
||||
-fx-fill: transparent;
|
||||
}
|
||||
|
||||
.jfx-list-view-float {
|
||||
-fx-padding: 5;
|
||||
-fx-background-insets: 0.0;
|
||||
-jfx-cell-horizontal-margin: 0.0;
|
||||
-jfx-cell-vertical-margin: 5.0;
|
||||
@@ -689,6 +725,7 @@
|
||||
.card {
|
||||
-fx-background-color: WHITE;
|
||||
-fx-background-radius: 4;
|
||||
-fx-padding: 8px;
|
||||
}
|
||||
|
||||
.card:selected {
|
||||
@@ -929,37 +966,41 @@
|
||||
-fx-text-fill: gray;
|
||||
}
|
||||
|
||||
.log {
|
||||
.log-window .scroll-bar .thumb {
|
||||
-fx-fill: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.log-window .list-cell {
|
||||
-fx-text-fill: black;
|
||||
-fx-border-width: 0 0 1 0;
|
||||
-fx-border-color: #dddddd;
|
||||
}
|
||||
|
||||
.log:empty {
|
||||
.log-window .list-cell:empty {
|
||||
-fx-border-width: 0;
|
||||
}
|
||||
|
||||
.log:fatal {
|
||||
.log-window .list-cell:fatal {
|
||||
-fx-background-color: #F7A699;
|
||||
}
|
||||
|
||||
.log:error {
|
||||
.log-window .list-cell:error {
|
||||
-fx-background-color: #FFCCBB;
|
||||
}
|
||||
|
||||
.log:warn {
|
||||
.log-window .list-cell:warn {
|
||||
-fx-background-color: #FFEECC;
|
||||
}
|
||||
|
||||
.log:info {
|
||||
.log-window .list-cell:info {
|
||||
-fx-background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.log:debug {
|
||||
.log-window .list-cell:debug {
|
||||
-fx-background-color: #EEE9E0;
|
||||
}
|
||||
|
||||
.log:trace {
|
||||
.log-window .list-cell:trace {
|
||||
-fx-background-color: #EEE9E0;
|
||||
}
|
||||
|
||||
@@ -1092,12 +1133,8 @@
|
||||
-fx-decorator-color: -fx-base-color;
|
||||
}
|
||||
|
||||
.jfx-decorator .jfx-decorator-buttons-container {
|
||||
-fx-background-color: -fx-decorator-color;
|
||||
}
|
||||
|
||||
.jfx-decorator-drawer {
|
||||
-fx-background-color: rgba(244, 244, 244, 0.55);
|
||||
-fx-background-color: rgba(244, 244, 244, 0.5);
|
||||
}
|
||||
|
||||
.jfx-decorator-title {
|
||||
@@ -1116,11 +1153,17 @@
|
||||
|
||||
.jfx-decorator-tab .tab-container:selected .tab-label {
|
||||
-fx-text-fill: -fx-base-text-fill;
|
||||
-fx-font-weight: BOLD;
|
||||
}
|
||||
|
||||
.resize-border {
|
||||
-fx-border-color: -fx-base-color;
|
||||
-fx-border-width: 2;
|
||||
.window {
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding: 16;
|
||||
}
|
||||
|
||||
.body {
|
||||
-fx-border-radius: 5;
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.4), 20, 0.3, 0.0, 0.0);
|
||||
}
|
||||
|
||||
.debug-border {
|
||||
@@ -1132,10 +1175,6 @@
|
||||
-fx-background-color: rgba(244, 244, 244, 0.5);
|
||||
}
|
||||
|
||||
.drawer-background {
|
||||
-fx-background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
.gray-background {
|
||||
-fx-background-color: rgba(244, 244, 244, 0.5);
|
||||
}
|
||||
@@ -1325,6 +1364,14 @@
|
||||
-fx-background-color: null;
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar {
|
||||
-fx-skin: "org.jackhuang.hmcl.ui.construct.FloatScrollBarSkin";
|
||||
}
|
||||
|
||||
.scroll-pane .scroll-bar .track {
|
||||
-fx-fill: transparent;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Error Facade *
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
<?import com.jfoenix.validation.RequiredFieldValidator?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<?import javafx.scene.shape.SVGPath?>
|
||||
<?import org.jackhuang.hmcl.ui.construct.RequiredValidator?>
|
||||
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
type="StackPane">
|
||||
@@ -45,13 +47,13 @@
|
||||
</JFXButton>
|
||||
</HBox>
|
||||
|
||||
<Label text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
|
||||
<Label fx:id="lblUsername" text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
|
||||
|
||||
<JFXTextField fx:id="txtUsername" GridPane.columnIndex="1" GridPane.rowIndex="2" GridPane.columnSpan="2"
|
||||
FXUtils.validateWhileTextChanged="true" onAction="#onCreationAccept">
|
||||
<validators>
|
||||
<RequiredFieldValidator message="%input.not_empty">
|
||||
</RequiredFieldValidator>
|
||||
<RequiredValidator message="%input.not_empty">
|
||||
</RequiredValidator>
|
||||
</validators>
|
||||
</JFXTextField>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</fx:define>
|
||||
<VBox fx:id="list" styleClass="jfx-list-view" maxHeight="150" spacing="8" maxWidth="400">
|
||||
<Label padding="${insets}" text="%install.modpack" />
|
||||
<JFXButton fx:id="btnLocal" styleClass="card" style="-fx-padding: 8; -fx-cursor: HAND" prefWidth="${list.width}" onMouseClicked="#onChooseLocalFile">
|
||||
<JFXButton fx:id="btnLocal" styleClass="card" style="-fx-cursor: HAND" prefWidth="${list.width}" onMouseClicked="#onChooseLocalFile">
|
||||
<graphic>
|
||||
<BorderPane mouseTransparent="true">
|
||||
<left>
|
||||
@@ -25,7 +25,7 @@
|
||||
</BorderPane>
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
<JFXButton fx:id="btnRemote" styleClass="card" style="-fx-padding: 8; -fx-cursor: HAND" prefWidth="${list.width}" onMouseClicked="#onChooseRemoteFile">
|
||||
<JFXButton fx:id="btnRemote" styleClass="card" style="-fx-cursor: HAND" prefWidth="${list.width}" onMouseClicked="#onChooseRemoteFile">
|
||||
<graphic>
|
||||
<BorderPane mouseTransparent="true">
|
||||
<left>
|
||||
|
||||
@@ -9,9 +9,20 @@
|
||||
type="BorderPane"
|
||||
prefHeight="400.0" prefWidth="600.0">
|
||||
<top>
|
||||
<StackPane styleClass="sponsor-pane" onMouseClicked="#onSponsor">
|
||||
<Label text="%sponsor.bmclapi" />
|
||||
</StackPane>
|
||||
<VBox>
|
||||
<StackPane styleClass="sponsor-pane" onMouseClicked="#onSponsor">
|
||||
<Label text="%sponsor.bmclapi" />
|
||||
</StackPane>
|
||||
<BorderPane style="-fx-padding: 0 16 16 16;">
|
||||
<left>
|
||||
<Label text="%settings.launcher.download_source" BorderPane.alignment="CENTER_LEFT" />
|
||||
</left>
|
||||
<right>
|
||||
<JFXComboBox fx:id="downloadSourceComboBox">
|
||||
</JFXComboBox>
|
||||
</right>
|
||||
</BorderPane>
|
||||
</VBox>
|
||||
</top>
|
||||
<center>
|
||||
<TransitionPane fx:id="root">
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
</fx:define>
|
||||
<VBox fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300">
|
||||
<Label padding="${insets}" text="%modpack.export.as" />
|
||||
<JFXButton fx:id="btnHMCL" prefWidth="${list.width}">
|
||||
<JFXButton fx:id="btnMCBBS" prefWidth="${list.width}">
|
||||
<graphic>
|
||||
<BorderPane mouseTransparent="true">
|
||||
<left>
|
||||
<TwoLineListItem title="%modpack.type.hmcl" subtitle="%modpack.type.hmcl.export" />
|
||||
<TwoLineListItem title="%modpack.type.mcbbs" subtitle="%modpack.type.mcbbs.export" />
|
||||
</left>
|
||||
<right>
|
||||
<fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import com.jfoenix.controls.JFXButton?>
|
||||
<?import com.jfoenix.controls.JFXToolbar?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import org.jackhuang.hmcl.ui.animation.TransitionPane?>
|
||||
<fx:root xmlns="http://javafx.com/javafx"
|
||||
type="StackPane"
|
||||
style="-fx-background-color: gray;"
|
||||
xmlns:fx="http://javafx.com/fxml">
|
||||
<VBox>
|
||||
<JFXToolbar fx:id="toolbar" styleClass="jfx-tool-bar">
|
||||
<leftItems>
|
||||
<JFXButton maxHeight="20" styleClass="toggle-icon3"
|
||||
StackPane.alignment="CENTER_RIGHT" onMouseClicked="#close">
|
||||
<graphic>
|
||||
<fx:include source="/assets/svg/close.fxml"/>
|
||||
</graphic>
|
||||
<StackPane.margin>
|
||||
<Insets left="20"/>
|
||||
</StackPane.margin>
|
||||
</JFXButton>
|
||||
<JFXButton fx:id="backButton" maxHeight="20" styleClass="toggle-icon3"
|
||||
StackPane.alignment="CENTER_LEFT" onMouseClicked="#back">
|
||||
<graphic>
|
||||
<fx:include source="/assets/svg/arrow-left.fxml"/>
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
<Label fx:id="titleLabel" style="-fx-text-fill:WHITE; -fx-font-size: 15;"
|
||||
StackPane.alignment="CENTER_LEFT"/>
|
||||
</leftItems>
|
||||
<rightItems>
|
||||
<JFXButton fx:id="refreshButton" maxHeight="20" styleClass="toggle-icon3" disable="true"
|
||||
StackPane.alignment="CENTER_RIGHT" onMouseClicked="#refrseh">
|
||||
<graphic>
|
||||
<fx:include source="/assets/svg/refresh.fxml"/>
|
||||
</graphic>
|
||||
<StackPane.margin>
|
||||
<Insets left="20"/>
|
||||
</StackPane.margin>
|
||||
</JFXButton>
|
||||
</rightItems>
|
||||
</JFXToolbar>
|
||||
|
||||
<TransitionPane fx:id="root"/>
|
||||
</VBox>
|
||||
</fx:root>
|
||||
@@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# Contributors: dxNeil
|
||||
# Contributors: dxNeil, machinesmith42
|
||||
about.copyright=Copyright
|
||||
about.copyright.statement=Copyright (c) 2020 huangyuhui.
|
||||
about.author=Author
|
||||
@@ -57,11 +57,16 @@ account.injector.server_name=Server Name
|
||||
account.manage=Account List
|
||||
account.methods=Login Type
|
||||
account.methods.authlib_injector=authlib-injector
|
||||
account.methods.microsoft=Microsoft Account
|
||||
account.methods.offline=Offline
|
||||
account.methods.yggdrasil=Mojang
|
||||
account.missing=No Account
|
||||
account.missing.add=Click here to add
|
||||
account.password=Password
|
||||
account.skin.file=Skin file
|
||||
account.skin.upload=Upload skin
|
||||
account.skin.upload.failed=Failed to upload skin
|
||||
account.skin.invalid_skin=Unrecognized skin file
|
||||
account.username=Name
|
||||
|
||||
archive.author=Authors
|
||||
@@ -101,13 +106,15 @@ download.failed.refresh=Unable to download version list. Click here to retry.
|
||||
download.provider.mcbbs=MCBBS (https://www.mcbbs.net/)
|
||||
download.provider.bmclapi=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
|
||||
download.provider.mojang=Mojang (OptiFine download service is provided by BMCLAPI)
|
||||
download.javafx=Downloading the JavaFX runtime components
|
||||
|
||||
extension.bat=Windows Bat file
|
||||
extension.mod=Mod file
|
||||
extension.png=Image file
|
||||
extension.sh=Bash shell
|
||||
|
||||
fatal.missing_javafx=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Java 8 or 10.\nIf you are using OpenJDK, please ensure OpenJFX is included.
|
||||
fatal.javafx.incompatible=Application cannot patch JavaFX on current Java environment below 11.\nPlease run this app using JDK 11 or higher or a JDK with JavaFX bundled.
|
||||
fatal.javafx.missing=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Oracle JRE 8, or install BellSoft Liberica Full JRE.\nIf you are using other OpenJDK builds, please ensure OpenJFX is included.
|
||||
fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use Hello Minecraft! Launcher, but it will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause the launcher not to function properly.\nPlease upgrade your Java Runtime to 8u101 or later to resolve the problem.
|
||||
fatal.config_loading_failure=The configuration is not accessible.\nPlease ensure Hello Minecraft! Launcher has read and write access to "%s" and the files in it.
|
||||
fatal.migration_requires_manual_reboot=The update is complete. Please reopen Hello Minecraft! Launcher.
|
||||
@@ -139,11 +146,12 @@ install.failed.install_online=Unable to recognize the provided installer file
|
||||
install.failed.malformed=The files just downloaded a moment ago is malformed. You may switch to other download provider to resolve this problem.
|
||||
install.failed.optifine_conflict=Fabric, OptiFine and Forge are installed simultaneously on Minecraft 1.13
|
||||
install.failed.version_mismatch=The library requires the game version %s, but the actual version is %s.
|
||||
install.installer.change_version=%s version: %s, this version is not compatible with current game version. Click here to choose another one.
|
||||
install.installer.change_version=%s, this version is not compatible with current game version. Click here to choose another one.
|
||||
install.installer.choose=Choose a %s version
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=Minecraft
|
||||
install.installer.incompatible=Incompatible with %s
|
||||
install.installer.install=Install %s
|
||||
install.installer.install_offline=Install/Upgrade from file
|
||||
install.installer.install_offline.extension=Forge/OptiFine installer
|
||||
@@ -151,9 +159,9 @@ install.installer.install_offline.tooltip=Supports import of Forge/OptiFine inst
|
||||
install.installer.install_online=Install Online
|
||||
install.installer.install_online.tooltip=Support installation of Fabric, Forge, OptiFine, LiteLoader.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=%s not Installed
|
||||
install.installer.not_installed=Not installed
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.version=%s Version
|
||||
install.installer.version=%s
|
||||
install.modpack=Install a modpack
|
||||
install.new_game=Install a New Game
|
||||
install.new_game.already_exists=This version already exists.
|
||||
@@ -168,13 +176,15 @@ lang.default=Use system language
|
||||
launch.advice.corrected=We have already fixed the JVM selection. If you want to keep your choice of Java version, you can disable the Java VM check in game settings.
|
||||
launch.advice.uncorrected=If you are sure that the game can be started normally, you can disable the Java VM check in game settings.
|
||||
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
|
||||
launch.advice.forge2760_liteloader=Forge 2760 and higher are not compatible with LiteLoader, please consider upgrading Forge to 2773 or later.
|
||||
launch.advice.forge2760_liteloader=Forge 2760 and higher are not compatible with LiteLoader, please consider upgrading Forge to 2773 or later. Shall we continue launching?
|
||||
launch.advice.forge28_2_2_optifine=Forge 28.2.2 and later versions are not compatible with OptiFine. Please consider downgrading Forge to 28.2.1 or earlier versions. Shall we continue launching?
|
||||
launch.advice.java8_1_13=Minecraft 1.13 and later can only run on Java 8 or later.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.0_51. Please install the latest version of Java 8.
|
||||
launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or later versions of Java.
|
||||
launch.advice.newer_java=Java 8 is recommended to make the game faster. For many Minecraft 1.12 of higher, and most mods, Java 8 is required.
|
||||
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game may crash. The launcher attempt to launch.
|
||||
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game may crash. The maximum memory capacity for 32 bit systems is is 1024MB. The launcher will attempt to launch.
|
||||
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game may crash. Shall we continue launching?
|
||||
launch.advice.require_newer_java_version=Minecraft %1$s requires Java %2$s or later, are you willing to download one now?
|
||||
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game may crash. The maximum memory capacity for 32 bit systems is is 1024MB. Shall we continue launching?
|
||||
launch.failed=Unable to launch
|
||||
launch.failed.cannot_create_jvm=Java virtual machine could not be created. Java arguments may cause issues. Please restart without JVM arguments.
|
||||
launch.failed.creating_process=Failed to create process. Check your Java path.
|
||||
@@ -256,6 +266,10 @@ modpack.invalid=Invalid modpack file.
|
||||
modpack.mismatched_type=Inappropriate modpack type, your current game is a %s modpack, but your update file is a %s modpack.
|
||||
modpack.name=Modpack Name
|
||||
modpack.not_a_valid_name=Invalid modpack name
|
||||
modpack.origin=Origin
|
||||
modpack.origin.url=Official Website
|
||||
modpack.origin.mcbbs=MCBBS
|
||||
modpack.origin.mcbbs.prompt=Thread id
|
||||
modpack.scan=Scanning this modpack
|
||||
modpack.task.install=Import Modpack
|
||||
modpack.task.install.error=This modpack file cannot be recognized. Only Curse and MultiMC modpacks are supported.
|
||||
@@ -265,8 +279,8 @@ modpack.type.curse.completion=Install files related to Curse modpack
|
||||
modpack.type.curse.tolerable_error=We cannot complete the download of all files of this Curse modpack. You can retry the download when starting corresponding game version. You may retry for a couple of times due to network problems.
|
||||
modpack.type.curse.error=Unable to install this Curse modpack. Please retry.
|
||||
modpack.type.curse.not_found=Some of required resources are missing and thus could not be downloaded. Please consider the latest version or other modpacks.
|
||||
modpack.type.hmcl=Hello Minecraft! Launcher
|
||||
modpack.type.hmcl.export=Can be imported by Hello Minecraft! Launcher
|
||||
modpack.type.mcbbs=MCBBS Standard
|
||||
modpack.type.mcbbs.export=Can be imported by Hello Minecraft! Launcher
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC
|
||||
modpack.type.server=Server Auto-Update Modpack
|
||||
@@ -282,6 +296,7 @@ modpack.wizard.step.2.title=Add files to the modpack.
|
||||
modpack.wizard.step.3=Modpack Type
|
||||
modpack.wizard.step.3.title=Choose the format of the modpack.
|
||||
modpack.wizard.step.initialization.exported_version=Exported game version
|
||||
modpack.wizard.step.initialization.force_update=Force updating modpack if possible
|
||||
modpack.wizard.step.initialization.include_launcher=Include the launcher
|
||||
modpack.wizard.step.initialization.save=Export to...
|
||||
modpack.wizard.step.initialization.warning=Before creating a modpack, you should ensure that the game can launch successfully,\nand that your Minecraft is a release version.\nDo NOT add mods which cannot be redistributed.
|
||||
@@ -397,6 +412,7 @@ settings.launcher.proxy.socks=Socks
|
||||
settings.launcher.proxy.username=Account
|
||||
settings.launcher.theme=Theme
|
||||
|
||||
settings.min_memory=Min Memory/MB
|
||||
settings.max_memory=Max Memory/MB
|
||||
settings.physical_memory=Physical Memory Size
|
||||
settings.show_log=Show Logs
|
||||
@@ -464,4 +480,4 @@ version.update=Update modpack
|
||||
wizard.prev=< Prev
|
||||
wizard.failed=Failed
|
||||
wizard.finish=Finish
|
||||
wizard.next=Next >
|
||||
wizard.next=Next >
|
||||
@@ -57,6 +57,7 @@ account.injector.server_name=Nombre de servidor
|
||||
account.manage=Lista de cuentas
|
||||
account.methods=Tipo de inición
|
||||
account.methods.authlib_injector=authlib-injector
|
||||
account.methods.microsoft=Microsoft
|
||||
account.methods.offline=Offline
|
||||
account.methods.yggdrasil=Mojang
|
||||
account.missing=No hay cuenta
|
||||
@@ -140,6 +141,7 @@ install.installer.choose=Escoja una versión de %s
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=Minecraft
|
||||
install.installer.incompatible=Incompatible with %s
|
||||
install.installer.install=Instalar %s
|
||||
install.installer.install_offline=Instalar/actualizar con archivo local
|
||||
install.installer.install_offline.extension=Instalador de Forge/OptiFine
|
||||
@@ -147,9 +149,9 @@ install.installer.install_offline.tooltip=Soporta importación de archivo instal
|
||||
install.installer.install_online=Instalar en línea
|
||||
install.installer.install_online.tooltip=Soporta instalación de Fabric, Forge, OptiFine, LiteLoader
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=%s no está instalado
|
||||
install.installer.not_installed=no está instalado
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.version=Versión %s
|
||||
install.installer.version=%s
|
||||
install.modpack=Instalar un modpack
|
||||
install.new_game=Instalar un juego nuevo
|
||||
install.new_game.already_exists=Esta versión ya existe.
|
||||
@@ -165,6 +167,7 @@ launch.advice.corrected=Ya hemos corregido la selección de JVM y el juego puede
|
||||
launch.advice.uncorrected=Si está seguro que el juego puede ser lanzado normalmente, puede deshabilitar el Java VM checker en la configuración del juego.
|
||||
launch.advice.different_platform=Su SO es 64-Bit pero su Java es 32-Bit. 64-Bit Java es recomendado.
|
||||
launch.advice.forge2760_liteloader=Forge 2760 y posterior no es compatible con LiteLoader, considere actualizando Forge a 2773 o más.
|
||||
launch.advice.forge28_2_2_optifine=Forge 28.2.2 and later versions are not compatible with OptiFine. Please consider downgrading Forge to 28.2.1 or earlier versions. Shall we continue launching?
|
||||
launch.advice.java8_1_13=Minecraft 1.13 y posterior sólo pueden correr en Java 8 o luego.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 puediera fallar en Java 8 anterior 1.8.0_51. Debe instalar la versión de Java 8 más reciente.
|
||||
launch.advice.java9=No puedes lanzar Minecraft hasta que su versión de juego sea más allá de 1.13 con Java 9 o posterior.
|
||||
@@ -443,4 +446,4 @@ version.update=Actualizar modpack
|
||||
wizard.prev=< Prev
|
||||
wizard.failed=Fallo
|
||||
wizard.finish=Acabar
|
||||
wizard.next=Próx >
|
||||
wizard.next=Próx >
|
||||
Loaded 100 of 258 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user