Compare commits
123
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a2ea07bd | ||
|
|
042e28216d | ||
|
|
9f486f5549 | ||
|
|
3e5a4b1d49 | ||
|
|
f023339a38 | ||
|
|
9a68c36cc6 | ||
|
|
703c7f7d0d | ||
|
|
46d502a5fd | ||
|
|
697ec45620 | ||
|
|
12c0165cdf | ||
|
|
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
@@ -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
|
||||
|
||||
+27
-2
@@ -1,3 +1,9 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '4.0.0'
|
||||
id 'application'
|
||||
@@ -77,10 +83,29 @@ def repack(File file) {
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.',
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
|
||||
'Main-Class': mainClassName,
|
||||
'Multi-Release': 'true',
|
||||
'Implementation-Version': version
|
||||
'Implementation-Version': version,
|
||||
'Add-Opens': [
|
||||
'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': [
|
||||
'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(" ")
|
||||
}
|
||||
finalizedBy shadowJar
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ 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;
|
||||
@@ -55,6 +56,7 @@ public final class Launcher extends Application {
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -74,6 +76,12 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
super.stop();
|
||||
Controllers.onApplicationStop();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
||||
@@ -106,7 +114,6 @@ public final class Launcher extends Application {
|
||||
Schedulers.shutdown();
|
||||
Controllers.shutdown();
|
||||
Platform.exit();
|
||||
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
@@ -62,10 +63,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 +81,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 +224,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");
|
||||
|
||||
@@ -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,8 +23,10 @@ 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.launch.NotDecompressingNativesException;
|
||||
import org.jackhuang.hmcl.launch.PermissionException;
|
||||
@@ -37,10 +39,7 @@ import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
|
||||
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,6 +48,7 @@ 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;
|
||||
@@ -59,10 +59,12 @@ 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.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 +93,6 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
|
||||
private CountDownLatch launchingLatch;
|
||||
|
||||
public void setTestMode() {
|
||||
launcherVisibility = LauncherVisibility.KEEP;
|
||||
@@ -108,9 +109,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 +127,32 @@ 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 ("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")
|
||||
.thenComposeAsync(() -> {
|
||||
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version)).orElse(null);
|
||||
})
|
||||
.thenComposeAsync(Task.supplyAsync(() -> {
|
||||
try {
|
||||
return account.logIn();
|
||||
@@ -165,7 +172,7 @@ public final class LauncherHelper {
|
||||
setting.toLaunchOptions(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 +199,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 +245,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) {
|
||||
@@ -315,7 +335,7 @@ public final class LauncherHelper {
|
||||
|
||||
// LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader.
|
||||
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9_AND_LATER
|
||||
&& version.getMainClass().contains("launchwrapper")
|
||||
&& version.getMainClass().equals(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN)
|
||||
&& version.getLibraries().stream()
|
||||
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
|
||||
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
|
||||
@@ -383,13 +403,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 +421,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;
|
||||
}
|
||||
}
|
||||
@@ -449,11 +481,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 +516,7 @@ public final class LauncherHelper {
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
logWindow.show();
|
||||
latch.countDown();
|
||||
logWindowLatch.countDown();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -502,9 +536,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 +571,7 @@ public final class LauncherHelper {
|
||||
|
||||
if (showLogs) {
|
||||
try {
|
||||
latch.await();
|
||||
logWindowLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
@@ -548,7 +580,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();
|
||||
}
|
||||
|
||||
@@ -157,6 +157,15 @@ public final class ModpackHelper {
|
||||
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 +185,11 @@ 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 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();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
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;
|
||||
@@ -35,14 +39,10 @@ import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
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;
|
||||
@@ -142,8 +142,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ 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;
|
||||
@@ -61,9 +62,22 @@ 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);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||
@@ -206,6 +220,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 +261,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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -554,8 +554,7 @@ public final class VersionSetting implements Cloneable {
|
||||
.setPrecalledCommand(getPreLaunchCommand())
|
||||
.setNoGeneratedJVMArgs(isNoJVMArgs());
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxyHost(config().getProxyHost());
|
||||
builder.setProxyPort(config().getProxyPort());
|
||||
builder.setProxy(ProxyManager.getProxy());
|
||||
if (config().hasProxyAuth()) {
|
||||
builder.setProxyUser(config().getProxyUser());
|
||||
builder.setProxyPass(config().getProxyPass());
|
||||
|
||||
@@ -17,10 +17,14 @@
|
||||
*/
|
||||
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.setting.EnumCommonDirectory;
|
||||
@@ -50,6 +54,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,12 +98,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;
|
||||
|
||||
stage.setHeight(config().getHeight());
|
||||
stageHeight.bind(stage.heightProperty());
|
||||
stage.setWidth(config().getWidth());
|
||||
stageWidth.bind(stage.widthProperty());
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
decorator = new DecoratorController(stage, getRootPage());
|
||||
@@ -110,13 +125,17 @@ public final class Controllers {
|
||||
|
||||
Task.runAsync(JavaVersion::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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,23 +23,30 @@ import javafx.beans.property.*;
|
||||
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.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.PromptDialogPane;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
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.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class AccountListItem extends RadioButton {
|
||||
|
||||
private final Account account;
|
||||
@@ -77,9 +84,9 @@ public class AccountListItem extends RadioButton {
|
||||
return new AccountListItemSkin(this);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
account.clearCache();
|
||||
thread(() -> {
|
||||
private Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
account.clearCache();
|
||||
try {
|
||||
account.logIn();
|
||||
} catch (CredentialExpiredException e) {
|
||||
@@ -89,13 +96,52 @@ 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 void refresh() {
|
||||
refreshAsync().whenComplete(e -> {}).start();
|
||||
}
|
||||
|
||||
public boolean canUploadSkin() {
|
||||
return account instanceof YggdrasilAccount && !(account instanceof AuthlibInjectorAccount);
|
||||
}
|
||||
|
||||
public void uploadSkin() {
|
||||
if (!(account instanceof YggdrasilAccount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("account.skin.upload"), (questions, resolve, reject) -> {
|
||||
PromptDialogPane.Builder.CandidatesQuestion q = (PromptDialogPane.Builder.CandidatesQuestion) questions.get(0);
|
||||
String model = q.getValue() == 0 ? "" : "slim";
|
||||
refreshAsync()
|
||||
.thenRunAsync(() -> ((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath()))
|
||||
.thenComposeAsync(this::refreshAsync)
|
||||
.thenRunAsync(Schedulers.javafx(), resolve::run)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e != null) {
|
||||
reject.accept(AddAccountPane.accountException(e));
|
||||
}
|
||||
}).start();
|
||||
}).addQuestion(new PromptDialogPane.Builder.CandidatesQuestion(i18n("account.skin.model"),
|
||||
i18n("account.skin.model.default"), i18n("account.skin.model.slim"))));
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -85,6 +84,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
btnRefresh.getStyleClass().add("toggle-icon4");
|
||||
@@ -92,6 +92,15 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
|
||||
right.getChildren().add(btnRefresh);
|
||||
|
||||
if (skinnable.canUploadSkin()) {
|
||||
JFXButton btnUpload = new JFXButton();
|
||||
btnUpload.setOnMouseClicked(e -> skinnable.uploadSkin());
|
||||
btnUpload.getStyleClass().add("toggle-icon4");
|
||||
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
|
||||
right.getChildren().add(btnUpload);
|
||||
}
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnMouseClicked(e -> skinnable.remove());
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
@@ -102,7 +111,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);
|
||||
|
||||
@@ -317,6 +317,8 @@ 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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.task.DownloadManager;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
@@ -37,7 +38,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
public class TaskExecutorDialogPane extends StackPane {
|
||||
private TaskExecutor executor;
|
||||
private Consumer<Region> onCancel;
|
||||
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
|
||||
private final Consumer<DownloadManager.SpeedEvent> speedEventHandler;
|
||||
|
||||
@FXML
|
||||
private Label lblTitle;
|
||||
@@ -75,7 +76,7 @@ public class TaskExecutorDialogPane extends StackPane {
|
||||
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
|
||||
});
|
||||
};
|
||||
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
DownloadManager.speedEvent.channel(DownloadManager.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
}
|
||||
|
||||
public void setExecutor(TaskExecutor executor) {
|
||||
|
||||
@@ -274,7 +274,7 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
public void setThrowable(Throwable throwable) {
|
||||
unbind();
|
||||
state.setText(throwable.getLocalizedMessage());
|
||||
state.setText(throwable == null ? "" : throwable.getLocalizedMessage());
|
||||
bar.setProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -101,7 +101,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,7 +121,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.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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -114,7 +114,7 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
|
||||
resolve.run();
|
||||
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(url, modpack.toFile(), null)
|
||||
new FileDownloadTask(url, modpack.toFile())
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
resolve.run();
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<Void> 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);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.stage.FileChooser;
|
||||
@@ -42,6 +43,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -74,9 +76,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 +86,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 +116,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 +146,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);
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
@@ -25,25 +26,31 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Pack200;
|
||||
|
||||
class HMCLDownloadTask extends FileDownloadTask {
|
||||
class HMCLDownloadTask extends Task<Void> {
|
||||
|
||||
private RemoteVersion.Type archiveFormat;
|
||||
private final FileDownloadTask task;
|
||||
private final RemoteVersion.Type archiveFormat;
|
||||
private final Path target;
|
||||
|
||||
public HMCLDownloadTask(RemoteVersion version, Path target) {
|
||||
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
|
||||
archiveFormat = version.getType();
|
||||
this.task = new FileDownloadTask(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
|
||||
this.archiveFormat = version.getType();
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return Collections.singletonList(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
super.execute();
|
||||
|
||||
try {
|
||||
Path target = getFile().toPath();
|
||||
|
||||
switch (archiveFormat) {
|
||||
case JAR:
|
||||
break;
|
||||
@@ -60,7 +67,7 @@ class HMCLDownloadTask extends FileDownloadTask {
|
||||
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
getFile().delete();
|
||||
Files.deleteIfExists(target);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
@@ -62,6 +62,11 @@ 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.model=Character model
|
||||
account.skin.model.default=Steve
|
||||
account.skin.model.slim=Alex
|
||||
account.skin.upload=Upload skin
|
||||
account.username=Name
|
||||
|
||||
archive.author=Authors
|
||||
@@ -139,11 +144,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 +157,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 +174,14 @@ 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.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.
|
||||
@@ -464,4 +471,4 @@ version.update=Update modpack
|
||||
wizard.prev=< Prev
|
||||
wizard.failed=Failed
|
||||
wizard.finish=Finish
|
||||
wizard.next=Next >
|
||||
wizard.next=Next >
|
||||
@@ -140,6 +140,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 +148,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 +166,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 +445,4 @@ version.update=Actualizar modpack
|
||||
wizard.prev=< Prev
|
||||
wizard.failed=Fallo
|
||||
wizard.finish=Acabar
|
||||
wizard.next=Próx >
|
||||
wizard.next=Próx >
|
||||
@@ -16,9 +16,9 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# Contributors: Botylyov Yury, The_BadUser
|
||||
# Содействующие: Botylyov Yury, The_BadUser
|
||||
about.copyright=Авторские права
|
||||
about.copyright.statement=(c) 2020 huangyuhui.
|
||||
about.copyright.statement=(C) 2020 huangyuhui.
|
||||
about.author=Автор
|
||||
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
|
||||
about.thanks_to=Отдельное спасибо
|
||||
@@ -66,7 +66,7 @@ account.username=Имя пользователя
|
||||
|
||||
archive.author=Авторы
|
||||
archive.game_version=Игра
|
||||
archive.name=Никнейм
|
||||
archive.name=Название
|
||||
archive.version=Версия
|
||||
|
||||
assets.download=Скачивание активов
|
||||
@@ -139,26 +139,27 @@ install.failed.install_online=Невозможно распознать пред
|
||||
install.failed.malformed=Файлы, скачаные минуту назад, неправильно сформированы. Для решения этой проблемы можно переключиться на другого поставщика скачивания.
|
||||
install.failed.optifine_conflict=Fabric, OptiFine и Forge установлены одновременно на Minecraft 1.13
|
||||
install.failed.version_mismatch=Библиотека требует версию игры %s, но фактическая версия %s.
|
||||
install.installer.change_version=Версия %s: %s, эта версия не совместима с текущей версией игры. Что бы выбрать другую, нажмите здесь.
|
||||
install.installer.change_version=%s, эта версия не совместима с текущей версией игры. Что бы выбрать другую, нажмите здесь.
|
||||
install.installer.choose=Выберите версию %s
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=Minecraft
|
||||
install.installer.install=Установить %s
|
||||
install.installer.incompatible=Incompatible with %s
|
||||
install.installer.install=Установка %s
|
||||
install.installer.install_offline=Установить/обновить из файла
|
||||
install.installer.install_offline.extension=Установщик Forge/OptiFine
|
||||
install.installer.install_offline.tooltip=Поддерживает импорт установочных файлов Forge/OptiFine
|
||||
install.installer.install_online=Установить из сети
|
||||
install.installer.install_online.tooltip=Поддерживается установка Fabric, Forge, OptiFine, LiteLoader.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=%s не установлен
|
||||
install.installer.not_installed=не установлен
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.version=%s - версия
|
||||
install.installer.version=%s
|
||||
install.modpack=Установить модпак
|
||||
install.new_game=Установите новую игру
|
||||
install.new_game=Установка новой игры
|
||||
install.new_game.already_exists=Эта версия уже существует.
|
||||
install.new_game.current_game_version=Текущая версия игры
|
||||
install.new_game.malformed=Недопустимое имя
|
||||
install.new_game.malformed=Недопустимое название
|
||||
install.select=Выберите операцию
|
||||
install.success=установлено успешно
|
||||
|
||||
@@ -169,6 +170,7 @@ launch.advice.corrected=Мы уже исправили выбор JVMЕ. Есл
|
||||
launch.advice.uncorrected=Если вы уверены, что игра запускается нормально, то можете отключить проверку Java VM в настройках игры.
|
||||
launch.advice.different_platform=Ваша ОС 64-битная, а ваша Java 32-битная. Рекомендуется использовать 64-битный Java.
|
||||
launch.advice.forge2760_liteloader=Forge 2760 и выше не совместимы с LiteLoader, рассмотрите возможность обновления Forge до 2773 или более поздней версии.
|
||||
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 и выше может работать только на Java 8 или более поздней версии.
|
||||
launch.advice.java8_51_1_13=У Minecraft 1.13 может произойти сбой на Java 8 меньше, чем 1.8.0_51. Установите последнюю версию Java 8.
|
||||
launch.advice.java9=Вы не можете запустить Minecraft 1.12 или более ранней версии с Java 9 или более новых версий Java.
|
||||
@@ -389,7 +391,7 @@ settings.launcher.log.font=Шрифт журналов (logs)
|
||||
settings.launcher.proxy=Прокси
|
||||
settings.launcher.proxy.authentication=Аутентификация прокси
|
||||
settings.launcher.proxy.disable=Использовать прокси системы
|
||||
settings.launcher.proxy.host=Host
|
||||
settings.launcher.proxy.host=Хост
|
||||
settings.launcher.proxy.http=HTTP
|
||||
settings.launcher.proxy.password=Пароль
|
||||
settings.launcher.proxy.port=Порт
|
||||
@@ -402,13 +404,13 @@ settings.physical_memory=Размер физической памяти
|
||||
settings.show_log=Показать журналы (logs)
|
||||
settings.tabs.installers=Установщики
|
||||
settings.type=Тип настройки версии
|
||||
settings.type.global=Глобальные настройки игры (все настройки разделены между играми)
|
||||
settings.type.global.manage=Глобальные настройки игры
|
||||
settings.type.global.edit=Настроить глобальные настройки игры
|
||||
settings.type.special.enable=Включить специальные настройки для этой игры
|
||||
settings.type.global=Глобальные настройки версии (все настройки разделены между версиями)
|
||||
settings.type.global.manage=Глобальные настройки версии
|
||||
settings.type.global.edit=Настроить глобальные настройки версии
|
||||
settings.type.special.enable=Включить специальные настройки для этой версии
|
||||
|
||||
sponsor.bmclapi=Сервисы загрузки предоставляются BMCLAPI и MCBBS. Чтобы узнать подробности нажмите здесь.
|
||||
sponsor.hmcl=Hello Minecraft! Launcher - это бесплатная пусковая установка Minecraft с открытым исходным кодом, которая позволяет пользователям легко управлять несколькими отдельными установками Minecraft. Мы используем Afdian, чтобы продолжать платить за хостинг и разработку проектов. Чтобы узнать подробности нажмите здесь.
|
||||
sponsor.bmclapi=Сервисы загрузки предоставляются BMCLAPI и MCBBS. Для подробностей нажмите здесь.
|
||||
sponsor.hmcl=Hello Minecraft! Launcher - это бесплатный лаунчер Minecraft с открытым исходным кодом, который позволяет пользователям легко управлять несколькими отдельными установками Minecraft. Мы используем Afdian, чтобы продолжать платить за хостинг и разработку проектов. Для подробностей нажмите здесь.
|
||||
|
||||
update=Обновление лаунчера
|
||||
update.accept=Обновить
|
||||
@@ -426,12 +428,12 @@ update.latest=Это последняя версия.
|
||||
update.no_browser=Не удаётся открыть браузер. Ссылка была скопирована в буфер обмена. Вставьте её в адресную строку браузера для обновления.
|
||||
update.tooltip=Обновить
|
||||
|
||||
version=Игры
|
||||
version=Версии
|
||||
version.cannot_read=Невозможно найти версию игры. Не удаётся продолжить автоустановку.
|
||||
version.empty=Нет игры
|
||||
version.empty=Нет установленных
|
||||
version.empty.add=Установить новую версию
|
||||
version.empty.launch=Нет версии для запуска, установите версию на странице списка версий.
|
||||
version.forbidden_name=Запрещённое имя, не используйте его.
|
||||
version.forbidden_name=Запрещённое название, не используйте его.
|
||||
version.game.old=Старые
|
||||
version.game.release=Релизы
|
||||
version.game.snapshot=Snapshots
|
||||
@@ -440,19 +442,23 @@ version.launch.test=Проверить игру
|
||||
version.launch_script=Создать скрипт запуска
|
||||
version.launch_script.failed=Невозможно создать скрипт запуска.
|
||||
version.launch_script.save=Сохранить скрипт запуска
|
||||
version.launch_script.success=Завершено создание скрипта, %s.
|
||||
version.launch_script.success=Создание скрипта завершено, %s.
|
||||
version.manage=Все версии
|
||||
version.manage.clean=Очистить каталог игры
|
||||
version.manage.clean.tooltip=Очистить журналы, отчёты об ошибках
|
||||
version.manage.duplicate=Copy game instance
|
||||
version.manage.duplicate.duplicate_save=Copy saves
|
||||
version.manage.duplicate.prompt=Type new version name
|
||||
version.manage.duplicate.confirm=
|
||||
version.manage.manage=Управление версией
|
||||
version.manage.redownload_assets_index=Обновить файлы игровых активов
|
||||
version.manage.remove=Удалить эту версию
|
||||
version.manage.remove.confirm=Удалить эту версию %s? Эту версию невозможно восстановить снова!
|
||||
version.manage.remove.confirm.trash=Удалить эту версию %s? Эту версию с именем %s возможно восстановить из корзины системы.
|
||||
version.manage.remove.confirm=Удалить эту версию %s? Невозможно восстановить снова!
|
||||
version.manage.remove.confirm.trash=Удалить эту версию %s? %s возможно восстановить из корзины системы.
|
||||
version.manage.remove.confirm.independent=Поскольку эта версия находится в независимом режиме, удаление этой версии также удалит все сохранённые миры, принадлежащие этой версии. Удалить эту версию %s?
|
||||
version.manage.remove_libraries=Удалить файлы библиотеки
|
||||
version.manage.rename=Переименовать эту версию
|
||||
version.manage.rename.message=Пожалуйста, введите новое имя для этой версии
|
||||
version.manage.rename.message=Введите новое название для этой версии
|
||||
version.manage.rename.fail=Не удалось переименовать эту версию.
|
||||
version.settings=Настройки
|
||||
version.update=Обновить модпак
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# Contributors: pan93412
|
||||
about.copyright=著作權
|
||||
about.copyright.statement=著作權所有 (c) 2020 huangyuhui。
|
||||
about.author=作者
|
||||
@@ -61,6 +62,11 @@ account.methods.yggdrasil=正版登入
|
||||
account.missing=沒有遊戲帳戶
|
||||
account.missing.add=按一下此處加入帳戶
|
||||
account.password=密碼
|
||||
account.skin.file=皮膚圖片檔案
|
||||
account.skin.model=人物模型
|
||||
account.skin.model.default=Steve
|
||||
account.skin.model.slim=Alex
|
||||
account.skin.upload=上傳皮膚
|
||||
account.username=使用者名稱
|
||||
|
||||
archive.author=作者
|
||||
@@ -138,11 +144,12 @@ install.failed.install_online=無法識別要安裝的軟體
|
||||
install.failed.malformed=剛才下載的檔案格式損壞。您可以切換到其他下載來源以解決此問題。
|
||||
install.failed.optifine_conflict=暫不支援 OptiFine 與 Forge 同時安裝在 Minecraft 1.13 上
|
||||
install.failed.version_mismatch=該軟體需要的遊戲版本為 %s,但實際的遊戲版本為 %s。
|
||||
install.installer.change_version=%s 版本: %s,該版本與當前遊戲不相容,您需要點擊此處更換版本或刪除
|
||||
install.installer.change_version=%s,該版本與當前遊戲不相容,您需要點擊此處更換版本或刪除
|
||||
install.installer.choose=選擇 %s 版本
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=Minecraft
|
||||
install.installer.incompatible=與 %s 不相容
|
||||
install.installer.install=安裝%s
|
||||
install.installer.install_offline=從本機檔案安裝或升級
|
||||
install.installer.install_offline.extension=Forge/OptiFine 安裝器
|
||||
@@ -150,9 +157,9 @@ install.installer.install_offline.tooltip=支援匯入已經下載好的 Forge/O
|
||||
install.installer.install_online=線上安裝
|
||||
install.installer.install_online.tooltip=支援安裝 Fabric、Forge、OptiFine、LiteLoader
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=暫時不安裝 %s,可以點選此處安裝
|
||||
install.installer.not_installed=不安裝
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.version=%s 版本
|
||||
install.installer.version=%s
|
||||
install.modpack=安裝整合包
|
||||
install.new_game=安裝新遊戲版本
|
||||
install.new_game.already_exists=此版本已經存在,請重新命名
|
||||
@@ -167,13 +174,14 @@ lang.default=使用系統語言
|
||||
launch.advice.corrected=我們已經修正了問題。如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
|
||||
launch.advice.uncorrected=如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
|
||||
launch.advice.different_platform=您的系統為 64 位元,但是 Java 是 32 位元,建議您安裝 64 位 Java。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容。請刪除 LiteLoader 或者變更 Forge 至 2773 或更新的版本。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容。請刪除 LiteLoader 或者變更 Forge 至 2773 或更新的版本。是否繼續啟動?
|
||||
launch.advice.forge28_2_2_optifine=Forge 28.2.2 或更高版本與 OptiFine 不相容。請變更 Forge 至 28.2.1 或更低版本。是否繼續啟動?
|
||||
launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。
|
||||
launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod 的 Minecraft 版本不支援 Java 9 或更高版本,請使用 Java 8。
|
||||
launch.advice.newer_java=偵測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更順暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要 Java 8 版本。
|
||||
launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。啟動器仍會嘗試啟動。
|
||||
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。啟動器仍會嘗試啟動。
|
||||
launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。是否繼續啟動?
|
||||
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。是否繼續啟動?
|
||||
launch.failed=啟動失敗
|
||||
launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。
|
||||
launch.failed.creating_process=啟動失敗,在建立新處理程序時發生錯誤。可能是 Java 路徑錯誤。
|
||||
@@ -326,7 +334,7 @@ world.time=yyyy年MM月dd日 HH:mm:ss
|
||||
profile=遊戲目錄
|
||||
profile.already_exists=該名稱已存在
|
||||
profile.default=目前目錄
|
||||
profile.home=官方啟動器
|
||||
profile.home=官方啟動器目錄
|
||||
profile.instance_directory=遊戲路徑
|
||||
profile.instance_directory.choose=選擇遊戲路徑
|
||||
profile.manage=遊戲目錄列表
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# Contributors: huangyuhui
|
||||
about.copyright=版权
|
||||
about.copyright.statement=版权所有 (c) 2020 huangyuhui.
|
||||
about.author=作者
|
||||
@@ -61,6 +62,11 @@ account.methods.yggdrasil=正版登录
|
||||
account.missing=没有游戏账户
|
||||
account.missing.add=点击此处添加账户
|
||||
account.password=密码
|
||||
account.skin.file=皮肤图片文件
|
||||
account.skin.model=人物模型
|
||||
account.skin.model.default=Steve
|
||||
account.skin.model.slim=Alex
|
||||
account.skin.upload=上传皮肤
|
||||
account.username=用户名
|
||||
|
||||
archive.author=作者
|
||||
@@ -82,7 +88,7 @@ button.no=否
|
||||
button.ok=确定
|
||||
button.refresh=刷新
|
||||
button.remove=删除
|
||||
button.remove.confirm=您确定要删除吗?该操作无法撤销!
|
||||
button.remove.confirm=您确定要删除吗?此操作无法撤销!
|
||||
button.save=保存
|
||||
button.yes=是
|
||||
|
||||
@@ -135,14 +141,15 @@ install.failed.downloading=安装失败,部分文件未能完成下载
|
||||
install.failed.downloading.detail=未能下载文件:%s
|
||||
install.failed.downloading.timeout=下载超时:%s
|
||||
install.failed.install_online=无法识别要安装的软件
|
||||
install.failed.malformed=刚才下载的文件格式损坏。您可以切换到其他下载源来解决此问题。
|
||||
install.failed.malformed=下载的文件格式损坏。您可以切换到其他下载源来解决此问题。
|
||||
install.failed.optifine_conflict=暂不支持 OptiFine, Fabric 与 Forge 同时安装在 Minecraft 1.13 及以上版本
|
||||
install.failed.version_mismatch=该软件需要的游戏版本为 %s,但实际的游戏版本为 %s。
|
||||
install.installer.change_version=%s 版本: %s,该版本与当前游戏不兼容,您需要点击此处更换版本或删除
|
||||
install.installer.change_version=%s,该版本与当前游戏不兼容,您需要点击此处更换版本或删除
|
||||
install.installer.choose=选择 %s 版本
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=Minecraft
|
||||
install.installer.incompatible=与 %s 不兼容
|
||||
install.installer.install=安装 %s
|
||||
install.installer.install_offline=从本地文件安装/升级
|
||||
install.installer.install_offline.extension=Forge/OptiFine 安装器
|
||||
@@ -150,9 +157,9 @@ install.installer.install_offline.tooltip=支持导入已经下载好的 Forge/O
|
||||
install.installer.install_online=在线安装
|
||||
install.installer.install_online.tooltip=支持安装 Fabric、Forge、OptiFine、LiteLoader
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=暂不安装 %s,可以点击此处安装
|
||||
install.installer.not_installed=不安装
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.version=%s 版本
|
||||
install.installer.version=%s
|
||||
install.modpack=安装整合包
|
||||
install.new_game=安装新游戏版本
|
||||
install.new_game.already_exists=此版本已经存在,请换一个名字
|
||||
@@ -167,13 +174,14 @@ lang.default=跟随系统语言
|
||||
launch.advice.corrected=我们已经修复了问题。如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。
|
||||
launch.advice.uncorrected=如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。
|
||||
launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2773 或更新的版本。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2773 或更新的版本。是否继续启动?
|
||||
launch.advice.forge28_2_2_optifine=Forge 28.2.2 或更高版本与 OptiFine 不兼容。请更换 Forge 至 28.2.1 或更低版本。是否继续启动?
|
||||
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装最新版的 Java 8。
|
||||
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装 Java 8。
|
||||
launch.advice.java9=低于 1.13 的有安装 Mod 的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
|
||||
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅,而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要 Java 8。
|
||||
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏,启动器仍会尝试启动。
|
||||
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小,启动器仍会尝试启动。
|
||||
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏。是否继续启动?
|
||||
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小。是否继续启动?
|
||||
launch.failed=启动失败
|
||||
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
|
||||
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
|
||||
@@ -233,7 +241,7 @@ modpack.desc=描述你要制作的整合包,比如整合包注意事项和更
|
||||
modpack.description=整合包描述
|
||||
modpack.enter_name=给游戏起个你喜欢的名字
|
||||
modpack.export=导出整合包
|
||||
modpack.export.as=请选择整合包类型。若你无法决定,请选择 HMCL 类型。
|
||||
modpack.export.as=请选择整合包类型 (若无法决定,请选择 HMCL 类型)
|
||||
modpack.file_api=整合包下载链接前缀
|
||||
modpack.files.blueprints=BuildCraft 蓝图
|
||||
modpack.files.config=Mod 配置文件
|
||||
@@ -326,7 +334,7 @@ world.time=yyyy 年 MM 月 dd 日 HH:mm:ss
|
||||
profile=游戏目录
|
||||
profile.already_exists=该名称已存在
|
||||
profile.default=当前目录
|
||||
profile.home=官方启动器
|
||||
profile.home=官方启动器目录
|
||||
profile.instance_directory=游戏路径
|
||||
profile.instance_directory.choose=选择游戏路径
|
||||
profile.manage=游戏目录列表
|
||||
@@ -378,7 +386,7 @@ settings.icon=游戏图标
|
||||
settings.launcher=启动器设置
|
||||
settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库文件放于此集中管理,如果游戏文件夹内有现成的将不会使用公共库文件
|
||||
settings.launcher.download_source=下载源
|
||||
settings.launcher.enable_game_list=在主页内显示游戏列表
|
||||
settings.launcher.enable_game_list=在主页内显示版本列表
|
||||
settings.launcher.language=语言
|
||||
settings.launcher.launcher_log.export=导出启动器日志
|
||||
settings.launcher.launcher_log.export.failed=无法导出日志
|
||||
@@ -428,8 +436,8 @@ update.tooltip=更新
|
||||
version=游戏
|
||||
version.cannot_read=读取游戏版本失败,无法进行自动安装
|
||||
version.empty=没有游戏版本
|
||||
version.empty.add=进入游戏列表安装
|
||||
version.empty.launch=没有可启动的游戏,你可以点击左侧游戏栏内的设置按钮进入游戏列表安装游戏
|
||||
version.empty.add=进入版本列表安装
|
||||
version.empty.launch=没有可启动的游戏,你可以点击左侧游戏栏内的设置按钮进入版本列表安装游戏
|
||||
version.forbidden_name=此版本名称不受支持,请换一个名字
|
||||
version.game.old=远古版
|
||||
version.game.release=稳定版
|
||||
@@ -440,7 +448,7 @@ version.launch_script=生成启动脚本
|
||||
version.launch_script.failed=生成启动脚本失败
|
||||
version.launch_script.save=保存启动脚本
|
||||
version.launch_script.success=启动脚本已生成完毕:%s
|
||||
version.manage=游戏列表
|
||||
version.manage=版本列表
|
||||
version.manage.clean=清理游戏目录
|
||||
version.manage.clean.tooltip=清理 logs, crash-reports
|
||||
version.manage.duplicate=复制游戏实例
|
||||
@@ -463,4 +471,4 @@ version.update=更新整合包
|
||||
wizard.prev=< 上一步
|
||||
wizard.failed=失败
|
||||
wizard.finish=完成
|
||||
wizard.next=下一步 >
|
||||
wizard.next=下一步 >
|
||||
+4
-3
@@ -142,17 +142,18 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof AuthlibInjectorAccount))
|
||||
if (obj == null || obj.getClass() != AuthlibInjectorAccount.class)
|
||||
return false;
|
||||
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
|
||||
return super.equals(another) && server.equals(another.server);
|
||||
return characterUUID.equals(another.characterUUID) && server.equals(another.server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("uuid", characterUUID)
|
||||
.append("username", getUsername())
|
||||
.append("server", getServer())
|
||||
.append("server", getServer().getUrl())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
+36
-26
@@ -31,6 +31,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -43,38 +44,31 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
|
||||
private final Path artifactLocation;
|
||||
private final Supplier<DownloadProvider> downloadProvider;
|
||||
|
||||
/**
|
||||
* The flag will be reset after application restart.
|
||||
*/
|
||||
private boolean updateChecked = false;
|
||||
|
||||
/**
|
||||
* @param artifactsDirectory where to save authlib-injector artifacts
|
||||
*/
|
||||
public AuthlibInjectorDownloader(Path artifactsDirectory, Supplier<DownloadProvider> downloadProvider) {
|
||||
this.artifactLocation = artifactsDirectory.resolve("authlib-injector.jar");
|
||||
public AuthlibInjectorDownloader(Path artifactLocation, Supplier<DownloadProvider> downloadProvider) {
|
||||
this.artifactLocation = artifactLocation;
|
||||
this.downloadProvider = downloadProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
|
||||
synchronized (artifactLocation) {
|
||||
Optional<AuthlibInjectorArtifactInfo> cached = getArtifactInfoImmediately();
|
||||
if (cached.isPresent()) {
|
||||
return cached.get();
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
Optional<AuthlibInjectorArtifactInfo> local = getLocalArtifact();
|
||||
|
||||
if (!local.isPresent() || !updateChecked) {
|
||||
try {
|
||||
update(local);
|
||||
updateChecked = true;
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to download authlib-injector", e);
|
||||
if (!local.isPresent()) {
|
||||
throw e;
|
||||
}
|
||||
LOG.warning("Fallback to use cached artifact: " + local.get());
|
||||
}
|
||||
if (local.isPresent()) {
|
||||
return local.get();
|
||||
}
|
||||
|
||||
return getLocalArtifact().orElseThrow(() -> new IOException("The updated authlib-inejector cannot be recognized"));
|
||||
LOG.info("No local authlib-injector found, downloading");
|
||||
updateChecked.set(true);
|
||||
update();
|
||||
local = getLocalArtifact();
|
||||
return local.orElseThrow(() -> new IOException("The downloaded authlib-inejector cannot be recognized"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +77,22 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
|
||||
return getLocalArtifact();
|
||||
}
|
||||
|
||||
private void update(Optional<AuthlibInjectorArtifactInfo> local) throws IOException {
|
||||
LOG.info("Checking update of authlib-injector");
|
||||
private final AtomicBoolean updateChecked = new AtomicBoolean(false);
|
||||
|
||||
public void checkUpdate() throws IOException {
|
||||
// this method runs only once
|
||||
if (updateChecked.compareAndSet(false, true)) {
|
||||
synchronized (this) {
|
||||
LOG.info("Checking update of authlib-injector");
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void update() throws IOException {
|
||||
AuthlibInjectorVersionInfo latest = getLatestArtifactInfo();
|
||||
|
||||
Optional<AuthlibInjectorArtifactInfo> local = getLocalArtifact();
|
||||
if (local.isPresent() && local.get().getBuildNumber() >= latest.buildNumber) {
|
||||
return;
|
||||
}
|
||||
@@ -116,11 +122,15 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
|
||||
}
|
||||
|
||||
private Optional<AuthlibInjectorArtifactInfo> getLocalArtifact() {
|
||||
if (!Files.isRegularFile(artifactLocation)) {
|
||||
return parseArtifact(artifactLocation);
|
||||
}
|
||||
|
||||
protected static Optional<AuthlibInjectorArtifactInfo> parseArtifact(Path path) {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(AuthlibInjectorArtifactInfo.from(artifactLocation));
|
||||
return Optional.of(AuthlibInjectorArtifactInfo.from(path));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Bad authlib-injector artifact", e);
|
||||
return Optional.empty();
|
||||
|
||||
+5
@@ -53,6 +53,11 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
|
||||
return toURL(apiRoot + "authserver/invalidate");
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getProfilePropertiesURL(UUID uuid) throws AuthenticationException {
|
||||
return toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
|
||||
|
||||
+32
-64
@@ -27,9 +27,7 @@ import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -56,82 +54,52 @@ import javafx.beans.Observable;
|
||||
@JsonAdapter(AuthlibInjectorServer.Deserializer.class)
|
||||
public class AuthlibInjectorServer implements Observable {
|
||||
|
||||
private static final int MAX_REDIRECTS = 5;
|
||||
private static final Gson GSON = new GsonBuilder().create();
|
||||
|
||||
public static AuthlibInjectorServer locateServer(String url) throws IOException {
|
||||
url = parseInputUrl(url);
|
||||
HttpURLConnection conn;
|
||||
int redirectCount = 0;
|
||||
for (;;) {
|
||||
conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
Optional<String> ali = getApiLocationIndication(conn);
|
||||
if (ali.isPresent()) {
|
||||
conn.disconnect();
|
||||
url = ali.get();
|
||||
if (redirectCount >= MAX_REDIRECTS) {
|
||||
throw new IOException("Exceeded maximum number of redirects (" + MAX_REDIRECTS + ")");
|
||||
}
|
||||
redirectCount++;
|
||||
LOG.info("Redirect API root to: " + url);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
|
||||
server.refreshMetadata(readFullyWithoutClosing(conn.getInputStream()));
|
||||
return server;
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
url = addHttpsIfMissing(url);
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
|
||||
String ali = conn.getHeaderField("x-authlib-injector-api-location");
|
||||
if (ali != null) {
|
||||
URL absoluteAli = new URL(conn.getURL(), ali);
|
||||
if (!urlEqualsIgnoreSlash(url, absoluteAli.toString())) {
|
||||
conn.disconnect();
|
||||
url = absoluteAli.toString();
|
||||
conn = (HttpURLConnection) absoluteAli.openConnection();
|
||||
}
|
||||
}
|
||||
|
||||
if (!url.endsWith("/"))
|
||||
url += "/";
|
||||
|
||||
try {
|
||||
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
|
||||
server.refreshMetadata(readFullyWithoutClosing(conn.getInputStream()));
|
||||
return server;
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<String> getApiLocationIndication(URLConnection conn) {
|
||||
return Optional.ofNullable(conn.getHeaderFields().get("X-Authlib-Injector-API-Location"))
|
||||
.flatMap(list -> list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)))
|
||||
.flatMap(indication -> {
|
||||
String currentUrl = appendSuffixSlash(conn.getURL().toString());
|
||||
String newUrl;
|
||||
try {
|
||||
newUrl = appendSuffixSlash(new URL(conn.getURL(), indication).toString());
|
||||
} catch (MalformedURLException e) {
|
||||
LOG.warning("Failed to resolve absolute ALI, the header is [" + indication + "]. Ignore it.");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (newUrl.equals(currentUrl)) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
return Optional.of(newUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String parseInputUrl(String url) {
|
||||
private static String addHttpsIfMissing(String url) {
|
||||
String lowercased = url.toLowerCase();
|
||||
if (!lowercased.startsWith("http://") && !lowercased.startsWith("https://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
url = appendSuffixSlash(url);
|
||||
return url;
|
||||
}
|
||||
|
||||
private static String appendSuffixSlash(String url) {
|
||||
if (!url.endsWith("/")) {
|
||||
return url + "/";
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public static AuthlibInjectorServer fetchServerInfo(String url) throws IOException {
|
||||
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
|
||||
server.refreshMetadata();
|
||||
return server;
|
||||
private static boolean urlEqualsIgnoreSlash(String a, String b) {
|
||||
if (!a.endsWith("/"))
|
||||
a += "/";
|
||||
if (!b.endsWith("/"))
|
||||
b += "/";
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
private String url;
|
||||
|
||||
+6
-1
@@ -21,7 +21,7 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
|
||||
public class MojangYggdrasilProvider implements YggdrasilProvider {
|
||||
|
||||
@@ -45,6 +45,11 @@ public class MojangYggdrasilProvider implements YggdrasilProvider {
|
||||
return NetworkUtils.toURL("https://authserver.mojang.com/invalidate");
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
|
||||
return NetworkUtils.toURL("https://api.mojang.com/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin");
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getProfilePropertiesURL(UUID uuid) {
|
||||
return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
|
||||
|
||||
@@ -17,13 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
@@ -34,13 +28,16 @@ import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
public class YggdrasilAccount extends Account {
|
||||
|
||||
private final YggdrasilService service;
|
||||
private final UUID characterUUID;
|
||||
private final String username;
|
||||
protected final YggdrasilService service;
|
||||
protected final UUID characterUUID;
|
||||
protected final String username;
|
||||
|
||||
private boolean authenticated = false;
|
||||
private YggdrasilSession session;
|
||||
@@ -177,9 +174,8 @@ public class YggdrasilAccount extends Account {
|
||||
Map<Object, Object> storage = new HashMap<>();
|
||||
storage.put("username", username);
|
||||
storage.putAll(session.toStorage());
|
||||
service.getProfileRepository().getImmediately(characterUUID).ifPresent(profile -> {
|
||||
storage.put("profileProperties", profile.getProperties());
|
||||
});
|
||||
service.getProfileRepository().getImmediately(characterUUID).ifPresent(profile ->
|
||||
storage.put("profileProperties", profile.getProperties()));
|
||||
return storage;
|
||||
}
|
||||
|
||||
@@ -193,6 +189,10 @@ public class YggdrasilAccount extends Account {
|
||||
service.getProfileRepository().invalidate(characterUUID);
|
||||
}
|
||||
|
||||
public void uploadSkin(String model, Path file) throws AuthenticationException, UnsupportedOperationException {
|
||||
service.uploadSkin(characterUUID, session.getAccessToken(), model, file);
|
||||
}
|
||||
|
||||
private static String randomClientToken() {
|
||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
}
|
||||
@@ -209,7 +209,7 @@ public class YggdrasilAccount extends Account {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof YggdrasilAccount))
|
||||
if (obj == null || obj.getClass() != YggdrasilAccount.class)
|
||||
return false;
|
||||
YggdrasilAccount another = (YggdrasilAccount) obj;
|
||||
return characterUUID.equals(another.characterUUID);
|
||||
|
||||
@@ -35,6 +35,24 @@ public interface YggdrasilProvider {
|
||||
|
||||
URL getInvalidationURL() throws AuthenticationException;
|
||||
|
||||
/**
|
||||
* URL to upload skin.
|
||||
*
|
||||
* Headers:
|
||||
* Authentication: Bearer <access token>
|
||||
*
|
||||
* Payload:
|
||||
* The payload for this API consists of multipart form data. There are two parts (order does not matter b/c of boundary):
|
||||
* model: Empty string for the default model and "slim" for the slim model
|
||||
* file: Raw image file data
|
||||
*
|
||||
* @see <a href="https://wiki.vg/Mojang_API#Upload_Skin">https://wiki.vg/Mojang_API#Upload_Skin</a>
|
||||
* @return url to upload skin
|
||||
* @throws AuthenticationException if url cannot be generated. e.g. some parameter or query is malformed.
|
||||
* @throws UnsupportedOperationException if the Yggdrasil provider does not support third-party skin uploading.
|
||||
*/
|
||||
URL getSkinUploadURL(UUID uuid) throws AuthenticationException, UnsupportedOperationException;
|
||||
|
||||
URL getProfilePropertiesURL(UUID uuid) throws AuthenticationException;
|
||||
|
||||
}
|
||||
@@ -26,11 +26,18 @@ import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpMultipartRequest;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -145,6 +152,27 @@ public class YggdrasilService {
|
||||
requireEmpty(request(provider.getInvalidationURL(), createRequestWithCredentials(accessToken, clientToken)));
|
||||
}
|
||||
|
||||
public void uploadSkin(UUID uuid, String accessToken, String model, Path file) throws AuthenticationException, UnsupportedOperationException {
|
||||
try {
|
||||
HttpURLConnection con = NetworkUtils.createHttpConnection(provider.getSkinUploadURL(uuid));
|
||||
con.setRequestMethod("PUT");
|
||||
con.setRequestProperty("Authorization", "Bearer " + accessToken);
|
||||
con.setDoOutput(true);
|
||||
try (HttpMultipartRequest request = new HttpMultipartRequest(con)) {
|
||||
request.param("model", model);
|
||||
try (InputStream fis = Files.newInputStream(file)) {
|
||||
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
|
||||
}
|
||||
}
|
||||
String response = IOUtils.readFullyAsString(con.getInputStream());
|
||||
if (response.startsWith("{")) {
|
||||
handleErrorMessage(fromJson(response, ErrorResponse.class));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete game profile.
|
||||
*
|
||||
@@ -225,7 +253,7 @@ public class YggdrasilService {
|
||||
try {
|
||||
return GSON.fromJson(text, typeOfT);
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
throw new ServerResponseMalformedException("Server response: " + text, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,23 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public abstract class AbstractDependencyManager implements DependencyManager {
|
||||
|
||||
public abstract DownloadProvider getPrimaryDownloadProvider();
|
||||
|
||||
public abstract List<DownloadProvider> getPreferredDownloadProviders();
|
||||
public abstract DownloadProvider getDownloadProvider();
|
||||
|
||||
@Override
|
||||
public abstract DefaultCacheRepository getCacheRepository();
|
||||
|
||||
@Override
|
||||
public VersionList<?> getVersionList(String id) {
|
||||
return getPrimaryDownloadProvider().getVersionListById(id);
|
||||
return getDownloadProvider().getVersionListById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.download;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The download provider that changes the real download source in need.
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class AdaptedDownloadProvider implements DownloadProvider {
|
||||
|
||||
private List<DownloadProvider> downloadProviderCandidates;
|
||||
|
||||
public void setDownloadProviderCandidates(List<DownloadProvider> downloadProviderCandidates) {
|
||||
this.downloadProviderCandidates = new ArrayList<>(downloadProviderCandidates);
|
||||
}
|
||||
|
||||
public DownloadProvider getPreferredDownloadProvider() {
|
||||
List<DownloadProvider> d = downloadProviderCandidates;
|
||||
if (d == null || d.isEmpty()) {
|
||||
throw new IllegalStateException("No download provider candidate");
|
||||
}
|
||||
return d.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersionListURL() {
|
||||
return getPreferredDownloadProvider().getVersionListURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAssetBaseURL() {
|
||||
return getPreferredDownloadProvider().getAssetBaseURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String injectURL(String baseURL) {
|
||||
return getPreferredDownloadProvider().injectURL(baseURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<URL> getAssetObjectCandidates(String assetObjectLocation) {
|
||||
return downloadProviderCandidates.stream()
|
||||
.flatMap(d -> d.getAssetObjectCandidates(assetObjectLocation).stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<URL> injectURLWithCandidates(String baseURL) {
|
||||
return downloadProviderCandidates.stream()
|
||||
.flatMap(d -> d.injectURLWithCandidates(baseURL).stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<URL> injectURLsWithCandidates(List<String> urls) {
|
||||
return downloadProviderCandidates.stream()
|
||||
.flatMap(d -> d.injectURLsWithCandidates(urls).stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionList<?> getVersionListById(String id) {
|
||||
return getPreferredDownloadProvider().getVersionListById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getConcurrency() {
|
||||
return getPreferredDownloadProvider().getConcurrency();
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,18 @@ import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.game.GameVersion;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.OPTIFINE;
|
||||
|
||||
/**
|
||||
* Note: This class has no state.
|
||||
@@ -39,13 +45,11 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
|
||||
private final DefaultGameRepository repository;
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final List<DownloadProvider> preferredDownloadProviders;
|
||||
private final DefaultCacheRepository cacheRepository;
|
||||
|
||||
public DefaultDependencyManager(DefaultGameRepository repository, DownloadProvider downloadProvider, List<DownloadProvider> preferredDownloadProviders, DefaultCacheRepository cacheRepository) {
|
||||
public DefaultDependencyManager(DefaultGameRepository repository, DownloadProvider downloadProvider, DefaultCacheRepository cacheRepository) {
|
||||
this.repository = repository;
|
||||
this.downloadProvider = downloadProvider;
|
||||
this.preferredDownloadProviders = preferredDownloadProviders;
|
||||
this.cacheRepository = cacheRepository;
|
||||
}
|
||||
|
||||
@@ -55,15 +59,10 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadProvider getPrimaryDownloadProvider() {
|
||||
public DownloadProvider getDownloadProvider() {
|
||||
return downloadProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DownloadProvider> getPreferredDownloadProviders() {
|
||||
return preferredDownloadProviders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultCacheRepository getCacheRepository() {
|
||||
return cacheRepository;
|
||||
@@ -94,6 +93,28 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
return new GameLibrariesTask(this, version, integrityCheck, version.getLibraries());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> checkPatchCompletionAsync(Version version, boolean integrityCheck) {
|
||||
return Task.composeAsync(() -> {
|
||||
List<Task<?>> tasks = new ArrayList<>();
|
||||
|
||||
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
|
||||
if (!gameVersion.isPresent()) return null;
|
||||
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version.resolvePreservingPatches(getGameRepository()));
|
||||
version.resolve(getGameRepository()).getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||
Optional<String> libraryVersion = analyzer.getVersion(OPTIFINE);
|
||||
if (OPTIFINE.matchLibrary(library) && libraryVersion.isPresent()) {
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version, library, integrityCheck)) {
|
||||
tasks.add(installLibraryAsync(gameVersion.get(), version, OPTIFINE.getPatchId(), libraryVersion.get()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Task.allOf(tasks);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<Version> installLibraryAsync(String gameVersion, Version baseVersion, String libraryId, String libraryVersion) {
|
||||
if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved");
|
||||
|
||||
@@ -32,21 +32,15 @@ import java.util.Map;
|
||||
public class DefaultGameBuilder extends GameBuilder {
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DownloadProvider downloadProvider;
|
||||
|
||||
public DefaultGameBuilder(DefaultDependencyManager dependencyManager) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.downloadProvider = dependencyManager.getPrimaryDownloadProvider();
|
||||
}
|
||||
|
||||
public DefaultDependencyManager getDependencyManager() {
|
||||
return dependencyManager;
|
||||
}
|
||||
|
||||
public DownloadProvider getDownloadProvider() {
|
||||
return downloadProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> buildAsync() {
|
||||
List<String> stages = new ArrayList<>();
|
||||
@@ -66,7 +60,7 @@ public class DefaultGameBuilder extends GameBuilder {
|
||||
stages.add(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion()));
|
||||
}
|
||||
|
||||
return libraryTask.thenComposeAsync(dependencyManager.getGameRepository()::save).whenComplete(exception -> {
|
||||
return libraryTask.thenComposeAsync(dependencyManager.getGameRepository()::saveAsync).whenComplete(exception -> {
|
||||
if (exception != null)
|
||||
dependencyManager.getGameRepository().removeVersionFromDisk(name);
|
||||
}).withStagesHint(stages);
|
||||
|
||||
@@ -56,6 +56,16 @@ public interface DependencyManager {
|
||||
*/
|
||||
Task<?> checkLibraryCompletionAsync(Version version, boolean integrityCheck);
|
||||
|
||||
/**
|
||||
* Check if patches of this version in complete.
|
||||
* If not, reinstall the patch if possible.
|
||||
*
|
||||
* @param version the version to be checked
|
||||
* @param integrityCheck check if some libraries are corrupt.
|
||||
* @return the task to check patches completion.
|
||||
*/
|
||||
Task<?> checkPatchCompletionAsync(Version version, boolean integrityCheck);
|
||||
|
||||
/**
|
||||
* The builder to build a brand new game then libraries such as Forge, LiteLoader and OptiFine.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The service provider that provides Minecraft online file downloads.
|
||||
*
|
||||
@@ -28,6 +35,10 @@ public interface DownloadProvider {
|
||||
|
||||
String getAssetBaseURL();
|
||||
|
||||
default List<URL> getAssetObjectCandidates(String assetObjectLocation) {
|
||||
return Collections.singletonList(NetworkUtils.toURL(getAssetBaseURL() + assetObjectLocation));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject into original URL provided by Mojang and Forge.
|
||||
*
|
||||
@@ -40,9 +51,26 @@ public interface DownloadProvider {
|
||||
String injectURL(String baseURL);
|
||||
|
||||
/**
|
||||
* the specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
|
||||
* Inject into original URL provided by Mojang and Forge.
|
||||
*
|
||||
* @param id the id of specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
|
||||
* Since there are many provided URLs that are written in JSONs and are unmodifiable,
|
||||
* this method provides a way to change them.
|
||||
*
|
||||
* @param baseURL original URL provided by Mojang and Forge.
|
||||
* @return the URL that is equivalent to [baseURL], but belongs to your own service provider.
|
||||
*/
|
||||
default List<URL> injectURLWithCandidates(String baseURL) {
|
||||
return Collections.singletonList(NetworkUtils.toURL(injectURL(baseURL)));
|
||||
}
|
||||
|
||||
default List<URL> injectURLsWithCandidates(List<String> urls) {
|
||||
return urls.stream().flatMap(url -> injectURLWithCandidates(url).stream()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* the specific version list that this download provider provides. i.e. "fabric", "forge", "liteloader", "game", "optifine"
|
||||
*
|
||||
* @param id the id of specific version list that this download provider provides. i.e. "fabric", "forge", "liteloader", "game", "optifine"
|
||||
* @return the version list
|
||||
* @throws IllegalArgumentException if the version list does not exist
|
||||
*/
|
||||
|
||||
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user