Compare commits

...
Author SHA1 Message Date
huanghongxun a141c6b137 Display changelog when update 2019-05-09 01:51:51 +08:00
huanghongxun 300efb99d2 Fix #540 2019-05-09 00:49:10 +08:00
yushijinhun 6fb3610bcc Refresh skins on startup
Mentioned in #539
2019-05-02 12:40:46 +08:00
yushijinhun 43fb3ca10a Fix illegal texturePayload may cause crash 2019-05-02 12:35:43 +08:00
yushijinhun adc360eaaa Fix #548 2019-05-02 00:12:33 +08:00
huanghongxun 5e659352d7 Support install Forge/OptiFine from local file 2019-04-30 20:26:31 +08:00
huanghongxun 6595e0a3cf Remove authlib-injector-servers.fxml 2019-04-30 00:29:19 +08:00
huanghongxun c43b2b3bb9 Fix potential "not on UI thread" exception 2019-04-30 00:28:22 +08:00
huanghongxun ea19e6efd8 Add Fabric installerItem in list page 2019-04-29 17:57:04 +08:00
huanghongxun 55d38f2b36 Fix importing fabric multimc instance 2019-04-28 14:18:04 +08:00
huanghongxun e0abf3e0c1 Support fabric detection 2019-04-28 14:17:28 +08:00
huanghongxun 974cba8dee Suppress NPE in LogWindow 2019-04-25 13:56:44 +08:00
huanghongxun 019cf5fc8f Suppress NPE 2019-04-25 13:50:51 +08:00
huanghongxun 1956ba7a73 Force refreshing version list when remove version from disk 2019-04-25 13:39:07 +08:00
huanghongxun c209617ee7 Try to fix ConcurrentModificationException 2019-04-25 13:38:44 +08:00
huanghongxun f4b5e061a4 Suppress NPE 2019-04-25 13:38:12 +08:00
huanghongxun 06b67a4b9a Suppress image resource not found 2019-04-25 12:56:04 +08:00
huanghongxun a385265956 Fix not on ui thread 2019-04-25 11:31:01 +08:00
huanghongxun 6ac32a40c7 Suppress UncheckedIOException thrown by loading alex and steve skin image. 2019-04-25 11:28:49 +08:00
huanghongxun 20c612c660 Fix #537 2019-04-25 11:20:58 +08:00
huanghongxun 7e8cc5fe6c Fix #547 2019-04-25 11:16:59 +08:00
huanghongxun 9ed14b227b Workaround fix for NPE when remove mods 2019-04-25 10:49:25 +08:00
huanghongxun ee466d0f4c Fix #545 2019-04-03 17:02:08 +08:00
huanghongxun b198eea9e4 Friendly prompt if response code is not OK 2019-04-03 14:05:14 +08:00
huanghongxun 8c7580488e More friendly prompt for SocketTimeoutException 2019-03-24 14:30:58 +08:00
huanghongxun 3f4364c67c Fix cannot launch a game multiple times 2019-03-24 12:14:13 +08:00
huanghongxun 23e2df5e58 Unzip natives to /versions/ reducing trashes in temp 2019-03-21 11:08:00 +08:00
huanghongxun 79136db162 Lower XZ preset 2019-03-19 11:11:59 +08:00
huanghongxun 3d008dcab5 Make DatapackListPage multiple-selectable 2019-03-19 10:31:02 +08:00
huanghongxun 5953cc6411 Add filter to hide world that version mismatch 2019-03-19 00:04:52 +08:00
huanghongxun 2dce675fe2 Ignore all color formatting in world name. 2019-03-18 23:24:50 +08:00
huanghongxun aa013a86dd Add upgrade button on Forge, LiteLoader, OptiFine 2019-03-18 23:17:53 +08:00
huanghongxun f866f692e2 Hide ForgeNew/OldInstallTask 2019-03-18 20:17:03 +08:00
huanghongxun 68a2514cef Disable mod pane when Forge/Liteloader is not installed. 2019-03-18 20:14:31 +08:00
huanghongxun 32562d7435 Revert JFoenix 2019-03-18 13:44:38 +08:00
huanghongxun 78be1aaa7c Fix blank space in bottom of settings page 2019-03-18 13:43:17 +08:00
huanghongxun 1eb4b1ca9e Fix unable to delete the version which failed to download 2019-03-05 19:37:37 +08:00
huanghongxun bf37db2cbe Fix #538 2019-03-05 19:32:24 +08:00
huanghongxun 5b30617a6b Forbid user from installing both Forge 1.13 and OptiFine simultaneously 2019-03-04 12:27:09 +08:00
huanghongxun 266c686a3a Fix forge 1.13 installation 2019-03-04 11:39:30 +08:00
huanghongxun dc1df98277 Fix compilation error 2019-02-24 23:13:31 +08:00
huanghongxun def45cc3e4 Fix NumberFormatException when parsing forge version list json 2019-02-24 23:11:09 +08:00
huanghongxun 8007f1958e Rename isDependentsSucceeded to isDependentSucceeded 2019-02-24 17:00:17 +08:00
huanghongxun b22e724a3f Remove SilentException 2019-02-24 12:39:40 +08:00
huanghongxun ed66b016db Rename then-series functions 2019-02-24 12:39:39 +08:00
yushijinhun 34890f21b3 Update issue template 2019-02-24 12:29:58 +08:00
yushijinhun 9f85e4a63b Do not run GameVersion.minecraftVersion on UI thread 2019-02-24 12:25:19 +08:00
yushijinhun 7fcd4d94a6 Merge branch 'javafx' of github.com:huanghongxun/HMCL into javafx 2019-02-23 23:52:26 +08:00
yushijinhun 84fbca7905 Remove unused code 2019-02-23 23:48:23 +08:00
huanghongxun 9add2387b9 Add comments to FileUtils and IOUtils 2019-02-23 23:42:05 +08:00
huanghongxun a388450f94 Remove AutoTypingMap 2019-02-23 22:50:01 +08:00
huanghongxun fff7abc2ac finalized no longer passes AutoTypingMap as callback parameter 2019-02-23 22:50:00 +08:00
yushijinhun e5587418c7 Remove SafeIntStringConverter 2019-02-23 22:36:38 +08:00
yushijinhun 7c89fac1e9 Rename MultiStepBinding->BindingMapping 2019-02-23 22:14:29 +08:00
yushijinhun bc9a9ffb72 rewrite MultiStepBinding.asyncMap 2019-02-23 21:58:44 +08:00
yushijinhun 46d27a04a2 Fix auth server list in add account pane 2019-02-23 21:49:38 +08:00
huanghongxun 0f33981c96 Fix #534 2019-02-23 19:13:24 +08:00
yushijinhun b396e0b208 Fix compilation error 2019-02-23 18:41:39 +08:00
yushijinhun 1bf2f17538 Fix #533 2019-02-23 18:38:43 +08:00
huanghongxun 608a531655 Remove AtomicReference 2019-02-20 20:18:34 +08:00
huanghongxun e57180eb15 remove lastException in task.variables 2019-02-20 20:07:43 +08:00
huanghongxun 3ae826d9ca Fix task.getLastException does not receive subtasks' value 2019-02-20 20:07:11 +08:00
huanghongxun e940845c60 Fix not correct mod installation 2019-02-20 16:40:26 +08:00
huanghongxun d8aa989cc4 Fix #532 2019-02-19 23:15:45 +08:00
huanghongxun 31ed4268b6 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2019-02-19 19:39:23 +08:00
huanghongxun c698eaac6f Add sponsor information 2019-02-19 19:38:37 +08:00
huanghongxun d0dd23fa14 Fix compilation error along with openjfx 2019-02-19 19:36:23 +08:00
yushijinhun c6f1aa21c5 Log exception in login pane 2019-02-19 18:56:36 +08:00
huanghongxun a3dc7798e7 fetch Forge remote version list only from BMCLAPI 2019-02-17 14:01:05 +08:00
huanghongxun 88f6fd1965 Set cursor in button to hand by default 2019-02-17 13:57:49 +08:00
huanghongxun 9062c2e447 Display more friendly error message when fail to install library 2019-02-17 13:45:10 +08:00
huanghongxun e3d2a4bf9d Update JFoenix to fix #523 2019-02-16 01:52:37 +08:00
huanghongxun c2d609cdb4 Use TableView in ModListPage to allow multiple selection 2019-02-15 19:39:43 +08:00
huanghongxun b3721e29c6 Update copyright statement 2019-02-14 16:20:14 +08:00
huanghongxun eab4463c2d HMCLauncher: verify if JVM version is between 8 and 10 2019-02-14 15:34:52 +08:00
yushijinhun 394873864f Fix #526 2019-02-13 20:09:51 +08:00
yushijinhun 758216245f Add missing license headers 2019-02-13 20:05:01 +08:00
huanghongxun 6edfd112a2 Replace "close" icon to "delete" icon on ModItem 2019-02-13 17:51:01 +08:00
huanghongxun 625215b068 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2019-02-13 17:46:18 +08:00
huanghongxun 4759e4957c refactor 2019-02-13 17:46:11 +08:00
huanghongxun b62f9494ae Have a placeholder in the bottom of LIstPage 2019-02-12 21:39:01 +08:00
huanghongxun 12b47e7d51 Better prompt of LibraryDownloadException 2019-02-12 21:30:45 +08:00
huanghongxun 9ced29aa93 Display furnace image as default for modded 1.13 game 2019-02-11 19:53:55 +08:00
huanghongxun 5095640ba2 Report memory usage when OutOfMemoryError occurred. 2019-02-11 19:47:17 +08:00
huanghongxun c889d7cb3f Download missing game jar when launching 2019-02-11 19:36:19 +08:00
huanghongxun 84636f3f50 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2019-02-10 10:51:39 +08:00
huanghongxun 9a6f07d7c9 make DefaultGameRepositoy.readVersionJson nonnull. 2019-02-10 10:51:04 +08:00
huanghongxun e8f5088049 Support automatic installation of Forge 1.13. 2019-02-10 10:49:48 +08:00
yushijinhun 0bee306d67 css clean up 2019-02-09 21:44:14 +08:00
yushijinhun 8d75b2d413 Fix #519 2019-02-09 21:37:26 +08:00
huanghongxun 426ea607e8 HMCLauncher: try bundled JRE 2019-02-08 22:05:16 +08:00
yushijinhun 507cade50d polish authlib-injector 2019-02-08 17:08:23 +08:00
huanghongxun f0c7f54cc6 Redownload asset index when install new game 2019-02-08 15:54:28 +08:00
yushijinhun 62c38482f6 polish 639bc58e84
see https://github.com/huanghongxun/HMCL/commit/639bc58e84b68a7b508ed9ba0a701a13b2fac1b4#r32215378
2019-02-08 14:55:58 +08:00
yushijinhun 0ff6ca18ed Use https for bmclapi 2019-02-08 14:35:16 +08:00
huanghongxun bbe6c388a3 Merge branch 'skin-refactor' into javafx 2019-02-06 22:53:38 +08:00
huanghongxun 639bc58e84 Support version identification of Minecraft 1.14 2019-02-06 21:54:51 +08:00
huanghongxun c0bcafd747 Revert "Use JFXListView in place of custom components to accelerate rendering"
This reverts commit 0813211
2019-02-06 17:14:49 +08:00
huanghongxun bcaa019551 Only maintain game version json with launchwrapper installed.
Forge modlauncher is not supported right now.
2019-02-06 15:14:08 +08:00
huanghongxun 38725abc48 DownloadInfo.url can be null in forge 1.13.2 version json 2019-02-06 13:35:16 +08:00
yushijinhun 68a02966d0 Skip parsing json file if it does not exist 2019-02-05 14:31:33 +08:00
191 changed files with 4796 additions and 2550 deletions

No files matched your search

+3 -3
View File
@@ -6,7 +6,7 @@
我们发现,对于不经常检查邮箱的人来说,GitHub issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
@@ -24,5 +24,5 @@
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
* 游戏崩溃报告(如果有):
* 启动器崩溃报告(如果有):
* 启动器日志文件(在启动器设置中打开日志文件夹并将文件夹内所有文件打包发在这里):
* 问题描述(如何触发问题):
* 启动器日志文件(出现问题后点击启动器设置中的【导出启动器日志】按钮):
* 问题描述(如何触发问题):
+2 -2
View File
@@ -112,7 +112,7 @@ def createExecutable(String suffix, String header) {
processResources {
ext.convertToBSS = { String resource ->
exclude resource
// exclude resource
doFirst {
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
@@ -140,7 +140,7 @@ task makePack(dependsOn: jar) {
task makePackXz(dependsOn: makePack) doLast {
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(6)).withCloseable { it << makePack.outputPath.bytes }
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(5)).withCloseable { it << makePack.outputPath.bytes }
createChecksum(packXz)
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
@@ -25,7 +24,9 @@ import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -39,6 +40,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -97,7 +99,7 @@ public final class Launcher extends Application {
public static void stopApplication() {
LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
@@ -111,7 +113,7 @@ public final class Launcher extends Application {
public static void stopWithoutPlatform() {
LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
@@ -35,6 +35,7 @@ public final class Metadata {
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String CHANGELOG_URL = "https://hmcl.huangyuhui.net/changelog/";
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
@@ -20,9 +20,6 @@ package org.jackhuang.hmcl.game;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
@@ -34,6 +31,8 @@ import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
private final Map<String, VersionSetting> versionSettings = new HashMap<>();
@@ -80,13 +79,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
}
@Override
public void refreshVersions() {
EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this));
refreshVersionsImpl();
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
}
public void changeDirectory(File newDirectory) {
setBaseDirectory(newDirectory);
refreshVersionsAsync().start();
@@ -160,16 +152,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
public Image getVersionIconImage(String id) {
if (id == null || !isLoaded())
return new Image("/assets/img/grass.png");
return newImage("/assets/img/grass.png");
Version version = getVersion(id);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass())
|| version.getMainClass().startsWith("net.fabricmc")
|| "cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
return newImage("/assets/img/furnace.png");
else
return new Image("/assets/img/grass.png");
return newImage("/assets/img/grass.png");
}
public boolean saveVersionSetting(String id) {
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.game;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
@@ -38,7 +39,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
@@ -122,14 +123,14 @@ public final class LauncherHelper {
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.then(variables -> {
.then(() -> {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version);
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS)))
.then(var -> {
.then(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
@@ -141,44 +142,42 @@ public final class LauncherHelper {
}
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN)))
.then(Task.of(i18n("account.methods"), variables -> {
.thenCompose(() -> Task.ofResult(i18n("account.methods"), () -> {
try {
variables.set("account", account.logIn());
return account.logIn();
} catch (CredentialExpiredException e) {
LOG.info("Credential has expired: " + e);
variables.set("account", DialogController.logIn(account));
return DialogController.logIn(account);
} catch (AuthenticationException e) {
LOG.warning("Authentication failed, try playing offline: " + e);
variables.set("account",
account.playOffline().orElseThrow(() -> e));
return account.playOffline().orElseThrow(() -> e);
}
}))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING)))
.then(Task.of(variables -> {
variables.set("launcher", new HMCLGameLauncher(
repository,
selectedVersion,
variables.get("account"),
setting.toLaunchOptions(profile.getGameDir()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
));
}))
.then(variables -> {
DefaultLauncher launcher = variables.get("launcher");
.thenApply(Schedulers.javafx(), authInfo -> {
emitStatus(LoadingState.LAUNCHING);
return authInfo;
})
.thenApply(authInfo -> new HMCLGameLauncher(
repository,
selectedVersion,
authInfo,
setting.toLaunchOptions(profile.getGameDir()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(authInfo, setting, gameVersion.isPresent())
))
.thenCompose(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
} else {
return new LaunchTask<>(() -> {
return new LaunchTask<ManagedProcess>(() -> {
launcher.makeLaunchScript(scriptFile);
return null;
}).setName(i18n("version.launch_script"));
}
})
.then(Task.of(variables -> {
.thenAccept(process -> { // process is LaunchTask's result
if (scriptFile == null) {
ManagedProcess process = variables.get(LaunchTask.LAUNCH_ID);
PROCESSES.add(process);
if (launcherVisibility == LauncherVisibility.CLOSE)
Launcher.stopApplication();
@@ -187,13 +186,13 @@ public final class LauncherHelper {
process.stop();
it.fireEvent(new DialogCloseEvent());
});
} else
} else {
Platform.runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}))
}
})
.executor();
launchingStepsPane.setExecutor(executor, false);
@@ -236,7 +235,7 @@ public final class LauncherHelper {
}
Controllers.dialog(message,
scriptFile == null ? i18n("launch.failed") : i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE);
MessageType.ERROR);
}
}
});
@@ -263,7 +262,7 @@ public final class LauncherHelper {
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse("Unknown"));
JavaVersion java = setting.getJavaVersion();
if (java == null) {
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
@@ -282,10 +281,10 @@ public final class LauncherHelper {
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, null);
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, onAccept);
}
flag = true;
}
@@ -301,10 +300,10 @@ public final class LauncherHelper {
if (java8.isPresent()) {
java8required = true;
setting.setJavaVersion(java8.get());
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.corrected"), i18n("message.info"), MessageBox.INFORMATION_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.corrected"), i18n("message.info"), MessageType.INFORMATION, onAccept);
flag = true;
} else {
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, null);
flag = true;
}
}
@@ -319,7 +318,7 @@ public final class LauncherHelper {
newJavaRequired = true;
setting.setJavaVersion(java8.get());
} else {
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, onAccept);
flag = true;
}
}
@@ -351,7 +350,7 @@ public final class LauncherHelper {
if (java64.isPresent()) {
setting.setJavaVersion(java64.get());
} else {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
}
@@ -361,13 +360,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"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageType.ERROR, onAccept);
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"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
@@ -379,7 +378,7 @@ 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"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
}
}
@@ -400,7 +399,10 @@ public final class LauncherHelper {
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(Controllers.getStage()::show);
Platform.runLater(() -> {
Optional.ofNullable(Controllers.getStage())
.ifPresent(Stage::show);
});
break;
case KEEP:
// No operations here
@@ -429,13 +431,6 @@ public final class LauncherHelper {
public void execute() throws Exception {
setResult(supplier.get());
}
@Override
public String getId() {
return LAUNCH_ID;
}
static final String LAUNCH_ID = "launch";
}
/**
@@ -110,13 +110,13 @@ public final class ModpackHelper {
if (modpack.getManifest() instanceof CurseManifest)
return new CurseInstallTask(profile.getDependency(), zipFile, modpack, ((CurseManifest) modpack.getManifest()), name)
.finalized(Schedulers.defaultScheduler(), ExceptionalConsumer.fromRunnable(success), failure);
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof HMCLModpackManifest)
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
.finalized(Schedulers.defaultScheduler(), ExceptionalConsumer.fromRunnable(success), failure);
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.finalized(Schedulers.defaultScheduler(), ExceptionalConsumer.fromRunnable(success), failure)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.then(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
}
@@ -17,11 +17,21 @@
*/
package org.jackhuang.hmcl.game;
import static java.util.Collections.singletonMap;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.embed.swing.SwingFXUtils;
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.task.FileDownloadTask;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.awt.Graphics2D;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
@@ -33,27 +43,16 @@ import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
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.javafx.MultiStepBinding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
/**
* @author yushijinhun
@@ -69,8 +68,8 @@ public final class TexturesLoader {
private final Map<String, String> metadata;
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
this.image = image;
this.metadata = metadata;
this.image = requireNonNull(image);
this.metadata = requireNonNull(metadata);
}
public BufferedImage getImage() {
@@ -98,6 +97,10 @@ public final class TexturesLoader {
}
public static LoadedTexture loadTexture(Texture texture) throws IOException {
if (StringUtils.isBlank(texture.getUrl())) {
throw new IOException("Texture url is empty");
}
Path file = getTexturePath(texture);
if (!Files.isRegularFile(file)) {
// download it
@@ -118,7 +121,11 @@ public final class TexturesLoader {
try (InputStream in = Files.newInputStream(file)) {
img = ImageIO.read(in);
}
return new LoadedTexture(img, texture.getMetadata());
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
metadata = emptyMap();
}
return new LoadedTexture(img, metadata);
}
// ====
@@ -128,8 +135,9 @@ public final class TexturesLoader {
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
}
private static void loadDefaultSkin(String path, TextureModel model) {
try (InputStream in = TexturesLoader.class.getResourceAsStream(path)) {
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);
@@ -142,7 +150,7 @@ public final class TexturesLoader {
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
return MultiStepBinding.of(service.getProfileRepository().binding(uuid))
return BindingMapping.of(service.getProfileRepository().binding(uuid))
.map(profile -> profile
.flatMap(it -> {
try {
@@ -152,20 +160,23 @@ public final class TexturesLoader {
return Optional.empty();
}
})
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN))))
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return getDefaultSkin(TextureModel.detectModelName(texture.getMetadata()));
}
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
} else {
return uuidFallback;
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback, POOL);
}, uuidFallback);
}
// ====
@@ -180,19 +191,16 @@ public final class TexturesLoader {
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
8 * scale, 8 * scale, 16 * scale, 16 * scale,
null);
if (skin.getWidth() == skin.getHeight()) {
g.drawImage(skin,
0, 0, size, size,
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
}
g.drawImage(skin,
0, 0, size, size,
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
g.dispose();
return avatar;
}
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return MultiStepBinding.of(skinBinding(service, uuid))
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(img -> SwingFXUtils.toFXImage(img, null));
}
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -32,9 +31,7 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.javafx.ImmediateObjectProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateStringProperty;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.io.File;
@@ -42,6 +39,7 @@ import java.lang.reflect.Type;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
/**
*
@@ -90,9 +88,9 @@ public final class Profile implements Observable {
return global.get();
}
private final ImmediateStringProperty name;
private final SimpleStringProperty name;
public ImmediateStringProperty nameProperty() {
public StringProperty nameProperty() {
return name;
}
@@ -131,8 +129,8 @@ public final class Profile implements Observable {
}
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
this.name = new ImmediateStringProperty(this, "name", name);
gameDir = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir);
this.name = new SimpleStringProperty(this, "name", name);
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
repository = new HMCLGameRepository(this, initialGameDir);
this.global.set(global == null ? new VersionSetting() : global);
this.selectedVersion.set(selectedVersion);
@@ -146,7 +144,7 @@ public final class Profile implements Observable {
}
private void checkSelectedVersion() {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (!repository.isLoaded()) return;
String newValue = selectedVersion.get();
if (!repository.hasVersion(newValue)) {
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.setting;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.*;
@@ -36,6 +35,7 @@ import java.util.stream.Collectors;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Profiles {
@@ -162,7 +162,7 @@ public final class Profiles {
});
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
Profile profile = selectedProfile.get();
if (profile != null && profile.getRepository() == event.getSource()) {
selectedVersion.bind(profile.selectedVersionProperty());
@@ -25,8 +25,10 @@ import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.File;
import java.io.IOException;
@@ -80,7 +82,7 @@ public class Theme {
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
@@ -132,7 +134,8 @@ public class Theme {
}
public static ObjectBinding<Color> foregroundFillBinding() {
return Bindings.createObjectBinding(() -> config().getTheme().getForegroundColor(), config().themeProperty());
return BindingMapping.of(config().themeProperty())
.map(Theme::getForegroundColor);
}
public static ObjectBinding<Color> blackFillBinding() {
@@ -21,13 +21,18 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.javafx.ImmediateBooleanProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateIntegerProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateObjectProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateStringProperty;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -60,9 +65,9 @@ public final class VersionSetting {
this.global = global;
}
private final ImmediateBooleanProperty usesGlobalProperty = new ImmediateBooleanProperty(this, "usesGlobal", false);
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", false);
public ImmediateBooleanProperty usesGlobalProperty() {
public BooleanProperty usesGlobalProperty() {
return usesGlobalProperty;
}
@@ -84,9 +89,9 @@ public final class VersionSetting {
// java
private final ImmediateStringProperty javaProperty = new ImmediateStringProperty(this, "java", "");
private final StringProperty javaProperty = new SimpleStringProperty(this, "java", "");
public ImmediateStringProperty javaProperty() {
public StringProperty javaProperty() {
return javaProperty;
}
@@ -110,7 +115,7 @@ public final class VersionSetting {
setDefaultJavaPath(null);
}
private final ImmediateStringProperty defaultJavaPathProperty = new ImmediateStringProperty(this, "defaultJavaPath", "");
private final StringProperty defaultJavaPathProperty = new SimpleStringProperty(this, "defaultJavaPath", "");
/**
* Path to Java executable, or null if user customizes java directory.
@@ -124,9 +129,9 @@ public final class VersionSetting {
defaultJavaPathProperty.set(defaultJavaPath);
}
private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", "");
private final StringProperty javaDirProperty = new SimpleStringProperty(this, "javaDir", "");
public ImmediateStringProperty javaDirProperty() {
public StringProperty javaDirProperty() {
return javaDirProperty;
}
@@ -141,9 +146,9 @@ public final class VersionSetting {
javaDirProperty.set(javaDir);
}
private final ImmediateStringProperty wrapperProperty = new ImmediateStringProperty(this, "wrapper", "");
private final StringProperty wrapperProperty = new SimpleStringProperty(this, "wrapper", "");
public ImmediateStringProperty wrapperProperty() {
public StringProperty wrapperProperty() {
return wrapperProperty;
}
@@ -158,9 +163,9 @@ public final class VersionSetting {
wrapperProperty.set(wrapper);
}
private final ImmediateStringProperty permSizeProperty = new ImmediateStringProperty(this, "permSize", "");
private final StringProperty permSizeProperty = new SimpleStringProperty(this, "permSize", "");
public ImmediateStringProperty permSizeProperty() {
public StringProperty permSizeProperty() {
return permSizeProperty;
}
@@ -175,9 +180,9 @@ public final class VersionSetting {
permSizeProperty.set(permSize);
}
private final ImmediateIntegerProperty maxMemoryProperty = new ImmediateIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
public ImmediateIntegerProperty maxMemoryProperty() {
public IntegerProperty maxMemoryProperty() {
return maxMemoryProperty;
}
@@ -195,9 +200,9 @@ public final class VersionSetting {
/**
* The minimum memory that JVM can allocate for heap.
*/
private final ImmediateObjectProperty<Integer> minMemoryProperty = new ImmediateObjectProperty<>(this, "minMemory", null);
private final ObjectProperty<Integer> minMemoryProperty = new SimpleObjectProperty<>(this, "minMemory", null);
public ImmediateObjectProperty<Integer> minMemoryProperty() {
public ObjectProperty<Integer> minMemoryProperty() {
return minMemoryProperty;
}
@@ -209,9 +214,9 @@ public final class VersionSetting {
minMemoryProperty.set(minMemory);
}
private final ImmediateStringProperty preLaunchCommandProperty = new ImmediateStringProperty(this, "precalledCommand", "");
private final StringProperty preLaunchCommandProperty = new SimpleStringProperty(this, "precalledCommand", "");
public ImmediateStringProperty preLaunchCommandProperty() {
public StringProperty preLaunchCommandProperty() {
return preLaunchCommandProperty;
}
@@ -229,9 +234,9 @@ public final class VersionSetting {
// options
private final ImmediateStringProperty javaArgsProperty = new ImmediateStringProperty(this, "javaArgs", "");
private final StringProperty javaArgsProperty = new SimpleStringProperty(this, "javaArgs", "");
public ImmediateStringProperty javaArgsProperty() {
public StringProperty javaArgsProperty() {
return javaArgsProperty;
}
@@ -246,9 +251,9 @@ public final class VersionSetting {
javaArgsProperty.set(javaArgs);
}
private final ImmediateStringProperty minecraftArgsProperty = new ImmediateStringProperty(this, "minecraftArgs", "");
private final StringProperty minecraftArgsProperty = new SimpleStringProperty(this, "minecraftArgs", "");
public ImmediateStringProperty minecraftArgsProperty() {
public StringProperty minecraftArgsProperty() {
return minecraftArgsProperty;
}
@@ -263,9 +268,9 @@ public final class VersionSetting {
minecraftArgsProperty.set(minecraftArgs);
}
private final ImmediateBooleanProperty noJVMArgsProperty = new ImmediateBooleanProperty(this, "noJVMArgs", false);
private final BooleanProperty noJVMArgsProperty = new SimpleBooleanProperty(this, "noJVMArgs", false);
public ImmediateBooleanProperty noJVMArgsProperty() {
public BooleanProperty noJVMArgsProperty() {
return noJVMArgsProperty;
}
@@ -280,9 +285,9 @@ public final class VersionSetting {
noJVMArgsProperty.set(noJVMArgs);
}
private final ImmediateBooleanProperty notCheckJVMProperty = new ImmediateBooleanProperty(this, "notCheckJVM", false);
private final BooleanProperty notCheckJVMProperty = new SimpleBooleanProperty(this, "notCheckJVM", false);
public ImmediateBooleanProperty notCheckJVMProperty() {
public BooleanProperty notCheckJVMProperty() {
return notCheckJVMProperty;
}
@@ -297,9 +302,9 @@ public final class VersionSetting {
notCheckJVMProperty.set(notCheckJVM);
}
private final ImmediateBooleanProperty notCheckGameProperty = new ImmediateBooleanProperty(this, "notCheckGame", false);
private final BooleanProperty notCheckGameProperty = new SimpleBooleanProperty(this, "notCheckGame", false);
public ImmediateBooleanProperty notCheckGameProperty() {
public BooleanProperty notCheckGameProperty() {
return notCheckGameProperty;
}
@@ -314,9 +319,9 @@ public final class VersionSetting {
notCheckGameProperty.set(notCheckGame);
}
private final ImmediateBooleanProperty showLogsProperty = new ImmediateBooleanProperty(this, "showLogs", false);
private final BooleanProperty showLogsProperty = new SimpleBooleanProperty(this, "showLogs", false);
public ImmediateBooleanProperty showLogsProperty() {
public BooleanProperty showLogsProperty() {
return showLogsProperty;
}
@@ -333,14 +338,14 @@ public final class VersionSetting {
// Minecraft settings.
private final ImmediateStringProperty serverIpProperty = new ImmediateStringProperty(this, "serverIp", "");
private final StringProperty serverIpProperty = new SimpleStringProperty(this, "serverIp", "");
public ImmediateStringProperty serverIpProperty() {
public StringProperty serverIpProperty() {
return serverIpProperty;
}
/**
* The server ip that will be entered after Minecraft successfully loaded immediately.
* The server ip that will be entered after Minecraft successfully loaded ly.
*
* Format: ip:port or without port.
*/
@@ -353,9 +358,9 @@ public final class VersionSetting {
}
private final ImmediateBooleanProperty fullscreenProperty = new ImmediateBooleanProperty(this, "fullscreen", false);
private final BooleanProperty fullscreenProperty = new SimpleBooleanProperty(this, "fullscreen", false);
public ImmediateBooleanProperty fullscreenProperty() {
public BooleanProperty fullscreenProperty() {
return fullscreenProperty;
}
@@ -370,9 +375,9 @@ public final class VersionSetting {
fullscreenProperty.set(fullscreen);
}
private final ImmediateIntegerProperty widthProperty = new ImmediateIntegerProperty(this, "width", 854);
private final IntegerProperty widthProperty = new SimpleIntegerProperty(this, "width", 854);
public ImmediateIntegerProperty widthProperty() {
public IntegerProperty widthProperty() {
return widthProperty;
}
@@ -392,9 +397,9 @@ public final class VersionSetting {
}
private final ImmediateIntegerProperty heightProperty = new ImmediateIntegerProperty(this, "height", 480);
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
public ImmediateIntegerProperty heightProperty() {
public IntegerProperty heightProperty() {
return heightProperty;
}
@@ -417,9 +422,9 @@ public final class VersionSetting {
* 0 - .minecraft<br/>
* 1 - .minecraft/versions/&lt;version&gt;/<br/>
*/
private final ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty = new ImmediateObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
private final ObjectProperty<EnumGameDirectory> gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
public ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty() {
public ObjectProperty<EnumGameDirectory> gameDirTypeProperty() {
return gameDirTypeProperty;
}
@@ -434,9 +439,9 @@ public final class VersionSetting {
/**
* Your custom gameDir
*/
private final ImmediateStringProperty gameDirProperty = new ImmediateStringProperty(this, "gameDir", "");
private final StringProperty gameDirProperty = new SimpleStringProperty(this, "gameDir", "");
public ImmediateStringProperty gameDirProperty() {
public StringProperty gameDirProperty() {
return gameDirProperty;
}
@@ -455,9 +460,9 @@ public final class VersionSetting {
* 1 - Hide the launcher when the game starts.<br/>
* 2 - Keep the launcher open.<br/>
*/
private final ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty = new ImmediateObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE);
private final ObjectProperty<LauncherVisibility> launcherVisibilityProperty = new SimpleObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE);
public ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty() {
public ObjectProperty<LauncherVisibility> launcherVisibilityProperty() {
return launcherVisibilityProperty;
}
@@ -17,10 +17,8 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
@@ -34,7 +32,11 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountList;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileList;
@@ -45,7 +47,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
@@ -57,6 +59,8 @@ import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
@@ -158,7 +162,7 @@ public final class Controllers {
});
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
mainPage.latestVersionProperty().bind(
MultiStepBinding.of(UpdateChecker.latestVersionProperty())
BindingMapping.of(UpdateChecker.latestVersionProperty())
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
Profiles.registerVersionsListener(profile -> {
@@ -173,7 +177,7 @@ public final class Controllers {
return node;
})
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (profile == Profiles.getSelectedProfile())
mainPage.getVersions().setAll(children);
});
@@ -209,7 +213,7 @@ public final class Controllers {
scene = new Scene(decorator.getDecorator(), 800, 519);
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
stage.getIcons().add(new Image("/assets/img/icon.png"));
stage.getIcons().add(newImage("/assets/img/icon.png"));
stage.setTitle(Metadata.TITLE);
}
@@ -223,14 +227,14 @@ public final class Controllers {
}
public static void dialog(String text, String title) {
dialog(text, title, MessageBox.INFORMATION_MESSAGE);
dialog(text, title, MessageType.INFORMATION);
}
public static void dialog(String text, String title, int type) {
public static void dialog(String text, String title, MessageType type) {
dialog(text, title, type, null);
}
public static void dialog(String text, String title, int type, Runnable onAccept) {
public static void dialog(String text, String title, MessageType type, Runnable onAccept) {
dialog(new MessageDialogPane(text, title, type, onAccept));
}
@@ -22,7 +22,6 @@ import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
@@ -30,6 +29,7 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -67,7 +67,7 @@ public class CrashWindow extends Stage {
Scene scene = new Scene(pane, 800, 480);
setScene(scene);
getIcons().add(new Image("/assets/img/icon.png"));
getIcons().add(newImage("/assets/img/icon.png"));
setTitle(i18n("message.error"));
setOnCloseRequest(e -> System.exit(1));
@@ -17,24 +17,26 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.task.SilentException;
import org.jackhuang.hmcl.ui.account.AccountLoginPane;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public final class DialogController {
public static AuthInfo logIn(Account account) throws Exception {
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
if (account instanceof YggdrasilAccount) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
JFXUtilities.runInFX(() -> {
runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
@@ -42,7 +44,7 @@ public final class DialogController {
Controllers.dialog(pane);
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(SilentException::new);
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
}
return account.logIn();
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
@@ -31,9 +30,11 @@ import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
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;
@@ -44,10 +45,12 @@ import javafx.scene.shape.Rectangle;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -74,6 +77,14 @@ public final class FXUtils {
private FXUtils() {
}
public static void runInFX(Runnable runnable) {
if (Platform.isFxApplicationThread()) {
runnable.run();
} else {
Platform.runLater(runnable);
}
}
public static void checkFxUserThread() {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not on FX application thread; currentThread = "
@@ -153,6 +164,20 @@ public final class FXUtils {
});
}
public static <K, T> void setupCellValueFactory(JFXTreeTableColumn<K, T> column, Function<K, ObservableValue<T>> mapper) {
column.setCellValueFactory(param -> {
if (column.validateValue(param))
return mapper.apply(param.getValue().getValue());
else
return column.getComputedValue(param);
});
}
public static Node wrapMargin(Node node, Insets insets) {
StackPane.setMargin(node, insets);
return new StackPane(node);
}
public static void setValidateWhileTextChanged(Node field, boolean validate) {
if (field instanceof JFXTextField) {
if (validate) {
@@ -251,7 +276,7 @@ public final class FXUtils {
}
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
try {
// Java 8
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
@@ -331,7 +356,7 @@ public final class FXUtils {
}
public static void bindInt(JFXTextField textField, Property<Number> property) {
textField.textProperty().bindBidirectional(property, SafeIntStringConverter.INSTANCE);
textField.textProperty().bindBidirectional(property, SafeStringConverter.fromInteger());
}
public static void unbindInt(JFXTextField textField, Property<Number> property) {
@@ -461,6 +486,21 @@ public final class FXUtils {
}
}
/**
* Suppress IllegalArgumentException since the url is supposed to be correct definitely.
* @param url the url of image. The image resource should be a file within the jar.
* @return the image resource within the jar.
* @see org.jackhuang.hmcl.util.CrashReporter
* @see ResourceNotFoundError
*/
public static Image newImage(String url) {
try {
return new Image(url);
} catch (IllegalArgumentException e) {
throw new ResourceNotFoundError("Cannot access image: " + url, e);
}
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
applyDragListener(node, filter, callback, null);
}
@@ -510,6 +550,7 @@ public final class FXUtils {
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(graphicBuilder.apply(item));
}
}
@@ -17,10 +17,14 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
@@ -30,26 +34,42 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
* @author huangyuhui
*/
public class InstallerItem extends BorderPane {
private final Consumer<InstallerItem> deleteCallback;
@FXML
private Label lblInstallerArtifact;
@FXML
private Label lblInstallerVersion;
public InstallerItem(String artifact, String version, Consumer<InstallerItem> deleteCallback) {
this.deleteCallback = deleteCallback;
FXUtils.loadFXML(this, "/assets/fxml/version/installer-item.fxml");
public InstallerItem(String artifact, String version, @Nullable Runnable upgrade, @Nullable Consumer<InstallerItem> deleteCallback) {
getStyleClass().add("two-line-list-item");
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblInstallerArtifact.setText(artifact);
lblInstallerVersion.setText(i18n("archive.version") + ": " + version);
{
TwoLineListItem item = new TwoLineListItem();
item.setTitle(artifact);
item.setSubtitle(i18n("archive.version") + ": " + version);
setCenter(item);
}
{
HBox hBox = new HBox();
if (upgrade != null) {
JFXButton upgradeButton = new JFXButton();
upgradeButton.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
upgradeButton.getStyleClass().add("toggle-icon4");
FXUtils.installFastTooltip(upgradeButton, i18n("install.change_version"));
upgradeButton.setOnMouseClicked(e -> upgrade.run());
hBox.getChildren().add(upgradeButton);
}
if (deleteCallback != null) {
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
deleteButton.getStyleClass().add("toggle-icon4");
deleteButton.setOnMouseClicked(e -> deleteCallback.accept(this));
hBox.getChildren().add(deleteButton);
}
hBox.setAlignment(Pos.CENTER_RIGHT);
setRight(hBox);
}
}
@FXML
private void onDelete() {
deleteCallback.accept(this);
}
}
@@ -17,15 +17,12 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
@@ -44,6 +41,8 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LeftPaneController extends AdvancedListBox {
@@ -71,12 +70,12 @@ public final class LeftPaneController extends AdvancedListBox {
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setImage(new Image("/assets/img/bookshelf.png"));
gameItem.setImage(newImage("/assets/img/bookshelf.png"));
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.setImage(new Image("/assets/img/command.png"));
launcherSettingsItem.setImage(newImage("/assets/img/command.png"));
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
@@ -112,7 +111,7 @@ public final class LeftPaneController extends AdvancedListBox {
private boolean checkedModpack = false;
private void onRefreshedVersions(HMCLGameRepository repository) {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (!checkedModpack) {
checkedModpack = true;
@@ -120,14 +119,12 @@ public final class LeftPaneController extends AdvancedListBox {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
Task.ofResult(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
.thenResult(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenResult(modpack -> {
AtomicReference<Region> region = new AtomicReference<>();
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), this::checkAccount)).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.installing")));
.thenApply(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenApply(modpack -> ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), this::checkAccount)).executor())
.thenAccept(Schedulers.javafx(), executor -> {
Controllers.taskDialog(executor, i18n("modpack.installing"));
executor.start();
return null;
}).start();
}
}
@@ -18,18 +18,11 @@
package org.jackhuang.hmcl.ui;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
public abstract class ListPage<T extends Node> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
public abstract class ListPage<T extends Node> extends ListPageBase<T> {
private final BooleanProperty refreshable = new SimpleBooleanProperty(this, "refreshable", false);
public abstract void add();
@@ -42,30 +35,6 @@ public abstract class ListPage<T extends Node> extends Control {
return new ListPageSkin(this);
}
public ObservableList<T> getItems() {
return items.get();
}
public void setItems(ObservableList<T> items) {
this.items.set(items);
}
public ListProperty<T> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
public boolean isRefreshable() {
return refreshable.get();
}
@@ -0,0 +1,55 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Control;
public class ListPageBase<T> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
public ObservableList<T> getItems() {
return items.get();
}
public void setItems(ObservableList<T> items) {
this.items.set(items);
}
public ListProperty<T> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
}
@@ -19,12 +19,13 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
@@ -36,6 +37,9 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
super(skinnable);
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
Pane placeholder = new Pane();
VBox list = new VBox();
StackPane contentPane = new StackPane();
{
@@ -43,14 +47,15 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
{
scrollPane.setFitToWidth(true);
VBox list = new VBox();
list.maxWidthProperty().bind(scrollPane.widthProperty());
list.setSpacing(10);
list.setPadding(new Insets(10));
VBox content = new VBox();
content.getChildren().setAll(list, placeholder);
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(list);
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
}
@@ -64,7 +69,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
JFXButton btnAdd = new JFXButton();
FXUtils.setLimitWidth(btnAdd, 40);
FXUtils.setLimitHeight(btnAdd, 40);
btnAdd.getStyleClass().setAll("jfx-button-raised-round");
btnAdd.getStyleClass().add("jfx-button-raised-round");
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1));
btnAdd.setOnMouseClicked(e -> skinnable.add());
@@ -72,7 +77,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
JFXButton btnRefresh = new JFXButton();
FXUtils.setLimitWidth(btnRefresh, 40);
FXUtils.setLimitHeight(btnRefresh, 40);
btnRefresh.getStyleClass().setAll("jfx-button-raised-round");
btnRefresh.getStyleClass().add("jfx-button-raised-round");
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
btnRefresh.setGraphic(SVG.refresh(Theme.whiteFillBinding(), -1, -1));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
@@ -81,12 +86,23 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
FXUtils.onChangeAndOperate(skinnable.refreshableProperty(),
refreshable -> {
if (refreshable) vBox.getChildren().setAll(btnRefresh, btnAdd);
else vBox.getChildren().setAll(btnAdd);
if (refreshable) {
list.setPadding(new Insets(10, 10, 15 + 40 + 15 + 40 + 15, 10));
vBox.getChildren().setAll(btnRefresh, btnAdd);
} else {
list.setPadding(new Insets(10, 10, 15 + 40 + 15, 10));
vBox.getChildren().setAll(btnAdd);
}
});
}
contentPane.getChildren().setAll(scrollPane, vBox);
// Keep a blank space to prevent buttons from blocking up mod items.
BorderPane group = new BorderPane();
group.setPickOnBounds(false);
group.setBottom(vBox);
placeholder.minHeightProperty().bind(vBox.heightProperty());
contentPane.getChildren().setAll(scrollPane, group);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
@@ -25,7 +25,6 @@ import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
@@ -35,6 +34,7 @@ import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.w3c.dom.Document;
@@ -44,6 +44,7 @@ import org.w3c.dom.Node;
import java.util.concurrent.CountDownLatch;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -65,7 +66,7 @@ public final class LogWindow extends Stage {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
setTitle(i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
getIcons().add(newImage("/assets/img/icon.png"));
}
public LogWindow(String text) {
@@ -174,7 +175,7 @@ public final class LogWindow extends Stage {
FXUtils.loadFXML(this, "/assets/fxml/log.fxml");
engine = webView.getEngine();
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/log-window-content.html")))
.replace("${FONT}", config().getFontSize() + "px \"" + config().getFontFamily() + "\""));
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
@@ -201,11 +202,11 @@ public final class LogWindow extends Stage {
if (!flag)
cboLines.getSelectionModel().select(0);
btnFatals.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(fatal.get()) + " fatals", fatal));
btnErrors.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(error.get()) + " errors", error));
btnWarns.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(warn.get()) + " warns", warn));
btnInfos.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(info.get()) + " infos", info));
btnDebugs.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(debug.get()) + " debugs", debug));
btnFatals.textProperty().bind(Bindings.concat(fatal, " fatals"));
btnErrors.textProperty().bind(Bindings.concat(error, " errors"));
btnWarns.textProperty().bind(Bindings.concat(warn, " warns"));
btnInfos.textProperty().bind(Bindings.concat(info, " infos"));
btnDebugs.textProperty().bind(Bindings.concat(debug, " debugs"));
btnFatals.selectedProperty().addListener(o -> specificChanged());
btnErrors.selectedProperty().addListener(o -> specificChanged());
@@ -29,6 +29,10 @@ public final class SVG {
private SVG() {
}
public interface SVGIcon {
Node createIcon(ObjectBinding<? extends Paint> fill, double width, double height);
}
private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) {
SVGPath path = new SVGPath();
path.getStyleClass().add("svg");
@@ -162,4 +166,8 @@ public final class SVG {
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z", fill, width, height);
}
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
}
}
@@ -31,7 +31,7 @@ import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
@@ -228,7 +228,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
try {
Files.write(logFile, Logging.getRawLogs());
} catch (IOException e) {
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + e, null, MessageBox.ERROR_MESSAGE));
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + e, null, MessageType.ERROR));
LOG.log(Level.WARNING, "Failed to export logs", e);
return;
}
@@ -247,4 +247,9 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
protected void onHelp() {
FXUtils.openLink(Metadata.HELP_URL);
}
@Override
protected void onSponsor() {
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
}
}
@@ -19,8 +19,10 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
@@ -71,10 +73,42 @@ public abstract class SettingsView extends StackPane {
{
VBox rootPane = new VBox();
rootPane.setStyle("-fx-padding: 18;");
rootPane.setPadding(new Insets(36, 18, 36, 18));
{
ComponentList settingsPane = new ComponentList();
{
{
StackPane sponsorPane = new StackPane();
sponsorPane.setCursor(Cursor.HAND);
sponsorPane.setOnMouseClicked(e -> onSponsor());
GridPane gridPane = new GridPane();
ColumnConstraints col = new ColumnConstraints();
col.setHgrow(Priority.SOMETIMES);
col.setMaxWidth(Double.POSITIVE_INFINITY);
gridPane.getColumnConstraints().setAll(col);
RowConstraints row = new RowConstraints();
row.setMinHeight(Double.NEGATIVE_INFINITY);
row.setValignment(VPos.TOP);
row.setVgrow(Priority.SOMETIMES);
gridPane.getRowConstraints().setAll(row);
{
Label label = new Label(i18n("sponsor.hmcl"));
label.setWrapText(true);
label.setTextAlignment(TextAlignment.JUSTIFY);
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
sponsorPane.getChildren().setAll(gridPane);
settingsPane.getContent().add(sponsorPane);
}
ComponentSublist updatePane = new ComponentSublist();
updatePane.setTitle(i18n("update"));
updatePane.setHasSubtitle(true);
@@ -83,7 +117,7 @@ public abstract class SettingsView extends StackPane {
lblUpdate = new Label(i18n("update"));
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdateSub.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
updatePane.setHeaderLeft(headerLeft);
@@ -92,7 +126,7 @@ public abstract class SettingsView extends StackPane {
{
btnUpdate = new JFXButton();
btnUpdate.setOnMouseClicked(e -> onUpdate());
btnUpdate.getStyleClass().setAll("toggle-icon4");
btnUpdate.getStyleClass().add("toggle-icon4");
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 20, 20));
updatePane.setHeaderRight(btnUpdate);
@@ -124,7 +158,7 @@ public abstract class SettingsView extends StackPane {
Label help = new Label(i18n("help"));
Label helpSubtitle = new Label(i18n("help.detail"));
helpSubtitle.getStyleClass().setAll("subtitle-label");
helpSubtitle.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(help, helpSubtitle);
updatePane.setLeft(headerLeft);
@@ -133,7 +167,7 @@ public abstract class SettingsView extends StackPane {
{
JFXButton btnExternal = new JFXButton();
btnExternal.setOnMouseClicked(e -> onHelp());
btnExternal.getStyleClass().setAll("toggle-icon4");
btnExternal.getStyleClass().add("toggle-icon4");
btnExternal.setGraphic(SVG.openInNew(Theme.blackFillBinding(), -1, -1));
updatePane.setRight(btnExternal);
@@ -337,7 +371,7 @@ public abstract class SettingsView extends StackPane {
{
JFXButton logButton = new JFXButton(i18n("settings.launcher.launcher_log.export"));
logButton.setOnMouseClicked(e -> onExportLogs());
logButton.getStyleClass().setAll("jfx-button-border");
logButton.getStyleClass().add("jfx-button-border");
logPane.setHeaderRight(logButton);
}
@@ -491,4 +525,5 @@ public abstract class SettingsView extends StackPane {
protected abstract void onUpdate();
protected abstract void onHelp();
protected abstract void onExportLogs();
protected abstract void onSponsor();
}
@@ -0,0 +1,96 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import java.util.List;
public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>> extends SkinBase<T> {
public ToolbarListPageSkin(T skinnable) {
super(skinnable);
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
BorderPane root = new BorderPane();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(initializeToolbar(skinnable));
root.setTop(toolbar);
}
{
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
VBox content = new VBox();
content.setSpacing(10);
content.setPadding(new Insets(10));
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
root.setCenter(scrollPane);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
}
public static Node wrap(Node node) {
StackPane stackPane = new StackPane();
stackPane.setPadding(new Insets(0, 5, 0, 2));
stackPane.getChildren().setAll(node);
return stackPane;
}
public static JFXButton createToolbarButton(String text, SVG.SVGIcon creator, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
return ret;
}
protected abstract List<Node> initializeToolbar(T skinnable);
}
@@ -0,0 +1,82 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class UpgradeDialog extends JFXDialogLayout {
private final WebView webView = new WebView();
public UpgradeDialog(Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
}
{
WebView webView = new WebView();
WebEngine engine = webView.getEngine();
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
String url = engine.getLoadWorker().getMessage().trim();
if (!url.startsWith(CHANGELOG_URL)) {
engine.getLoadWorker().cancel();
FXUtils.openLink(url);
}
});
setBody(webView);
}
{
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnMouseClicked(e -> updateRunnable.run());
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
setActions(updateButton, cancelButton);
}
}
}
@@ -18,11 +18,11 @@
package org.jackhuang.hmcl.ui;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class WebStage extends Stage {
private final WebView webView = new WebView();
@@ -30,7 +30,7 @@ public class WebStage extends Stage {
public WebStage() {
setScene(new Scene(webView, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
getIcons().add(new Image("/assets/img/icon.png"));
getIcons().add(newImage("/assets/img/icon.png"));
}
public WebView getWebView() {
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
@@ -29,6 +28,7 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountAdvancedListItem extends AdvancedListItem {
@@ -42,7 +42,7 @@ public class AccountAdvancedListItem extends AdvancedListItem {
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageProperty().unbind();
setImage(new Image("/assets/img/craft_table.png"));
setImage(newImage("/assets/img/craft_table.png"));
} else {
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
setSubtitle(accountSubtitle(account));
@@ -37,6 +37,7 @@ 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 {
@@ -84,6 +85,8 @@ public class AccountListItem extends RadioButton {
} catch (CredentialExpiredException e) {
try {
DialogController.logIn(account);
} catch (CancellationException e1) {
// ignore cancellation
} catch (Exception e1) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
}
@@ -17,12 +17,9 @@
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
@@ -31,16 +28,16 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
public class AccountListItemSkin extends SkinBase<AccountListItem> {
public AccountListItemSkin(AccountListItem skinnable) {
@@ -75,7 +72,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
if (skinnable.getAccount() instanceof AuthlibInjectorAccount) {
Tooltip tooltip = new Tooltip();
AuthlibInjectorServer server = ((AuthlibInjectorAccount) skinnable.getAccount()).getServer();
tooltip.textProperty().bind(Bindings.createStringBinding(server::toString, server));
tooltip.textProperty().bind(BindingMapping.of(server, AuthlibInjectorServer::toString));
FXUtils.installSlowTooltip(subtitle, tooltip);
}
VBox item = new VBox(title, subtitle);
@@ -91,7 +88,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
right.getChildren().add(btnRefresh);
JFXButton btnRemove = new JFXButton();
@@ -99,7 +96,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
right.getChildren().add(btnRemove);
root.setRight(right);
@@ -31,6 +31,9 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import java.util.function.Consumer;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class AccountLoginPane extends StackPane {
private final Account oldAccount;
@@ -59,11 +62,12 @@ public class AccountLoginPane extends StackPane {
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult(() -> oldAccount.logInWithPassword(password))
.finalizedResult(Schedulers.javafx(), authInfo -> {
.whenComplete(Schedulers.javafx(), authInfo -> {
success.accept(authInfo);
fireEvent(new DialogCloseEvent());
progressBar.setVisible(false);
}, e -> {
LOG.log(Level.INFO, "Failed to login with password: " + oldAccount, e);
if (e instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else {
@@ -17,9 +17,10 @@
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
@@ -47,12 +48,13 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
@@ -126,7 +128,7 @@ public class AddAccountPane extends StackPane {
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
// authlib-injector links
links.bind(MultiStepBinding.of(cboServers.getSelectionModel().selectedItemProperty())
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
.map(AddAccountPane::createHyperlinks)
.map(FXCollections::observableList));
Bindings.bindContent(linksContainer.getChildren(), links);
@@ -196,7 +198,7 @@ public class AddAccountPane extends StackPane {
Object additionalData = getAuthAdditionalData();
Task.ofResult(() -> factory.create(new Selector(), username, password, additionalData))
.finalizedResult(Schedulers.javafx(), account -> {
.whenComplete(Schedulers.javafx(), account -> {
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
@@ -293,7 +295,7 @@ public class AddAccountPane extends StackPane {
} catch (InterruptedException ignore) {
throw new NoSelectedCharacterException();
} finally {
JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
}
}
}
@@ -106,11 +106,11 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
Task.of(() -> {
serverBeingAdded = AuthlibInjectorServer.locateServer(url);
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
}).whenComplete(Schedulers.javafx(), (isDependentSucceeded, exception) -> {
addServerPane.setDisable(false);
nextPane.hideSpinner();
if (isDependentsSucceeded) {
if (isDependentSucceeded) {
lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl());
@@ -118,7 +118,6 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else {
Exception exception = variables.get("lastException");
LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception);
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
}
@@ -27,6 +27,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -35,21 +36,14 @@ import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
public class AuthlibInjectorServersPage extends ListPage<AuthlibInjectorServerItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.injector.manage.title"));
@FXML private ScrollPane scrollPane;
@FXML private VBox listPane;
@FXML private StackPane contentPane;
private ObservableList<AuthlibInjectorServerItem> serverItems;
private final ObservableList<AuthlibInjectorServerItem> serverItems;
public AuthlibInjectorServersPage() {
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
smoothScrolling(scrollPane);
serverItems = MappedObservableList.create(config().getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(listPane.getChildren(), serverItems);
Bindings.bindContent(itemsProperty(), serverItems);
}
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
@@ -57,8 +51,8 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
item -> config().getAuthlibInjectorServers().remove(item.getServer()));
}
@FXML
private void onAdd() {
@Override
public void add() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
@@ -83,7 +83,7 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
HBox right = new HBox();
right.setAlignment(Pos.CENTER);
right.setMouseTransparent(true);
right.getStyleClass().setAll("toggle-icon4");
right.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(right, 40);
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
newGraphic -> {
@@ -99,7 +99,7 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
visible -> root.setRight(visible ? right : null));
stackPane.setStyle("-fx-padding: 10 16 10 16;");
stackPane.getStyleClass().setAll("transparent");
stackPane.getStyleClass().add("transparent");
stackPane.setPickOnBounds(false);
stackPane.getChildren().setAll(root);
@@ -21,7 +21,7 @@ import static javafx.collections.FXCollections.emptyObservableList;
import static javafx.collections.FXCollections.observableList;
import static javafx.collections.FXCollections.singletonObservableList;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListCell;
@@ -49,7 +49,7 @@ public class FontComboBox extends JFXComboBox<String> {
}
});
itemsProperty().bind(MultiStepBinding.of(valueProperty())
itemsProperty().bind(BindingMapping.of(valueProperty())
.map(value -> value == null ? emptyObservableList() : singletonObservableList(value)));
setOnMouseClicked(e -> {
@@ -45,7 +45,7 @@ public class IconedItem extends RipplerContainer {
hBox.getChildren().add(icon);
}
hBox.getStyleClass().setAll("iconed-item-container");
hBox.getStyleClass().add("iconed-item-container");
Label textLabel = new Label();
textLabel.setId("label");
textLabel.setMouseTransparent(true);
@@ -0,0 +1,137 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeTableCell;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
private final StackPane pane;
private final CheckBox checkBox;
private boolean showLabel;
private ObservableValue<Boolean> booleanProperty;
public JFXCheckBoxTreeTableCell() {
this(null, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
this(getSelectedProperty, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty,
final StringConverter<T> converter) {
this.getStyleClass().add("check-box-tree-table-cell");
this.checkBox = new JFXCheckBox();
this.pane = new StackPane(checkBox);
this.pane.setAlignment(Pos.CENTER);
setGraphic(null);
setSelectedStateCallback(getSelectedProperty);
setConverter(converter);
}
private ObjectProperty<StringConverter<T>> converter =
new SimpleObjectProperty<StringConverter<T>>(this, "converter") {
protected void invalidated() {
updateShowLabel();
}
};
public final ObjectProperty<StringConverter<T>> converterProperty() {
return converter;
}
public final void setConverter(StringConverter<T> value) {
converterProperty().set(value);
}
public final StringConverter<T> getConverter() {
return converterProperty().get();
}
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<Integer, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
public final void setSelectedStateCallback(Callback<Integer, ObservableValue<Boolean>> value) {
selectedStateCallbackProperty().set(value);
}
public final Callback<Integer, ObservableValue<Boolean>> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
@SuppressWarnings("unchecked")
@Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
StringConverter<T> c = getConverter();
if (showLabel) {
setText(c.toString(item));
}
setGraphic(pane);
if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
ObservableValue<?> obsValue = getSelectedProperty();
if (obsValue instanceof BooleanProperty) {
booleanProperty = (ObservableValue<Boolean>) obsValue;
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
}
checkBox.disableProperty().bind(Bindings.not(
getTreeTableView().editableProperty().and(
getTableColumn().editableProperty()).and(
editableProperty())
));
}
}
private void updateShowLabel() {
this.showLabel = converter != null;
this.checkBox.setAlignment(showLabel ? Pos.CENTER_LEFT : Pos.CENTER);
}
private ObservableValue<?> getSelectedProperty() {
return getSelectedStateCallback() != null ?
getSelectedStateCallback().call(getIndex()) :
getTableColumn().getCellObservableValue(getIndex());
}
}
@@ -1,172 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextInputDialog;
import org.jackhuang.hmcl.ui.FXUtils;
import javax.swing.*;
import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageBox {
private MessageBox() {
}
private static final String TITLE = i18n("message.info");
/**
* User Operation: Yes
*/
public static final int YES_OPTION = 0;
/**
* User Operation: No
*/
public static final int NO_OPTION = 1;
/**
* User Operation: Cancel
*/
public static final int CANCEL_OPTION = 2;
/**
* User Operation: OK
*/
public static final int OK_OPTION = 0;
/**
* User Operation: Closed Message Box
*/
public static final int CLOSED_OPTION = -1;
/**
* Buttons: Yes No
*/
public static final int YES_NO_OPTION = 10;
/**
* Buttons: Yes No Cancel
*/
public static final int YES_NO_CANCEL_OPTION = 11;
/**
* Buttons: OK Cancel
*/
public static final int OK_CANCEL_OPTION = 12;
/**
* Message Box Type: Error
*/
public static final int ERROR_MESSAGE = 0;
/**
* Message Box Type: Info
*/
public static final int INFORMATION_MESSAGE = 1;
/**
* Message Box Type: Warning
*/
public static final int WARNING_MESSAGE = 2;
/**
* Message Box Type: Question
*/
public static final int QUESTION_MESSAGE = 3;
/**
* Message Box Type: Fine
*/
public static final int FINE_MESSAGE = 4;
/**
* Message Box Type: Plain
*/
public static final int PLAIN_MESSAGE = -1;
public static void show(String message) {
show(message, TITLE);
}
/**
* Show MsgBox with title and options
*
* @param message The Message
* @param title The title of MsgBox.
* @return user operation.
*/
public static Optional<ButtonType> show(String message, String title) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
return alert.showAndWait();
}
public static int confirm(String message, String title) {
return confirm(message, title, -1);
}
public static int confirm(String message, int option) {
return confirm(message, TITLE, option);
}
public static int confirm(String message, String title, int option) {
return FXUtils.runInUIThread(() -> {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
switch (option) {
case YES_NO_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
break;
case YES_NO_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
break;
case OK_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
break;
default:
throw new IllegalArgumentException("Unrecognized message box option " + option);
}
Optional<ButtonType> buttonType = alert.showAndWait();
if (!buttonType.isPresent()) return CLOSED_OPTION;
else if (buttonType.get() == ButtonType.OK) return OK_OPTION;
else if (buttonType.get() == ButtonType.YES) return YES_OPTION;
else if (buttonType.get() == ButtonType.NO) return NO_OPTION;
else if (buttonType.get() == ButtonType.CANCEL) return CANCEL_OPTION;
else throw new IllegalStateException("Unexpected button type:" + buttonType.get());
});
}
public static Optional<String> input(String message) {
return input(message, UIManager.getString("OptionPane.inputDialogTitle"));
}
public static Optional<String> input(String message, String title) {
return input(message, title, "");
}
public static Optional<String> input(String message, String title, String initialValue) {
TextInputDialog dialog = new TextInputDialog(initialValue);
dialog.setTitle(title);
dialog.setHeaderText(message);
dialog.setContentText(message);
return dialog.showAndWait();
}
}
@@ -32,6 +32,15 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends StackPane {
public static enum MessageType {
ERROR,
INFORMATION,
WARNING,
QUESTION,
FINE,
PLAIN,
}
@FXML
private JFXButton acceptButton;
@FXML
@@ -45,7 +54,7 @@ public final class MessageDialogPane extends StackPane {
@FXML
private HBox actions;
public MessageDialogPane(String text, String title, int type, Runnable onAccept) {
public MessageDialogPane(String text, String title, MessageType type, Runnable onAccept) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
if (title != null)
@@ -60,19 +69,19 @@ public final class MessageDialogPane extends StackPane {
actions.getChildren().remove(cancelButton);
switch (type) {
case MessageBox.INFORMATION_MESSAGE:
case INFORMATION:
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.ERROR_MESSAGE:
case ERROR:
graphic.setGraphic(SVG.closeCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.FINE_MESSAGE:
case FINE:
graphic.setGraphic(SVG.checkCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.WARNING_MESSAGE:
case WARNING:
graphic.setGraphic(SVG.alert(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.QUESTION_MESSAGE:
case QUESTION:
graphic.setGraphic(SVG.helpCircle(Theme.blackFillBinding(), 40, 40));
break;
default:
@@ -81,7 +90,7 @@ public final class MessageDialogPane extends StackPane {
}
public MessageDialogPane(String text, String title, Runnable onAccept, Runnable onCancel) {
this(text, title, MessageBox.QUESTION_MESSAGE, onAccept);
this(text, title, MessageType.QUESTION, onAccept);
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
@@ -64,7 +64,7 @@ public class PopupMenu extends Control {
public static Node wrapPopupMenuItem(Node node) {
StackPane pane = new StackPane();
pane.getChildren().setAll(node);
pane.getStyleClass().setAll("menu-container");
pane.getStyleClass().add("menu-container");
node.setMouseTransparent(true);
return new RipplerContainer(pane);
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXRippler;
import javafx.animation.Transition;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.NamedArg;
@@ -64,39 +63,16 @@ public class RipplerContainer extends StackPane {
mask.resize(buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset());
return mask;
}
@Override
protected void initListeners() {
this.ripplerPane.setOnMousePressed(event -> {
if (releaseManualRippler != null)
releaseManualRippler.run();
releaseManualRippler = null;
createRipple(event.getX(), event.getY());
});
}
};
private Transition clickedAnimation;
private final CornerRadii defaultRadii = new CornerRadii(3);
private Runnable releaseManualRippler;
public RipplerContainer(@NamedArg("container") Node container) {
setContainer(container);
getStyleClass().add(DEFAULT_STYLE_CLASS);
buttonRippler.setPosition(JFXRippler.RipplerPos.BACK);
buttonContainer.getChildren().add(buttonRippler);
setOnMousePressed(event -> {
if (clickedAnimation != null) {
clickedAnimation.setRate(1);
clickedAnimation.play();
}
});
setOnMouseReleased(event -> {
if (clickedAnimation != null) {
clickedAnimation.setRate(-1);
clickedAnimation.play();
}
});
focusedProperty().addListener((a, b, newValue) -> {
if (newValue) {
if (!isPressed())
@@ -109,23 +85,7 @@ public class RipplerContainer extends StackPane {
setPickOnBounds(false);
buttonContainer.setPickOnBounds(false);
buttonContainer.shapeProperty().bind(shapeProperty());
buttonContainer.borderProperty().bind(borderProperty());
buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
if (getBackground() == null || isJavaDefaultBackground(getBackground()) || isJavaDefaultClickedBackground(getBackground()))
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
try {
return new Background(new BackgroundFill(getBackground() != null ? getBackground().getFills().get(0).getFill() : Color.TRANSPARENT,
getBackground() != null ? getBackground().getFills().get(0).getRadii() : defaultRadii, Insets.EMPTY));
} catch (Exception e) {
return getBackground();
}
}, backgroundProperty()));
ripplerFillProperty().addListener((a, b, newValue) -> buttonRippler.setRipplerFill(newValue));
if (getBackground() == null || isJavaDefaultBackground(getBackground()))
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
buttonRippler.ripplerFillProperty().bind(ripplerFillProperty());
containerProperty().addListener(o -> updateChildren());
updateChildren();
@@ -153,24 +113,6 @@ public class RipplerContainer extends StackPane {
getChildren().get(i).setPickOnBounds(false);
}
private boolean isJavaDefaultBackground(Background background) {
try {
String firstFill = background.getFills().get(0).getFill().toString();
return "0xffffffba".equals(firstFill) || "0xffffffbf".equals(firstFill) || "0xffffffbd".equals(firstFill);
} catch (Exception e) {
return false;
}
}
private boolean isJavaDefaultClickedBackground(Background background) {
try {
String firstFill = background.getFills().get(0).getFill().toString();
return "0x039ed3ff".equals(firstFill);
} catch (Exception e) {
return false;
}
}
public Node getContainer() {
return container.get();
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
@@ -34,6 +33,8 @@ import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class TaskExecutorDialogPane extends StackPane {
private TaskExecutor executor;
private Consumer<Region> onCancel;
@@ -121,6 +122,6 @@ public class TaskExecutorDialogPane extends StackPane {
public void setCancel(Consumer<Region> onCancel) {
this.onCancel = onCancel;
JFXUtilities.runInFX(() -> btnCancel.setDisable(onCancel == null));
runInFX(() -> btnCancel.setDisable(onCancel == null));
}
}
@@ -17,15 +17,11 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
@@ -45,16 +41,15 @@ import java.util.Map;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class TaskListPane extends StackPane {
private final JFXListView<Task> listBox = new JFXListView<>();
private final AdvancedListBox listBox = new AdvancedListBox();
private final Map<Task, ProgressListNode> nodes = new HashMap<>();
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
public TaskListPane() {
getChildren().setAll(listBox);
listBox.setSpacing(0);
listBox.setPadding(Insets.EMPTY);
listBox.setCellFactory(listView -> new ProgressListNode());
getChildren().setAll(listBox);
}
public ReadOnlyIntegerProperty finishedTasksProperty() {
@@ -70,7 +65,7 @@ public final class TaskListPane extends StackPane {
@Override
public void onStart() {
Platform.runLater(() -> {
listBox.getItems().clear();
listBox.clear();
finishedTasks.set(0);
totTasks.set(0);
});
@@ -112,66 +107,64 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.scan"));
}
Platform.runLater(() -> listBox.getItems().add(task));
ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
Platform.runLater(() -> listBox.add(node));
}
@Override
public void onFinished(Task task) {
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
node.unbind();
Platform.runLater(() -> {
if (listBox.getItems().remove(task))
finishedTasks.set(finishedTasks.getValue() + 1);
listBox.remove(node);
finishedTasks.set(finishedTasks.getValue() + 1);
});
}
@Override
public void onFailed(Task task, Throwable throwable) {
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
Platform.runLater(() -> {
node.setThrowable(throwable);
finishedTasks.set(finishedTasks.getValue() + 1);
});
}
});
}
private static class ProgressListNode extends ListCell<Task> {
private final BorderPane borderPane = new BorderPane();
private static class ProgressListNode extends BorderPane {
private final JFXProgressBar bar = new JFXProgressBar();
private final Label title = new Label();
private final Label state = new Label();
{
borderPane.setLeft(title);
borderPane.setRight(state);
borderPane.setBottom(bar);
borderPane.setMinWidth(0);
borderPane.setPrefWidth(1);
public ProgressListNode(Task task) {
bar.progressProperty().bind(task.progressProperty());
title.setText(task.getName());
state.textProperty().bind(task.messageProperty());
setPadding(Insets.EMPTY);
setLeft(title);
setRight(state);
setBottom(bar);
bar.minWidthProperty().bind(widthProperty());
bar.prefWidthProperty().bind(widthProperty());
bar.maxWidthProperty().bind(widthProperty());
}
@Override
protected void updateItem(Task item, boolean empty) {
boolean wasEmpty = isEmpty();
Task oldTask = getItem();
public void unbind() {
bar.progressProperty().unbind();
state.textProperty().unbind();
}
if (!wasEmpty && oldTask != null) {
bar.progressProperty().unbind();
state.textProperty().unbind();
}
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
setGraphic(borderPane);
bar.visibleProperty().bind(Bindings.createBooleanBinding(() -> item.progressProperty().get() != -1, item.progressProperty()));
bar.progressProperty().bind(item.progressProperty());
state.textProperty().bind(Bindings.createObjectBinding(() -> {
if (item.getState() == Task.TaskState.FAILED) {
return item.getLastException().getLocalizedMessage();
} else {
return item.messageProperty().get();
}
}, item.messageProperty(), item.stateProperty()));
title.setText(item.getName());
}
public void setThrowable(Throwable throwable) {
unbind();
state.setText(throwable.getLocalizedMessage());
bar.setProgress(0);
}
}
}
@@ -45,7 +45,10 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.StackContainerPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
@@ -61,6 +64,7 @@ import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class DecoratorController {
@@ -92,7 +96,7 @@ public class DecoratorController {
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
welcomeView = new ImageView();
welcomeView.setImage(new Image("/assets/img/welcome.png"));
welcomeView.setImage(newImage("/assets/img/welcome.png"));
welcomeView.setCursor(Cursor.HAND);
FXUtils.limitSize(welcomeView, 796, 517);
welcomeView.setOnMouseClicked(e -> {
@@ -152,7 +156,7 @@ public class DecoratorController {
config().backgroundImageProperty()));
}
private Image defaultBackground = new Image("/assets/img/background.jpg");
private Image defaultBackground = newImage("/assets/img/background.jpg");
/**
* Load background image from bg/, background.png, background.jpg
@@ -73,7 +73,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
Decorator skinnable = getSkinnable();
BorderPane root = new BorderPane();
root.getStyleClass().setAll("jfx-decorator", "resize-border");
root.getStyleClass().addAll("jfx-decorator", "resize-border");
root.setPrefHeight(519);
root.setPrefWidth(800);
root.setMaxHeight(Region.USE_PREF_SIZE);
@@ -83,7 +83,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
StackPane drawerWrapper = new StackPane();
skinnable.setDrawerWrapper(drawerWrapper);
drawerWrapper.getStyleClass().setAll("jfx-decorator-drawer");
drawerWrapper.getStyleClass().add("jfx-decorator-drawer");
drawerWrapper.backgroundProperty().bind(skinnable.backgroundProperty());
FXUtils.setOverflowHidden(drawerWrapper, true);
{
@@ -92,10 +92,10 @@ public class DecoratorSkin extends SkinBase<Decorator> {
{
BorderPane leftRootPane = new BorderPane();
FXUtils.setLimitWidth(leftRootPane, 200);
leftRootPane.getStyleClass().setAll("jfx-decorator-content-container");
leftRootPane.getStyleClass().add("jfx-decorator-content-container");
StackPane drawerContainer = new StackPane();
drawerContainer.getStyleClass().setAll("gray-background");
drawerContainer.getStyleClass().add("gray-background");
Bindings.bindContent(drawerContainer.getChildren(), skinnable.drawerProperty());
leftRootPane.setCenter(drawerContainer);
@@ -111,7 +111,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
{
contentPlaceHolder = new StackPane();
contentPlaceHolder.getStyleClass().setAll("jfx-decorator-content-container");
contentPlaceHolder.getStyleClass().add("jfx-decorator-content-container");
contentPlaceHolder.backgroundProperty().bind(skinnable.contentBackgroundProperty());
FXUtils.setOverflowHidden(contentPlaceHolder, true);
Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty());
@@ -189,14 +189,14 @@ public class DecoratorSkin extends SkinBase<Decorator> {
{
JFXButton backNavButton = new JFXButton();
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
backNavButton.getStyleClass().setAll("jfx-decorator-button");
backNavButton.getStyleClass().add("jfx-decorator-button");
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
backNavButton.visibleProperty().bind(skinnable.canBackProperty());
closeNavButton = new JFXButton();
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.getStyleClass().setAll("jfx-decorator-button");
closeNavButton.getStyleClass().add("jfx-decorator-button");
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
@@ -219,7 +219,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
VBox navCenter = new VBox();
navCenter.setAlignment(Pos.CENTER_LEFT);
Label titleLabel = new Label();
titleLabel.getStyleClass().setAll("jfx-decorator-title");
titleLabel.getStyleClass().add("jfx-decorator-title");
titleLabel.textProperty().bind(skinnable.drawerTitleProperty());
navCenter.getChildren().setAll(titleLabel);
navBar.setCenter(navCenter);
@@ -228,7 +228,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
navRight.setAlignment(Pos.CENTER_RIGHT);
refreshNavButton = new JFXButton();
refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), -1, -1));
refreshNavButton.getStyleClass().setAll("jfx-decorator-button");
refreshNavButton.getStyleClass().add("jfx-decorator-button");
refreshNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
refreshNavButton.visibleProperty().bind(skinnable.canRefreshProperty());
@@ -251,12 +251,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
StackPane pane = new StackPane(minus);
pane.setAlignment(Pos.CENTER);
btnMin.setGraphic(pane);
btnMin.getStyleClass().setAll("jfx-decorator-button");
btnMin.getStyleClass().add("jfx-decorator-button");
btnMin.setOnAction(e -> skinnable.minimize());
JFXButton btnClose = new JFXButton();
btnClose.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
btnClose.getStyleClass().setAll("jfx-decorator-button");
btnClose.getStyleClass().add("jfx-decorator-button");
btnClose.setOnAction(e -> skinnable.close());
buttonsContainer.getChildren().setAll(separator, btnMin, btnClose);
@@ -52,7 +52,7 @@ public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorD
wizardController.setProvider(provider);
wizardController.onStart();
getStyleClass().setAll("white-background");
getStyleClass().add("white-background");
}
@Override
@@ -21,15 +21,27 @@ import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import java.net.SocketTimeoutException;
import java.util.Map;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class InstallerWizardProvider implements WizardProvider {
@@ -46,9 +58,9 @@ public final class InstallerWizardProvider implements WizardProvider {
this.version = version;
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
forge = analyzer.getForge().map(Library::getVersion).orElse(null);
liteLoader = analyzer.getLiteLoader().map(Library::getVersion).orElse(null);
optiFine = analyzer.getOptiFine().map(Library::getVersion).orElse(null);
forge = analyzer.get(FORGE).map(Library::getVersion).orElse(null);
liteLoader = analyzer.get(LITELOADER).map(Library::getVersion).orElse(null);
optiFine = analyzer.get(OPTIFINE).map(Library::getVersion).orElse(null);
}
public Profile getProfile() {
@@ -82,18 +94,18 @@ public final class InstallerWizardProvider implements WizardProvider {
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> alertFailureMessage(exception, next));
Task ret = Task.ofResult("version", () -> version);
TaskResult<Version> ret = Task.ofResult(() -> version);
if (settings.containsKey("forge"))
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("forge")));
ret = ret.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("forge")));
if (settings.containsKey("liteloader"))
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("liteloader")));
ret = ret.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("liteloader")));
if (settings.containsKey("optifine"))
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("optifine")));
ret = ret.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("optifine")));
return ret.then(profile.getRepository().refreshVersionsAsync());
}
@@ -114,4 +126,31 @@ public final class InstallerWizardProvider implements WizardProvider {
return true;
}
public static void alertFailureMessage(Exception exception, Runnable next) {
if (exception instanceof LibraryDownloadException) {
Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
} else if (exception instanceof DownloadException) {
if (exception.getCause() instanceof SocketTimeoutException) {
Controllers.dialog(i18n("install.failed.downloading.timeout", ((DownloadException) exception).getUrl()), i18n("install.failed.downloading"), 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() + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
}
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
}
} else if (exception instanceof OptiFineInstallTask.UnsupportedOptiFineInstallationException) {
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageType.ERROR, next);
} else if (exception instanceof UnsupportedOperationException) {
Controllers.dialog(i18n("install.failed.install_online"), i18n("install.failed"), MessageType.ERROR, next);
} else if (exception instanceof VersionMismatchException) {
VersionMismatchException e = ((VersionMismatchException) exception);
Controllers.dialog(i18n("install.failed.version_mismatch", e.getExpect(), e.getActual()), i18n("install.failed"), MessageType.ERROR, next);
} else {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageType.ERROR, next);
}
}
}
@@ -24,14 +24,12 @@ import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.FileNotFoundException;
@@ -86,11 +84,11 @@ public class ModpackInstallWizardProvider implements WizardProvider {
try {
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"), MessageBox.ERROR_MESSAGE);
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
} catch (MismatchedModpackTypeException e) {
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageType.ERROR);
} catch (IOException e) {
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageType.ERROR);
}
return null;
} else {
@@ -107,14 +105,12 @@ public class ModpackInstallWizardProvider implements WizardProvider {
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
if (exception instanceof CurseCompletionException) {
if (exception.getCause() instanceof FileNotFoundException) {
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageBox.INFORMATION_MESSAGE, next);
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageType.INFORMATION, next);
}
} else if (exception instanceof DownloadException) {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
} else {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
InstallerWizardProvider.alertFailureMessage(exception, next);
}
}
});
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebStage;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -110,8 +110,8 @@ public final class ModpackPage extends StackPane implements WizardPage {
spinnerPane.showSpinner();
Task.ofResult(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
.thenResult(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
.finalizedResult(Schedulers.javafx(), manifest -> {
.thenApply(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
.whenComplete(Schedulers.javafx(), manifest -> {
spinnerPane.hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
@@ -129,7 +129,7 @@ public final class ModpackPage extends StackPane implements WizardPage {
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
}
}, e -> {
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
Platform.runLater(controller::onEnd);
}).start();
}
@@ -0,0 +1,95 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import java.util.LinkedList;
import java.util.Map;
import static org.jackhuang.hmcl.ui.download.InstallerWizardProvider.alertFailureMessage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class UpdateInstallerWizardProvider implements WizardProvider {
private final Profile profile;
private final String gameVersion;
private final Version version;
private final String libraryId;
private final Library oldLibrary;
public UpdateInstallerWizardProvider(Profile profile, String gameVersion, Version version, String libraryId, Library oldLibrary) {
this.profile = profile;
this.gameVersion = gameVersion;
this.version = version;
this.libraryId = libraryId;
this.oldLibrary = oldLibrary;
}
@Override
public void start(Map<String, Object> settings) {
}
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", i18n("install.success"));
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> alertFailureMessage(exception, next));
// We remove library but not save it,
// so if installation failed will not break down current version.
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(oldLibrary);
return new MaintainTask(version.setLibraries(newList))
.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get(libraryId)))
.then(profile.getRepository().refreshVersionsAsync());
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
DownloadProvider provider = profile.getDependency().getDownloadProvider();
switch (step) {
case 0:
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, provider, libraryId, () -> {
Controllers.confirmDialog(i18n("install.change_version.confirm", i18n("install.installer." + libraryId), oldLibrary.getVersion(), ((RemoteVersion) settings.get(libraryId)).getSelfVersion()),
i18n("install.change_version"), controller::onFinish, controller::onCancel);
});
default:
throw new IllegalStateException();
}
}
@Override
public boolean cancel() {
return true;
}
@Override
public boolean cancelIfCannotGoBack() {
// VersionsPage will call wizardController.onPrev(cleanUp = true) when list is empty.
// So we cancel this wizard when VersionPage calls the method.
return true;
}
}
@@ -22,14 +22,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Map;
@@ -63,23 +59,14 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
if (settings.containsKey("optifine"))
builder.version((RemoteVersion) settings.get("optifine"));
return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions())
return builder.buildAsync().whenComplete((a, b) -> profile.getRepository().refreshVersions())
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
}
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", i18n("install.success"));
settings.put("failure_callback", new FailureCallback() {
@Override
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
if (exception instanceof DownloadException) {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
} else {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
}
}
});
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> InstallerWizardProvider.alertFailureMessage(exception, next));
return finishVersionDownloadingAsync(settings);
}
@@ -25,6 +25,7 @@ import com.jfoenix.controls.JFXSpinner;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.fxml.FXML;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -43,7 +44,7 @@ import java.util.Map;
import java.util.logging.Level;
import java.util.stream.Collectors;
public final class VersionsPage extends StackPane implements WizardPage, Refreshable {
public final class VersionsPage extends BorderPane implements WizardPage, Refreshable {
private final String gameVersion;
private final DownloadProvider downloadProvider;
private final String libraryId;
@@ -59,6 +60,8 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
@FXML
private StackPane emptyPane;
@FXML
private StackPane root;
@FXML
private JFXCheckBox chkRelease;
@FXML
private JFXCheckBox chkSnapshot;
@@ -69,7 +72,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
@FXML
private VBox centrePane;
private final TransitionHandler transitionHandler = new TransitionHandler(this);
private final TransitionHandler transitionHandler;
private final VersionList<?> versionList;
private TaskExecutor executor;
@@ -83,6 +86,8 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
transitionHandler = new TransitionHandler(root);
if (versionList.hasType()) {
centrePane.getChildren().setAll(checkPane, list);
} else
@@ -122,9 +127,9 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
@Override
public void refresh() {
getChildren().setAll(spinner);
executor = versionList.refreshAsync(downloadProvider).finalized((variables, isDependentsSucceeded) -> {
if (isDependentsSucceeded) {
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
executor = versionList.refreshAsync(gameVersion, downloadProvider).whenComplete((isDependentSucceeded, exception) -> {
if (isDependentSucceeded) {
List<VersionsPageItem> items = loadVersions();
Platform.runLater(() -> {
@@ -142,7 +147,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
}
});
} else {
LOG.log(Level.WARNING, "Failed to fetch versions list", (Throwable) variables.get("lastException"));
LOG.log(Level.WARNING, "Failed to fetch versions list", exception);
Platform.runLater(() -> {
transitionHandler.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
});
@@ -169,4 +174,9 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
@FXML
private void onBack() { controller.onPrev(true); }
@FXML
private void onSponsor() {
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/bmclapi_sponsor");
}
}
@@ -73,9 +73,9 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
ModAdviser.ModSuggestion state = ModAdviser.ModSuggestion.SUGGESTED;
if (basePath.length() > "minecraft/".length()) {
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (file.isDirectory() ? "/" : ""), file.isDirectory());
if (file.isFile() && Objects.equals(FileUtils.getNameWithoutExtension(file), version))
if (file.isFile() && Objects.equals(FileUtils.getNameWithoutExtension(file), version)) // Ignore <version>.json, <version>.jar
state = ModAdviser.ModSuggestion.HIDDEN;
if (file.isDirectory() && Objects.equals(file.getName(), version + "-natives"))
if (file.isDirectory() && Objects.equals(file.getName(), version + "-natives")) // Ignore <version>-natives
state = ModAdviser.ModSuggestion.HIDDEN;
if (state == ModAdviser.ModSuggestion.HIDDEN)
return null;
@@ -19,13 +19,14 @@ package org.jackhuang.hmcl.ui.profile;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class ProfileAdvancedListItem extends AdvancedListItem {
private ObjectProperty<Profile> profile = new SimpleObjectProperty<Profile>() {
@@ -41,7 +42,7 @@ public class ProfileAdvancedListItem extends AdvancedListItem {
};
public ProfileAdvancedListItem() {
setImage(new Image("/assets/img/craft_table.png"));
setImage(newImage("/assets/img/craft_table.png"));
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
}
@@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
@@ -31,6 +30,8 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
public ProfileListItemSkin(ProfileListItem skinnable) {
@@ -54,7 +55,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().set(new Image("/assets/img/craft_table.png"));
imageView.imageProperty().set(newImage("/assets/img/craft_table.png"));
TwoLineListItem item = new TwoLineListItem();
BorderPane.setAlignment(item, Pos.CENTER);
@@ -1,60 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListItem extends BorderPane {
public DatapackListItem(Datapack.Pack info, Consumer<DatapackListItem> deleteCallback) {
JFXCheckBox chkEnabled = new JFXCheckBox();
BorderPane.setAlignment(chkEnabled, Pos.CENTER);
setLeft(chkEnabled);
TwoLineListItem modItem = new TwoLineListItem();
BorderPane.setAlignment(modItem, Pos.CENTER);
setCenter(modItem);
JFXButton btnRemove = new JFXButton();
FXUtils.installFastTooltip(btnRemove, i18n("datapack.remove"));
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
setRight(btnRemove);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
modItem.setTitle(info.getId());
modItem.setSubtitle(info.getDescription());
chkEnabled.selectedProperty().bindBidirectional(info.activeProperty());
}
}
@@ -19,11 +19,16 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.Datapack;
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.ListPage;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -32,16 +37,19 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListPage extends ListPage<DatapackListItem> implements DecoratorPage {
public class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty();
private final Path worldDir;
private final Datapack datapack;
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
public DatapackListPage(String worldName, Path worldDir) {
this.worldDir = worldDir;
@@ -50,14 +58,7 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
datapack = new Datapack(worldDir.resolve("datapacks"));
datapack.loadFromDir();
setItems(MappedObservableList.create(datapack.getInfo(),
pack -> new DatapackListItem(pack, item -> {
try {
datapack.deletePack(pack);
} catch (IOException e) {
Logging.LOG.warning("Failed to delete datapack");
}
})));
setItems(items = MappedObservableList.create(datapack.getInfo(), DatapackListPageSkin.DatapackInfoObject::new));
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)), mods -> {
mods.forEach(it -> {
@@ -69,7 +70,19 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e);
}
});
}, datapack::loadFromDir);
}, this::refresh);
}
@Override
protected Skin<?> createDefaultSkin() {
return new DatapackListPageSkin(this);
}
public void refresh() {
setLoading(true);
Task.of(datapack::loadFromDir)
.with(Task.of(Schedulers.javafx(), () -> setLoading(false)))
.start();
}
@Override
@@ -77,7 +90,6 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
return title;
}
@Override
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.choose_datapack"));
@@ -97,4 +109,32 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
datapack.loadFromDir();
}
void removeSelected(ObservableList<TreeItem<DatapackListPageSkin.DatapackInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(pack -> {
try {
datapack.deletePack(pack);
} catch (IOException e) {
// Fail to remove mods if the game is running or the datapack is absent.
Logging.LOG.warning("Failed to delete datapack " + pack);
}
});
}
void enableSelected(ObservableList<TreeItem<DatapackListPageSkin.DatapackInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(info -> info.setActive(true));
}
void disableSelected(ObservableList<TreeItem<DatapackListPageSkin.DatapackInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(info -> info.setActive(false));
}
}
@@ -0,0 +1,126 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXTreeTableColumn;
import com.jfoenix.controls.JFXTreeTableView;
import com.jfoenix.controls.RecursiveTreeItem;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.ui.FXUtils.setupCellValueFactory;
import static org.jackhuang.hmcl.ui.FXUtils.wrapMargin;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class DatapackListPageSkin extends SkinBase<DatapackListPage> {
DatapackListPageSkin(DatapackListPage skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
JFXTreeTableView<DatapackInfoObject> tableView = new JFXTreeTableView<>();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh));
toolbar.getChildren().add(createToolbarButton(i18n("datapack.add"), SVG::plus, skinnable::add));
toolbar.getChildren().add(createToolbarButton(i18n("mods.remove"), SVG::delete, () ->
skinnable.removeSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG::check, () ->
skinnable.enableSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG::close, () ->
skinnable.disableSelected(tableView.getSelectionModel().getSelectedItems())));
root.setTop(toolbar);
}
{
SpinnerPane center = new SpinnerPane();
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
tableView.getStyleClass().addAll("no-header");
tableView.setShowRoot(false);
tableView.setEditable(true);
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableView.setRoot(new RecursiveTreeItem<>(skinnable.getItems(), RecursiveTreeObject::getChildren));
JFXTreeTableColumn<DatapackInfoObject, Boolean> activeColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(activeColumn, DatapackInfoObject::activeProperty);
activeColumn.setCellFactory(c -> new JFXCheckBoxTreeTableCell<>());
activeColumn.setEditable(true);
activeColumn.setMaxWidth(40);
activeColumn.setMinWidth(40);
JFXTreeTableColumn<DatapackInfoObject, Node> detailColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(detailColumn, DatapackInfoObject::nodeProperty);
tableView.getColumns().setAll(activeColumn, detailColumn);
tableView.setColumnResizePolicy(JFXTreeTableView.CONSTRAINED_RESIZE_POLICY);
center.setContent(tableView);
root.setCenter(center);
}
getChildren().setAll(root);
}
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
private final BooleanProperty active;
private final Datapack.Pack packInfo;
private final ObjectProperty<Node> node;
DatapackInfoObject(Datapack.Pack packInfo) {
this.packInfo = packInfo;
this.active = packInfo.activeProperty();
this.node = new SimpleObjectProperty<>(wrapMargin(new TwoLineListItem(packInfo.getId(), StringUtils.parseColorEscapes(packInfo.getDescription())),
new Insets(8, 0, 8, 0)));
}
BooleanProperty activeProperty() {
return active;
}
ObjectProperty<Node> nodeProperty() {
return node;
}
Datapack.Pack getPackInfo() {
return packInfo;
}
}
}
@@ -17,13 +17,13 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.image.Image;
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.construct.AdvancedListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
@@ -38,7 +38,7 @@ public class GameAdvancedListItem extends AdvancedListItem {
} else {
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
setImage(new Image("/assets/img/grass.png"));
setImage(newImage("/assets/img/grass.png"));
}
});
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
@@ -25,11 +26,21 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.setting.Profile;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
import static org.jackhuang.hmcl.util.Lang.threadPool;
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;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class GameItem extends Control {
private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 1, TimeUnit.SECONDS);
private final Profile profile;
private final String version;
private final StringProperty title = new SimpleStringProperty();
@@ -40,16 +51,19 @@ public class GameItem extends Control {
this.profile = profile;
this.version = id;
String game = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse("Unknown");
StringBuilder libraries = new StringBuilder(game);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getVersion(id));
analyzer.getForge().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))));
analyzer.getLiteLoader().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))));
analyzer.getOptiFine().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))));
// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
CompletableFuture.supplyAsync(() -> GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse("Unknown"), POOL_VERSION_RESOLVE)
.thenAcceptAsync(game -> {
StringBuilder libraries = new StringBuilder(game);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getVersion(id));
analyzer.get(FORGE).ifPresent(library -> libraries.append(", ").append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))));
analyzer.get(LITELOADER).ifPresent(library -> libraries.append(", ").append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))));
analyzer.get(OPTIFINE).ifPresent(library -> libraries.append(", ").append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))));
subtitle.set(libraries.toString());
}, Platform::runLater)
.exceptionally(handleUncaught);
title.set(id);
subtitle.set(libraries.toString());
image.set(profile.getRepository().getVersionIconImage(version));
}
@@ -17,12 +17,10 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.Node;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
@@ -30,32 +28,31 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameList extends Control implements DecoratorPage {
public class GameList extends ListPageBase<GameListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(I18n.i18n("version.manage"));
private final BooleanProperty loading = new SimpleBooleanProperty(true);
private final ListProperty<GameListItem> items = new SimpleListProperty<>(FXCollections.observableArrayList());
private ToggleGroup toggleGroup;
public GameList() {
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == Profiles.getSelectedProfile().getRepository())
JFXUtilities.runInFX(() -> loading.set(true));
runInFX(() -> setLoading(true));
});
Profiles.registerVersionsListener(this::loadVersions);
@@ -72,10 +69,10 @@ public class GameList extends Control implements DecoratorPage {
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(version -> new GameListItem(toggleGroup, profile, version.getId()))
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
loading.set(false);
items.setAll(children);
setLoading(false);
itemsProperty().setAll(children);
children.forEach(GameListItem::checkSelection);
profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
@@ -97,8 +94,8 @@ public class GameList extends Control implements DecoratorPage {
}
@Override
protected Skin<?> createDefaultSkin() {
return new GameListSkin(this);
protected GameListSkin createDefaultSkin() {
return new GameListSkin();
}
void addNewGame() {
@@ -128,11 +125,20 @@ public class GameList extends Control implements DecoratorPage {
return title.getReadOnlyProperty();
}
public BooleanProperty loadingProperty() {
return loading;
}
private class GameListSkin extends ToolbarListPageSkin<GameList> {
public ListProperty<GameListItem> itemsProperty() {
return items;
public GameListSkin() {
super(GameList.this);
}
@Override
protected List<Node> initializeToolbar(GameList skinnable) {
return Arrays.asList(
createToolbarButton(i18n("install.new_game"), SVG::plus, skinnable::addNewGame),
createToolbarButton(i18n("install.modpack"), SVG::importIcon, skinnable::importModpack),
createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
createToolbarButton(i18n("settings.type.global.manage"), SVG::gear, skinnable::modifyGlobalGameSettings)
);
}
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXRadioButton;
@@ -33,6 +32,7 @@ import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameListItemSkin extends SkinBase<GameListItem> {
@@ -73,7 +73,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
btnUpgrade.setOnMouseClicked(e -> skinnable.update());
btnUpgrade.getStyleClass().add("toggle-icon4");
btnUpgrade.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")));
runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")));
right.getChildren().add(btnUpgrade);
}
@@ -1,120 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.controls.JFXSpinner;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.i18n.I18n;
public class GameListSkin extends SkinBase<GameList> {
private static Node wrap(Node node) {
StackPane stackPane = new StackPane();
stackPane.setPadding(new Insets(0, 5, 0, 2));
stackPane.getChildren().setAll(node);
return stackPane;
}
public GameListSkin(GameList skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().setAll("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
JFXButton btnAddNewGame = new JFXButton();
btnAddNewGame.getStyleClass().add("jfx-tool-bar-button");
btnAddNewGame.textFillProperty().bind(Theme.foregroundFillBinding());
btnAddNewGame.setGraphic(wrap(SVG.plus(Theme.foregroundFillBinding(), -1, -1)));
btnAddNewGame.setText(I18n.i18n("install.new_game"));
btnAddNewGame.setOnMouseClicked(e -> skinnable.addNewGame());
toolbar.getChildren().add(btnAddNewGame);
JFXButton btnImportModpack = new JFXButton();
btnImportModpack.getStyleClass().add("jfx-tool-bar-button");
btnImportModpack.textFillProperty().bind(Theme.foregroundFillBinding());
btnImportModpack.setGraphic(wrap(SVG.importIcon(Theme.foregroundFillBinding(), -1, -1)));
btnImportModpack.setText(I18n.i18n("install.modpack"));
btnImportModpack.setOnMouseClicked(e -> skinnable.importModpack());
toolbar.getChildren().add(btnImportModpack);
JFXButton btnRefresh = new JFXButton();
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
btnRefresh.textFillProperty().bind(Theme.foregroundFillBinding());
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)));
btnRefresh.setText(I18n.i18n("button.refresh"));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
toolbar.getChildren().add(btnRefresh);
JFXButton btnModify = new JFXButton();
btnModify.getStyleClass().add("jfx-tool-bar-button");
btnModify.textFillProperty().bind(Theme.foregroundFillBinding());
btnModify.setGraphic(wrap(SVG.gear(Theme.foregroundFillBinding(), -1, -1)));
btnModify.setText(I18n.i18n("settings.type.global.manage"));
btnModify.setOnMouseClicked(e -> skinnable.modifyGlobalGameSettings());
toolbar.getChildren().add(btnModify);
root.setTop(toolbar);
}
{
StackPane center = new StackPane();
JFXSpinner spinner = new JFXSpinner();
spinner.getStyleClass().setAll("first-spinner");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
VBox gameList = new VBox();
gameList.maxWidthProperty().bind(scrollPane.widthProperty());
gameList.setSpacing(10);
gameList.setStyle("-fx-padding: 10 10 10 10;");
Bindings.bindContent(gameList.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(gameList);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.onChangeAndOperate(skinnable.loadingProperty(),
loading -> center.getChildren().setAll(loading ? spinner : scrollPane));
root.setCenter(center);
}
getChildren().setAll(root);
}
}
@@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
@@ -26,53 +29,128 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.download.InstallerWizardProvider;
import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Optional;
import java.util.List;
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;
public class InstallerListPage extends ListPage<InstallerItem> {
public class InstallerListPage extends ListPageBase<InstallerItem> {
private Profile profile;
private String versionId;
private Version version;
private String gameVersion;
{
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "exe").contains(FileUtils.getExtension(it)), mods -> {
if (!mods.isEmpty())
doInstallOffline(mods.get(0));
});
}
@Override
protected Skin<?> createDefaultSkin() {
return new InstallerListPageSkin();
}
public void loadVersion(Profile profile, String versionId) {
this.profile = profile;
this.versionId = versionId;
this.version = profile.getRepository().getResolvedVersion(versionId);
this.gameVersion = null;
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
Task.ofResult(() -> {
gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse(null);
Function<Library, Consumer<InstallerItem>> removeAction = library -> x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
return LibraryAnalyzer.analyze(version);
}).thenAccept(Schedulers.javafx(), analyzer -> {
Function<Library, Consumer<InstallerItem>> removeAction = library -> x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(maintainedVersion -> new VersionJsonSaveTask(profile.getRepository(), maintainedVersion))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
itemsProperty().clear();
analyzer.getForge().ifPresent(library -> itemsProperty().add(new InstallerItem("Forge", library.getVersion(), removeAction.apply(library))));
analyzer.getLiteLoader().ifPresent(library -> itemsProperty().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction.apply(library))));
analyzer.getOptiFine().ifPresent(library -> itemsProperty().add(new InstallerItem("OptiFine", library.getVersion(), removeAction.apply(library))));
itemsProperty().clear();
analyzer.get(FORGE).ifPresent(library -> itemsProperty().add(
new InstallerItem("Forge", library.getVersion(), () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, "forge", library));
}, removeAction.apply(library))));
analyzer.get(LITELOADER).ifPresent(library -> itemsProperty().add(
new InstallerItem("LiteLoader", library.getVersion(), () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, "liteloader", library));
}, removeAction.apply(library))));
analyzer.get(OPTIFINE).ifPresent(library -> itemsProperty().add(
new InstallerItem("OptiFine", library.getVersion(), () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, "optifine", library));
}, removeAction.apply(library))));
analyzer.get(FABRIC).ifPresent(library -> itemsProperty().add(new InstallerItem("Fabric", library.getVersion(), null, null)));
}).start();
}
@Override
public void add() {
Optional<String> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version));
if (!gameVersion.isPresent())
public void installOnline() {
if (gameVersion == null)
Controllers.dialog(i18n("version.cannot_read"));
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version));
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion, version));
}
public void installOffline() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("install.installer.install_offline.extension"), "*.jar", "*.exe"));
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null) doInstallOffline(file);
}
private void doInstallOffline(File file) {
Task task = profile.getDependency().installLibraryAsync(version, file.toPath())
.then(profile.getRepository().refreshVersionsAsync());
task.setName(i18n("install.installer.install_offline"));
TaskExecutor executor = task.executor(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
runInFX(() -> {
if (success) {
loadVersion(profile, versionId);
Controllers.dialog(i18n("install.success"));
} else {
if (executor.getLastException() == null)
return;
InstallerWizardProvider.alertFailureMessage(executor.getLastException(), null);
}
});
}
});
Controllers.taskDialog(executor, i18n("install.installer.install_offline"));
executor.start();
}
private class InstallerListPageSkin extends ToolbarListPageSkin<InstallerListPage> {
InstallerListPageSkin() {
super(InstallerListPage.this);
}
@Override
protected List<Node> initializeToolbar(InstallerListPage skinnable) {
return Arrays.asList(
createToolbarButton(i18n("install.installer.install_online"), SVG::plus, skinnable::installOnline),
createToolbarButton(i18n("install.installer.install_offline"), SVG::plus, skinnable::installOffline));
}
}
}
@@ -1,73 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModItem extends BorderPane {
public ModItem(ModInfo info, Consumer<ModItem> deleteCallback) {
JFXCheckBox chkEnabled = new JFXCheckBox();
BorderPane.setAlignment(chkEnabled, Pos.CENTER);
setLeft(chkEnabled);
TwoLineListItem modItem = new TwoLineListItem();
BorderPane.setAlignment(modItem, Pos.CENTER);
setCenter(modItem);
JFXButton btnRemove = new JFXButton();
JFXUtilities.runInFX(() -> {
FXUtils.installFastTooltip(btnRemove, i18n("mods.remove"));
});
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
setRight(btnRemove);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
modItem.setTitle(info.getFileName());
StringBuilder message = new StringBuilder(info.getName());
if (StringUtils.isNotBlank(info.getVersion()))
message.append(", ").append(i18n("archive.version")).append(": ").append(info.getVersion());
if (StringUtils.isNotBlank(info.getGameVersion()))
message.append(", ").append(i18n("archive.game_version")).append(": ").append(info.getGameVersion());
if (StringUtils.isNotBlank(info.getAuthors()))
message.append(", ").append(i18n("archive.author")).append(": ").append(info.getAuthors());
modItem.setSubtitle(message.toString());
chkEnabled.setSelected(info.isActive());
chkEnabled.selectedProperty().addListener((a, b, newValue) ->
info.activeProperty().set(newValue));
}
}
@@ -17,9 +17,14 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXTabPane;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.setting.Profile;
@@ -27,7 +32,7 @@ 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.ListPage;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -36,17 +41,21 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModListPage extends ListPage<ModItem> {
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> {
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
private JFXTabPane parentTab;
private ModManager modManager;
private LibraryAnalyzer libraryAnalyzer;
public ModListPage() {
setRefreshable(true);
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
mods.forEach(it -> {
@@ -61,60 +70,38 @@ public final class ModListPage extends ListPage<ModItem> {
}
@Override
protected Skin<?> createDefaultSkin() {
return new ModListPageSkin(this);
}
public void refresh() {
loadMods(modManager);
}
public void loadVersion(Profile profile, String id) {
libraryAnalyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedVersion(id));
modded.set(libraryAnalyzer.hasModLoader());
loadMods(profile.getRepository().getModManager(id));
}
public void loadMods(ModManager modManager) {
private void loadMods(ModManager modManager) {
this.modManager = modManager;
Task.of(variables -> {
Task.ofResult(() -> {
synchronized (ModListPage.this) {
JFXUtilities.runInFX(() -> loadingProperty().set(true));
runInFX(() -> loadingProperty().set(true));
modManager.refreshMods();
// Surprisingly, if there are a great number of mods, this processing will cause a long UI pause,
// constructing UI elements.
// We must do this asynchronously.
LinkedList<ModItem> list = new LinkedList<>();
for (ModInfo modInfo : modManager.getMods()) {
ModItem item = new ModItem(modInfo, i -> {
try {
modManager.removeMods(modInfo);
} catch (IOException ignore) {
// Fail to remove mods if the game is running or the mod is absent.
}
loadMods(modManager);
});
modInfo.activeProperty().addListener((a, b, newValue) -> {
if (newValue)
item.getStyleClass().remove("disabled");
else
item.getStyleClass().add("disabled");
});
if (!modInfo.isActive())
item.getStyleClass().add("disabled");
list.add(item);
}
variables.set("list", list);
return new LinkedList<>(modManager.getMods());
}
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
}).whenComplete(Schedulers.javafx(), (list, isDependentSucceeded, exception) -> {
loadingProperty().set(false);
if (isDependentsSucceeded)
if (isDependentSucceeded)
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue != null && newValue.getUserData() == ModListPage.this)
itemsProperty().setAll(variables.<List<ModItem>>get("list"));
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).collect(Collectors.toList()));
});
}).start();
}
@Override
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("mods.choose_mod"));
@@ -125,7 +112,7 @@ public final class ModListPage extends ListPage<ModItem> {
List<String> succeeded = new LinkedList<>();
List<String> failed = new LinkedList<>();
if (res == null) return;
Task.of(variables -> {
Task.of(() -> {
for (File file : res) {
try {
modManager.addMod(file);
@@ -137,7 +124,7 @@ public final class ModListPage extends ListPage<ModItem> {
// Actually addMod will not throw exceptions because FileChooser has already filtered files.
}
}
}).with(Task.of(Schedulers.javafx(), variables -> {
}).with(Task.of(Schedulers.javafx(), () -> {
List<String> prompt = new LinkedList<>();
if (!succeeded.isEmpty())
prompt.add(i18n("mods.add.success", String.join(", ", succeeded)));
@@ -151,4 +138,43 @@ public final class ModListPage extends ListPage<ModItem> {
public void setParentTab(JFXTabPane parentTab) {
this.parentTab = parentTab;
}
public void removeSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
try {
modManager.removeMods(selectedItems.stream()
.map(TreeItem::getValue)
.filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.toArray(ModInfo[]::new));
loadMods(modManager);
} catch (IOException ignore) {
// Fail to remove mods if the game is running or the mod is absent.
}
}
public void enableSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(true));
}
public void disableSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(false));
}
public boolean isModded() {
return modded.get();
}
public BooleanProperty moddedProperty() {
return modded;
}
public void setModded(boolean modded) {
this.modded.set(modded);
}
}
@@ -0,0 +1,146 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXTreeTableColumn;
import com.jfoenix.controls.JFXTreeTableView;
import com.jfoenix.controls.RecursiveTreeItem;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import static org.jackhuang.hmcl.ui.FXUtils.setupCellValueFactory;
import static org.jackhuang.hmcl.ui.FXUtils.wrapMargin;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class ModListPageSkin extends SkinBase<ModListPage> {
ModListPageSkin(ModListPage skinnable) {
super(skinnable);
StackPane pane = new StackPane();
pane.getStyleClass().addAll("notice-pane", "white-background");
BorderPane root = new BorderPane();
JFXTreeTableView<ModInfoObject> tableView = new JFXTreeTableView<>();
{
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh));
toolbar.getChildren().add(createToolbarButton(i18n("mods.add"), SVG::plus, skinnable::add));
toolbar.getChildren().add(createToolbarButton(i18n("mods.remove"), SVG::delete, () ->
skinnable.removeSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG::check, () ->
skinnable.enableSelected(tableView.getSelectionModel().getSelectedItems())));
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG::close, () ->
skinnable.disableSelected(tableView.getSelectionModel().getSelectedItems())));
root.setTop(toolbar);
}
{
SpinnerPane center = new SpinnerPane();
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
tableView.getStyleClass().add("no-header");
tableView.setShowRoot(false);
tableView.setEditable(true);
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableView.setRoot(new RecursiveTreeItem<>(skinnable.getItems(), RecursiveTreeObject::getChildren));
JFXTreeTableColumn<ModInfoObject, Boolean> activeColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(activeColumn, ModInfoObject::activeProperty);
activeColumn.setCellFactory(c -> new JFXCheckBoxTreeTableCell<>());
activeColumn.setEditable(true);
activeColumn.setMaxWidth(40);
activeColumn.setMinWidth(40);
JFXTreeTableColumn<ModInfoObject, Node> detailColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(detailColumn, ModInfoObject::nodeProperty);
tableView.getColumns().setAll(activeColumn, detailColumn);
tableView.setColumnResizePolicy(JFXTreeTableView.CONSTRAINED_RESIZE_POLICY);
center.setContent(tableView);
root.setCenter(center);
}
Label label = new Label(i18n("mods.not_modded"));
label.prefWidthProperty().bind(pane.widthProperty().add(-100));
FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> {
if (modded) pane.getChildren().setAll(root);
else pane.getChildren().setAll(label);
});
getChildren().setAll(pane);
}
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> {
private final BooleanProperty active;
private final ModInfo modInfo;
private final ObjectProperty<Node> node;
ModInfoObject(ModInfo modInfo) {
this.modInfo = modInfo;
this.active = modInfo.activeProperty();
StringBuilder message = new StringBuilder(modInfo.getName());
if (isNotBlank(modInfo.getVersion()))
message.append(", ").append(i18n("archive.version")).append(": ").append(modInfo.getVersion());
if (isNotBlank(modInfo.getGameVersion()))
message.append(", ").append(i18n("archive.game_version")).append(": ").append(modInfo.getGameVersion());
if (isNotBlank(modInfo.getAuthors()))
message.append(", ").append(i18n("archive.author")).append(": ").append(modInfo.getAuthors());
this.node = new SimpleObjectProperty<>(wrapMargin(new TwoLineListItem(modInfo.getFileName(), message.toString()), new Insets(8, 0, 8, 0)));
}
BooleanProperty activeProperty() {
return active;
}
ObjectProperty<Node> nodeProperty() {
return node;
}
ModInfo getModInfo() {
return modInfo;
}
}
}
@@ -34,11 +34,7 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -57,10 +53,11 @@ import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -71,7 +68,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private Profile profile;
private String versionId;
private boolean javaItemsLoaded;
private InvalidationListener specificSettingsListener;
@FXML private VBox rootPane;
@FXML private ScrollPane scroll;
@@ -100,6 +96,12 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
@FXML private JFXCheckBox chkEnableSpecificSettings;
@FXML private BorderPane settingsTypePane;
private InvalidationListener specificSettingsListener = any -> {
chkEnableSpecificSettings.setSelected(!lastVersionSetting.isUsesGlobal());
};
private InvalidationListener javaListener = any -> initJavaSubtitle();
public VersionSettingsPage() {
FXUtils.loadFXML(this, "/assets/fxml/version/version-settings.fxml");
@@ -113,15 +115,14 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.smoothScrolling(scroll);
Task.of(variables -> variables.set("list", JavaVersion.getJavas()))
.subscribe(Schedulers.javafx(), variables -> {
javaItem.loadChildren(
(variables.<List<JavaVersion>>get("list")).stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion() + i18n("settings.game.java_directory.bit", javaVersion.getPlatform().getBit()), javaVersion.getBinary().toString(), javaVersion))
.collect(Collectors.toList()));
javaItemsLoaded = true;
initializeSelectedJava();
});
Task.ofResult(JavaVersion::getJavas).thenAccept(Schedulers.javafx(), list -> {
javaItem.loadChildren(list.stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion() + i18n("settings.game.java_directory.bit",
javaVersion.getPlatform().getBit()), javaVersion.getBinary().toString(), javaVersion))
.collect(Collectors.toList()));
javaItemsLoaded = true;
initializeSelectedJava();
}).start();
javaItem.setSelectedData(null);
javaItem.setFallbackData(JavaVersion.fromCurrentEnvironment());
@@ -148,10 +149,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
Platform.runLater(() -> loadVersion(profile, versionId));
});
specificSettingsListener = o -> {
chkEnableSpecificSettings.setSelected(!lastVersionSetting.isUsesGlobal());
};
componentList.disableProperty().bind(chkEnableSpecificSettings.selectedProperty().not());
advancedSettingsPane.disableProperty().bind(chkEnableSpecificSettings.selectedProperty().not());
}
@@ -193,6 +190,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.unbindEnum(cboLauncherVisibility);
lastVersionSetting.usesGlobalProperty().removeListener(specificSettingsListener);
lastVersionSetting.javaDirProperty().removeListener(javaListener);
lastVersionSetting.javaProperty().removeListener(javaListener);
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().unbind();
@@ -232,9 +231,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
});
versionSetting.javaDirProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
initJavaSubtitle(versionSetting);
versionSetting.javaDirProperty().addListener(javaListener);
versionSetting.javaProperty().addListener(javaListener);
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
@@ -243,6 +241,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
lastVersionSetting = versionSetting;
initializeSelectedJava();
initJavaSubtitle();
loadIcon();
}
@@ -267,11 +266,14 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
}
private void initJavaSubtitle(VersionSetting versionSetting) {
Task.of(variables -> variables.set("java", versionSetting.getJavaVersion()))
.subscribe(Task.of(Schedulers.javafx(),
variables -> javaItem.setSubtitle(variables.<JavaVersion>getOptional("java")
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Directory"))));
private void initJavaSubtitle() {
VersionSetting versionSetting = lastVersionSetting;
if (versionSetting == null)
return;
Task.ofResult(versionSetting::getJavaVersion)
.thenAccept(Schedulers.javafx(), javaVersion -> javaItem.setSubtitle(Optional.ofNullable(javaVersion)
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
.start();
}
@FXML
@@ -311,7 +313,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private void loadIcon() {
if (versionId == null) {
iconPickerItem.setImage(new Image("/assets/img/grass.png"));
iconPickerItem.setImage(newImage("/assets/img/grass.png"));
return;
}
@@ -319,7 +321,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
if (iconFile.exists())
iconPickerItem.setImage(new Image("file:" + iconFile.getAbsolutePath()));
else
iconPickerItem.setImage(new Image("/assets/img/grass.png"));
iconPickerItem.setImage(newImage("/assets/img/grass.png"));
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
@@ -50,9 +50,7 @@ public class Versions {
isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") :
i18n("version.manage.remove.confirm", version);
Controllers.confirmDialog(message, i18n("message.confirm"), () -> {
if (profile.getRepository().removeVersionFromDisk(version)) {
profile.getRepository().refreshVersionsAsync().start();
}
profile.getRepository().removeVersionFromDisk(version);
}, null);
}
@@ -81,8 +79,7 @@ public class Versions {
public static void updateGameAssets(Profile profile, String version) {
Version resolvedVersion = profile.getRepository().getResolvedVersion(version);
TaskExecutor executor = new GameAssetIndexDownloadTask(profile.getDependency(), resolvedVersion)
.then(new GameAssetDownloadTask(profile.getDependency(), resolvedVersion))
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), resolvedVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY)
.executor();
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"));
executor.start();
@@ -35,6 +35,7 @@ import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListItem extends Control {
@@ -48,7 +49,7 @@ public class WorldListItem extends Control {
this.world = world;
this.simpleDateFormat = new SimpleDateFormat(i18n("world.time"));
title.set(world.getWorldName());
title.set(parseColorEscapes(world.getWorldName()));
subtitle.set(i18n("world.description", world.getFileName(), simpleDateFormat.format(new Date(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
}
@@ -17,49 +17,83 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Node;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
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.ListPage;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListPage extends ListPage<WorldListItem> {
public class WorldListPage extends ListPageBase<WorldListItem> {
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
private Path savesDir;
private List<World> worlds;
private Profile profile;
private String id;
private String gameVersion;
public WorldListPage() {
FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
installWorld(modpacks.get(0));
});
}
public void loadVersion(Profile profile, String id) {
this.savesDir = profile.getRepository().getRunDirectory(id).toPath().resolve("saves");
setLoading(true);
Task.ofResult(() -> World.getWorlds(savesDir).parallel().collect(Collectors.toList()))
.finalizedResult(Schedulers.javafx(), (result, isDependentsSucceeded) -> {
setLoading(false);
if (isDependentsSucceeded)
itemsProperty().setAll(result.stream().map(WorldListItem::new).collect(Collectors.toList()));
}).start();
showAll.addListener(e -> {
if (worlds != null)
itemsProperty().setAll(worlds.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(WorldListItem::new).collect(Collectors.toList()));
});
}
@Override
protected ToolbarListPageSkin createDefaultSkin() {
return new WorldListPageSkin();
}
public void loadVersion(Profile profile, String id) {
this.profile = profile;
this.id = id;
this.savesDir = profile.getRepository().getRunDirectory(id).toPath().resolve("saves");
refresh();
}
public void refresh() {
if (profile == null || id == null)
return;
setLoading(true);
Task
.of(() -> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse(null))
.thenSupply(() -> World.getWorlds(savesDir).parallel().collect(Collectors.toList()))
.whenComplete(Schedulers.javafx(), (result, isDependentSucceeded, exception) -> {
worlds = result;
setLoading(false);
if (isDependentSucceeded)
itemsProperty().setAll(result.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(WorldListItem::new).collect(Collectors.toList()));
}).start();
}
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("world.import.choose"));
@@ -74,10 +108,10 @@ public class WorldListPage extends ListPage<WorldListItem> {
// Only accept one world file because user is required to confirm the new world name
// Or too many input dialogs are popped.
Task.ofResult(() -> new World(zipFile.toPath()))
.finalizedResult(Schedulers.javafx(), world -> {
.whenComplete(Schedulers.javafx(), world -> {
Controllers.inputDialog(i18n("world.name.enter"), (name, resolve, reject) -> {
Task.of(() -> world.install(savesDir, name))
.finalized(Schedulers.javafx(), var -> {
.whenComplete(Schedulers.javafx(), () -> {
itemsProperty().add(new WorldListItem(new World(savesDir.resolve(name))));
resolve.run();
}, e -> {
@@ -92,4 +126,36 @@ public class WorldListPage extends ListPage<WorldListItem> {
Controllers.dialog(i18n("world.import.invalid"));
}).start();
}
public boolean isShowAll() {
return showAll.get();
}
public BooleanProperty showAllProperty() {
return showAll;
}
public void setShowAll(boolean showAll) {
this.showAll.set(showAll);
}
private class WorldListPageSkin extends ToolbarListPageSkin<WorldListPage> {
WorldListPageSkin() {
super(WorldListPage.this);
}
@Override
protected List<Node> initializeToolbar(WorldListPage skinnable) {
JFXCheckBox chkShowAll = new JFXCheckBox();
chkShowAll.getStyleClass().add("jfx-tool-bar-checkbox");
chkShowAll.textFillProperty().bind(Theme.foregroundFillBinding());
chkShowAll.setText(i18n("world.show_all"));
chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty());
return Arrays.asList(chkShowAll,
createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
createToolbarButton(i18n("world.add"), SVG::plus, skinnable::add));
}
}
}
@@ -17,19 +17,19 @@
*/
package org.jackhuang.hmcl.ui.wizard;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Map;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplayer {
@@ -59,17 +59,16 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
pane.setSubtitle((String) subtitle);
}
JFXUtilities.runInFX(() -> {
runInFX(() -> {
TaskExecutor executor = task.executor(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
JFXUtilities.runInFX(() -> {
pane.fireEvent(new DialogCloseEvent());
runInFX(() -> {
if (success) {
if (settings.containsKey("success_message") && settings.get("success_message") instanceof String)
Controllers.dialog((String) settings.get("success_message"), null, MessageBox.FINE_MESSAGE, () -> onEnd());
Controllers.dialog((String) settings.get("success_message"), null, MessageType.FINE, () -> onEnd());
else if (!settings.containsKey("forbid_success_message"))
Controllers.dialog(i18n("message.success"), null, MessageBox.FINE_MESSAGE, () -> onEnd());
Controllers.dialog(i18n("message.success"), null, MessageType.FINE, () -> onEnd());
} else {
if (executor.getLastException() == null)
return;
@@ -77,9 +76,9 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
if (settings.get("failure_callback") instanceof WizardProvider.FailureCallback)
((WizardProvider.FailureCallback)settings.get("failure_callback")).onFail(settings, executor.getLastException(), () -> onEnd());
else if (settings.get("failure_message") instanceof String)
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> onEnd());
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageType.ERROR, () -> onEnd());
else if (!settings.containsKey("forbid_failure_message"))
Controllers.dialog(appendix, i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> onEnd());
Controllers.dialog(appendix, i18n("wizard.failed"), MessageType.ERROR, () -> onEnd());
}
});
@@ -83,7 +83,12 @@ public class WizardController implements Navigation {
@Override
public void onPrev(boolean cleanUp) {
if (!canPrev()) {
throw new IllegalStateException("Cannot go backward since this is the back page. Pages: " + pages);
if (provider.cancelIfCannotGoBack()) {
onCancel();
return;
} else {
throw new IllegalStateException("Cannot go backward since this is the back page. Pages: " + pages);
}
}
Node page = pages.pop();
@@ -26,6 +26,9 @@ public interface WizardProvider {
Object finish(Map<String, Object> settings);
Node createPage(WizardController controller, int step, Map<String, Object> settings);
boolean cancel();
default boolean cancelIfCannotGoBack() {
return false;
}
interface FailureCallback {
void onFail(Map<String, Object> settings, Exception exception, Runnable next);
@@ -70,6 +70,10 @@ public final class IntegrityChecker {
for (ZipEntry entry : zip.stream().toArray(ZipEntry[]::new)) {
String filename = entry.getName();
try (InputStream in = zip.getInputStream(entry)) {
if (in == null) {
throw new IOException("entry is null");
}
if (SIGNATURE_FILE.equals(filename)) {
signature = IOUtils.readFullyAsByteArray(in);
} else {
@@ -26,7 +26,8 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.UpgradeDialog;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
@@ -91,41 +92,42 @@ public final class UpdateHandler {
public static void updateFrom(RemoteVersion version) {
checkFxUserThread();
Path downloaded;
try {
downloaded = Files.createTempFile("hmcl-update-", ".jar");
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create temp file", e);
return;
}
Controllers.dialog(new UpgradeDialog(() -> {
Path downloaded;
try {
downloaded = Files.createTempFile("hmcl-update-", ".jar");
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create temp file", e);
return;
}
Task task = new HMCLDownloadTask(version, downloaded);
Task task = new HMCLDownloadTask(version, downloaded);
TaskExecutor executor = task.executor();
Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"));
thread(() -> {
boolean success = executor.test();
TaskExecutor executor = task.executor();
Controllers.taskDialog(executor, i18n("message.downloading"));
thread(() -> {
boolean success = executor.test();
if (success) {
try {
if (!IntegrityChecker.isSelfVerified()) {
throw new IOException("Current JAR is not verified");
if (success) {
try {
if (!IntegrityChecker.isSelfVerified()) {
throw new IOException("Current JAR is not verified");
}
requestUpdate(downloaded, getCurrentLocation());
System.exit(0);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR));
}
requestUpdate(downloaded, getCurrentLocation());
System.exit(0);
} catch (IOException e) {
} else {
Throwable e = executor.getLastException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
return;
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageType.ERROR));
}
} else {
Throwable e = task.getLastException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
}
});
});
}));
}
private static void applyUpdate(Path target) throws IOException {
@@ -18,9 +18,10 @@
package org.jackhuang.hmcl.util;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.CrashWindow;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -50,6 +51,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
put("Location is not set", i18n("crash.NoClassDefFound"));
put("UnsatisfiedLinkError", i18n("crash.user_fault"));
put("java.lang.NoClassDefFoundError", i18n("crash.NoClassDefFound"));
put("org.jackhuang.hmcl.util.ResourceNotFoundError", i18n("crash.NoClassDefFound"));
put("java.lang.VerifyError", i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchMethodError", i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchFieldError", i18n("crash.NoClassDefFound"));
@@ -69,7 +71,10 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
String info = entry.getValue();
LOG.severe(info);
try {
MessageBox.show(info);
Alert alert = new Alert(AlertType.INFORMATION, info);
alert.setTitle(i18n("message.info"));
alert.setHeaderText(i18n("message.info"));
alert.showAndWait();
} catch (Throwable t) {
LOG.log(Level.SEVERE, "Unable to show message", t);
}
@@ -103,7 +108,10 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
"-- System Details --\n" +
" Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n";
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
LOG.log(Level.SEVERE, text);
@@ -15,29 +15,27 @@
* 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;
package org.jackhuang.hmcl.util;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.Lang;
import java.util.Optional;
import java.io.InputStream;
/**
* @author huangyuhui
* Suppress the throwable when we make sure the resource cannot miss.
* @see CrashReporter
*/
public final class SafeIntStringConverter extends StringConverter<Number> {
public static final SafeIntStringConverter INSTANCE = new SafeIntStringConverter();
private SafeIntStringConverter() {
public class ResourceNotFoundError extends Error {
public ResourceNotFoundError(String message) {
super(message);
}
@Override
public Number fromString(String string) {
return Optional.ofNullable(string).map(Lang::toIntOrNull).orElse(null);
public ResourceNotFoundError(String message, Throwable cause) {
super(message, cause);
}
@Override
public String toString(Number object) {
return Optional.ofNullable(object).map(Object::toString).orElse("");
public static InputStream getResourceAsStream(String url) {
InputStream stream = ResourceNotFoundError.class.getResourceAsStream(url);
if (stream == null)
throw new ResourceNotFoundError("Resource not found: " + url);
return stream;
}
}
@@ -57,4 +57,12 @@ public final class I18n {
}
}
public static boolean hasKey(String key) {
try {
getResourceBundle().getString(key);
return true;
} catch (MissingResourceException e) {
return false;
}
}
}
@@ -58,10 +58,6 @@ public final class Locales {
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH);
public static SupportedLocale getLocale(int index) {
return Lang.get(LOCALES, index).orElse(DEFAULT);
}
public static SupportedLocale getLocaleByName(String name) {
if (name == null) return DEFAULT;
switch (name.toLowerCase()) {
Binary file not shown.
+46 -182
View File
@@ -57,8 +57,11 @@
-fx-text-fill: -fx-base-text-fill;
}
.rippler-container= .label {
-fx-text-fill: white;
.notice-pane > .label {
-fx-text-fill: #0079FF;
-fx-font-size: 20;
-fx-wrap-text: true;
-fx-text-alignment: CENTER;
}
.class-title {
@@ -132,6 +135,16 @@
-fx-fill: -fx-base-darker-color;
}
.sponsor-pane {
-fx-padding: 16;
-fx-background-color: white;
-fx-cursor: hand;
}
.sponsor-pane .label {
-fx-wrap-text: true;
}
/*******************************************************************************
* *
* JFX Tab Pane *
@@ -342,12 +355,11 @@
.jfx-tool-bar HBox {
-fx-alignment: center;
/* -fx-spacing: 25.0;*/
-fx-padding: 0.0 5.0;
}
.jfx-tool-bar .jfx-decorator-button {
-fx-cursor: head;
-fx-cursor: hand;
}
.jfx-tool-bar Label {
@@ -370,6 +382,8 @@
-fx-pref-height: 42;
-fx-padding: 2 2 2 2;
-fx-background-color: -fx-base-check-color;
-fx-alignment: CENTER-LEFT;
-fx-spacing: 8;
}
.jfx-tool-bar-second .jfx-rippler {
@@ -447,33 +461,6 @@
-fx-fill: -fx-base-check-color;
}
.custom-jfx-radio-button {
-fx-font-size: 16.0px;
}
.custom-jfx-radio-button .radio {
-fx-stroke-width: 2.0px;
-fx-fill: transparent;
}
.custom-jfx-radio-button-blue {
-fx-text-fill: -fx-base-color;
-jfx-selected-color: -fx-base-color;
-jfx-unselected-color: #212121;
}
.custom-jfx-radio-button-red {
-fx-text-fill: #f44336;
-jfx-selected-color: #f44336;
-jfx-unselected-color: #b71c1c;
}
.custom-jfx-radio-button-green {
-fx-text-fill: #4caf50;
-jfx-selected-color: #4caf50;
-jfx-unselected-color: #1b5e20;
}
/*******************************************************************************
* *
* JFX Slider *
@@ -514,18 +501,12 @@
-fx-font-size: 10.0;
}
/******************************************************/
/*******************************************************************************
* *
* JFX Rippler *
* *
*******************************************************************************/
/*.jfx-rippler {
-fx-rippler-fill: -fx-base-color;
-fx-mask-type: RECT;
}*/
.jfx-rippler:hover {
-fx-cursor: hand;
}
@@ -536,62 +517,29 @@
* *
*******************************************************************************/
.custom-jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: YELLOW;
}
.custom-jfx-button-raised {
-fx-padding: 0.7em 0.57em;
-fx-font-size: 14.0px;
-jfx-button-type: RAISED;
-fx-background-color: rgb(102.0, 153.0, 102.0);
-fx-pref-width: 200.0;
-fx-text-fill: WHITE;
}
.circle-jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: YELLOW;
}
.circle-jfx-button-raised {
-fx-padding: 0.7em 0.57em;
-fx-font-size: 14.0px;
-jfx-button-type: RAISED;
-fx-background-color: rgb(102.0, 153.0, 102.0);
-fx-pref-width: 200.0;
-fx-text-fill: WHITE;
-jfx-mask-type: CIRCLE;
.jfx-button {
-jfx-disable-visual-focus: true;
-fx-cursor: hand;
}
.jfx-button-raised {
-fx-text-fill: white;
-fx-background-color: -fx-base-color;
-fx-font-size: 14px;
}
.jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: white;
}
.jfx-button-raised .label {
-fx-text-fill: white;
.jfx-button-raised, .jfx-button-raised * {
-fx-text-fill: -fx-base-text-fill;
-fx-font-size: 14px;
}
.jfx-button-border {
-fx-text-fill: -fx-base-color;
-fx-border-color: gray;
-fx-border-radius: 5px;
-fx-border-width: 0.2px;
-fx-padding: 8px;
}
.jfx-button-border .jfx-rippler {
-jfx-rippler-fill: -fx-base-check-color;
}
.jfx-button-border .label {
-fx-text-fill: -fx-base-color;
.jfx-button-border, .jfx-button-border * {
-fx-text-fill: -fx-base-darker-color;
}
.jfx-button-raised-round {
@@ -610,20 +558,6 @@
-jfx-checked-color: -fx-base-check-color;
}
.custom-jfx-check-box {
-jfx-checked-color: RED;
}
.custom-jfx-check-box-all-colored {
-jfx-checked-color: -fx-base-color;
-jfx-unchecked-color: -fx-base-color;
-fx-text-fill: -fx-base-color;
}
.custom-jfx-check-box-text-colored {
-fx-text-fill: rgb(153.0, 0.0, 0.0);
}
/*******************************************************************************
* *
* JFX Progress Bar *
@@ -647,15 +581,6 @@
-fx-background-color: -fx-base-check-color;
}
.custom-jfx-progress-bar > .bar {
-fx-background-color: rgb(255.0, 128.0, 0.0);
}
.custom-jfx-progress-bar-stroke > .bar {
-fx-background-color: -fx-base-color;
-fx-padding: 6;
}
/*******************************************************************************
* *
* JFX Textfield *
@@ -708,47 +633,6 @@
-jfx-expanded: false;
}
.custom-jfx-list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
.custom-jfx-list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
-fx-background-color: rgba(255, 0, 0, 0.2);
}
.custom-jfx-list-view {
-fx-background-insets: 0.0;
-jfx-cell-horizontal-margin: 0.0;
-jfx-cell-vertical-margin: 5.0;
-jfx-expanded: false;
-fx-max-width: 200.0px;
/* important to hide the list change of height */
-fx-background-color: TRANSPARENT;
}
.custom-jfx-list-view .jfx-rippler {
-jfx-rippler-fill: RED;
}
.custom-jfx-list-view1 {
-jfx-vertical-gap: 10.0;
-fx-pref-width: 150px;
-fx-background-color: transparent;
}
.custom-jfx-list-view-icon,
.jfx-list-cell:selected .label .custom-jfx-list-view-icon {
-fx-fill: -fx-base-color;
-fx-padding: 0.0 10.0 0.0 5.0;
-fx-cursor: hand;
}
.custom-jfx-list-view-icon-container {
-fx-pref-width: 40px;
}
.custom-jfx-list-view .sublist-item {
-fx-border-color: #e0e0e0;
-fx-border-width: 1 0 1 0;
}
.options-list {
-fx-background-color: transparent;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
@@ -798,20 +682,6 @@
-jfx-mask-type: CIRCLE;
}
.custom-jfx-list-view .jfx-list-cell .sublist-header > .drop-icon {
-fx-background-color: GRAY;
}
.custom-jfx-list-view .jfx-list-cell:filled:hover .sublist-header > .drop-icon {
-fx-background-color: BLACK;
}
/*******************************************************************************/
/*******************************************************************************/
/*******************************************************************************/
/*******************************************************************************/
/*******************************************************************************/
/*******************************************************************************/
/*******************************************************************************
* *
* JFX SUBLIST IMPORTANT *
@@ -851,10 +721,6 @@
-fx-padding: 0 0 0 12;
}
/*.custom-jfx-list-view .sublist-container {
-fx-padding : 0 0 5 0;
}*/
/*******************************************************************************
* *
* JFX Toggle Button *
@@ -865,14 +731,6 @@
-jfx-toggle-color: -fx-base-check-color;
}
.custom-jfx-toggle-button {
-jfx-toggle-color: #4285F4;
}
.custom-jfx-toggle-button-red {
-jfx-toggle-color: red;
}
.toggle-label {
-fx-font-size: 14.0px;
}
@@ -1038,6 +896,14 @@
-fx-stroke-width: 3.0;
}
.large-spinner-pane .jfx-spinner {
-jfx-radius: 20;
}
.large-spinner-pane .jfx-spinner {
-fx-stroke-width: 5.0;
}
.second-spinner {
-jfx-radius: 30;
}
@@ -1131,7 +997,8 @@
}
.jfx-decorator-title {
-fx-text-fill: -fx-base-text-fill; -fx-font-size: 15;
-fx-text-fill: -fx-base-text-fill;
-fx-font-size: 15;
}
.resize-border {
@@ -1149,7 +1016,7 @@
}
.white-background {
-fx-background-color: rgba(255, 255, 255);
-fx-background-color: rgb(255, 255, 255);
}
/*******************************************************************************
@@ -1158,6 +1025,12 @@
* *
*******************************************************************************/
.no-header .column-header-background {
-fx-max-height: 0;
-fx-pref-height: 0;
-fx-min-height: 0;
}
.tree-table-view {
-fx-tree-table-color: rgba(82, 100, 174, 0.4);
-fx-tree-table-rippler-color: rgba(82, 100, 174, 0.6);
@@ -1190,7 +1063,7 @@
.tree-table-view .column-header .label {
-fx-text-fill: #949494;
-fx-padding: 16 0 16 0;
-fx-padding: 8 0 8 0;
}
.tree-table-view .column-header .arrow, .tree-table-view .column-header .sort-order-dot {
@@ -1208,8 +1081,8 @@
.tree-table-view .tree-table-cell {
-fx-border-width: 0 0 0 0;
-fx-padding: 16 0 16 0;
-fx-alignment: top-center;
-fx-padding: 0 0 0 0;
/*-fx-alignment: top-center;*/
}
.tree-table-view .column-overlay {
@@ -1254,7 +1127,7 @@
}
.tree-table-row-group {
-fx-background-color: rgba(230, 230, 230);
-fx-background-color: rgb(230, 230, 230);
}
.tree-table-view .menu-item:focused {
@@ -1362,12 +1235,3 @@
.fit-width {
-fx-pref-width: 100%;
}
/*
.jfx-scroll-pane .main-header {
-fx-background-image: url("../bg1.jpg");
}
.jfx-scroll-pane .condensed-header {
-fx-background-image: url("../bg4.jpg");
}
*/
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.*?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<StackPane fx:id="contentPane">
<ScrollPane fx:id="scrollPane" fitToWidth="true" fitToHeight="true">
<VBox fx:id="listPane" spacing="10" style="-fx-padding: 20 20 70 20;">
</VBox>
</ScrollPane>
<VBox style="-fx-padding: 15;" spacing="15" pickOnBounds="false" alignment="BOTTOM_RIGHT">
<JFXButton prefWidth="40" prefHeight="40" buttonType="RAISED" onMouseClicked="#onAdd" styleClass="jfx-button-raised-round">
<graphic>
<fx:include source="/assets/svg/plus.fxml"/>
</graphic>
</JFXButton>
</VBox>
</StackPane>
</fx:root>
@@ -19,7 +19,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblForge">Install Forge</Label>
<Label fx:id="lblForge"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -31,7 +31,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblLiteLoader">Install LiteLoader</Label>
<Label fx:id="lblLiteLoader"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -43,7 +43,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblOptiFine">Install OptiFine</Label>
<Label fx:id="lblOptiFine"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -19,7 +19,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblForge">Install Forge</Label>
<Label fx:id="lblForge"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -31,7 +31,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblLiteLoader">Install LiteLoader</Label>
<Label fx:id="lblLiteLoader"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -43,7 +43,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblOptiFine">Install OptiFine</Label>
<Label fx:id="lblOptiFine"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -14,7 +14,7 @@
<fx:define>
<Insets fx:id="insets" bottom="12" />
</fx:define>
<SpinnerPane fx:id="spinnerPane">
<SpinnerPane fx:id="spinnerPane" styleClass="large-spinner-pane">
<VBox fx:id="borderPane" alignment="CENTER" FXUtils.limitWidth="500">
<HBox style="-fx-padding: 0 0 16 5;"><Label text="%modpack.task.install" /></HBox>
<ComponentList>
@@ -5,22 +5,31 @@
<?import javafx.scene.layout.*?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane"
type="BorderPane"
prefHeight="400.0" prefWidth="600.0">
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
<VBox fx:id="centrePane">
<HBox fx:id="checkPane" spacing="10" style="-fx-padding: 10;">
<JFXCheckBox fx:id="chkRelease" text="%version.game.release" selected="true" />
<JFXCheckBox fx:id="chkSnapshot" text="%version.game.snapshot" />
<JFXCheckBox fx:id="chkOld" text="%version.game.old" />
</HBox>
<JFXListView fx:id="list" styleClass="jfx-list-view" VBox.vgrow="ALWAYS">
</JFXListView>
</VBox>
<StackPane fx:id="failedPane">
<Label onMouseClicked="#onRefresh" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.refresh" />
</StackPane>
<StackPane fx:id="emptyPane">
<Label onMouseClicked="#onBack" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.empty" />
</StackPane>
<top>
<StackPane styleClass="sponsor-pane" onMouseClicked="#onSponsor">
<Label text="%sponsor.bmclapi" />
</StackPane>
</top>
<center>
<StackPane fx:id="root">
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
<VBox fx:id="centrePane">
<HBox fx:id="checkPane" spacing="10" style="-fx-padding: 10;">
<JFXCheckBox fx:id="chkRelease" text="%version.game.release" selected="true" />
<JFXCheckBox fx:id="chkSnapshot" text="%version.game.snapshot" />
<JFXCheckBox fx:id="chkOld" text="%version.game.old" />
</HBox>
<JFXListView fx:id="list" styleClass="jfx-list-view" VBox.vgrow="ALWAYS">
</JFXListView>
</VBox>
<StackPane fx:id="failedPane" styleClass="notice-pane">
<Label onMouseClicked="#onRefresh" text="%download.failed.refresh" />
</StackPane>
<StackPane fx:id="emptyPane" styleClass="notice-pane">
<Label onMouseClicked="#onBack" text="%download.failed.empty" />
</StackPane>
</StackPane>
</center>
</fx:root>
@@ -18,7 +18,7 @@
# Contributors: dxNeil
about.copyright=Copyright
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.copyright.statement=Copyright (c) 2019 huangyuhui.
about.author=Author
about.author.statement=huanghongxun ([email protected])
about.thanks_to=Thanks to
@@ -66,6 +66,7 @@ account.username=Name
archive.author=Authors
archive.game_version=Game
archive.name=Name
archive.version=Version
assets.download=Download assets
@@ -90,11 +91,12 @@ crash.NoClassDefFound=Please check "HMCL" software is complete.
crash.user_fault=Your OS or Java environment may not be properly installed resulting in crashing of this software, please check your Java Environment or your computer!
download=Download
download.code.404=File does not found in remote server
download.failed=Failed to download
download.failed.empty=No candidates. Click here to return.
download.failed.refresh=Unable to load version list. Click here to retry.
download.provider.bmclapi=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
download.provider.mojang=Mojang
download.provider.mojang=Mojang (Forge and OptiFine installation being downloaded from BMCLAPI)
extension.bat=Windows Bat file
extension.mod=Mod file
@@ -123,12 +125,24 @@ input.not_empty=Required field
input.url=Must be a valid URL.
install=Install New Game
install.change_version=Change version
install.change_version.confirm=Sure to update %s from verison %s to %s?
install.failed=Failed to install
install.failed.downloading=Failed to install due to some files not downloaded successfully
install.failed.downloading.detail=Failed to download file: %s
install.failed.downloading.timeout=Download timed out: %s
install.failed.install_online=Unable to recognize what you provided installer file is
install.failed.optifine_conflict=OptiFine and Forge are both installed simultaneously on Minecraft 1.13
install.failed.version_mismatch=The library requires game version %s, but actual version is %s.
install.installer.choose=Choose a %s version
install.installer.forge=Forge
install.installer.game=Game
install.installer.install=
install.installer.install=Install %s
install.installer.install_offline=Install/Upgrade from local file
install.installer.install_offline.extension=Forge/OptiFine installer
install.installer.install_offline.tooltip=Support importing Forge/OptiFine installer jar file
install.installer.install_online=Install Online
install.installer.install_online.tooltip=Support Forge, OptiFine, LiteLoader installation.
install.installer.liteloader=LiteLoader
install.installer.not_installed=%s not Installed
install.installer.optifine=OptiFine
@@ -252,7 +266,11 @@ mods.add=Add mods
mods.add.failed=Failed to add mods %s.
mods.add.success=Successfully added mods %s.
mods.choose_mod=Choose your mods
mods.enable=Enable
mods.disable=Disable
mods.name=Name
mods.remove=Remove
mods.not_modded=You should install a mod loader first (Forge, or LiteLoader)
datapack=Data packs
datapack.add=Add data pack
@@ -262,6 +280,7 @@ datapack.title=World %s - Datapacks
datapack.remove=Remove
world=Worlds/Datapacks
world.add=Add world(.zip)
world.datapack=Manage data packs
world.datapack.1_13=Only Minecraft 1.13 and later versions support data packs.
world.description=%s. Last played time: %s. Game version: %s.
@@ -278,6 +297,7 @@ world.import.invalid=Not a valid world zip
world.name=World Name
world.name.enter=Enter the world name
world.reveal=Reveal in Explorer
world.show_all=Show all
world.time=EEE, MMM d, yyyy HH:mm:ss
profile=Game Directories
@@ -364,7 +384,12 @@ settings.type.global.manage=Global Game Settings
settings.type.global.edit=Configure global game settings
settings.type.special.enable=Enable specialized settings for this game
sponsor.bmclapi=To obtain a stable download service, please consider support BMCLAPI. Click here for more information
sponsor.hmcl=HMCL is a free, open source Minecraft launcher the enables users to easily manage multiple, separate installations of Minecraft. We're using Afdian so that we can continue to pay for our hosting and project development. Click here for more information.
update=Update
update.accept=Update
update.changelog=Changelog
update.channel.dev=Update to development version
update.channel.stable=Update to stable version
update.checking=Checking for updates
@@ -17,7 +17,7 @@
#
about.copyright=版權
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.copyright.statement=Copyright (c) 2019 huangyuhui.
about.author=作者
about.author.statement=huanghongxun ([email protected])
about.thanks_to=鳴謝
@@ -65,6 +65,7 @@ account.username=使用者名稱
archive.author=作者
archive.game_version=遊戲版本
archive.name=名稱
archive.version=版本
assets.download=下載資源
@@ -89,11 +90,12 @@ crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟體崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
download.code.404=遠程伺服器不包含需要下載的文件
download.failed=下載失敗
download.failed.empty=沒有可供安裝的版本,點擊此處返回。
download.failed.refresh=載入版本列表失敗,點擊此處重試。
download.provider.bmclapi=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.provider.mojang=官方伺服器(Mojang)
download.provider.mojang=官方伺服器(Forge 和 OptiFine 自動安裝的下載來源是 BMCLAPI)
extension.bat=Windows 腳本
extension.mod=模組檔案
@@ -122,12 +124,24 @@ input.not_empty=必填
input.url=必須為有效連結
install=新增遊戲
install.change_version=更換版本
install.change_version.confirm=你確定要將 %s 從 %s 更新到 %s 嗎?
install.failed=安裝失敗
install.failed.downloading=安裝失敗,部分文件未能完成下載
install.failed.downloading.detail=未能下載檔案:%s
install.failed.downloading.timeout=下載超時:%s
install.failed.install_online=無法識別要安裝的軟體
install.failed.optifine_conflict=暫不支持 OptiFine 與 Forge 同時安裝在 Minecraft 1.13 上
install.failed.version_mismatch=該軟體需要的遊戲版本為 %s,但實際的遊戲版本為 %s。
install.installer.choose=選擇 %s 版本
install.installer.forge=Forge
install.installer.game=遊戲
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=支持安裝 Forge、OptiFine、LiteLoader
install.installer.liteloader=LiteLoader
install.installer.not_installed=暫時不安裝 %s,可以點擊此處安裝
install.installer.optifine=OptiFine
@@ -251,7 +265,10 @@ mods.add=新增模組
mods.add.failed=新增模組 %s 失敗。
mods.add.success=成功新增模組 %s。
mods.choose_mod=選擇模組
mods.enable=啟用
mods.disable=禁用
mods.remove=刪除
mods.not_modded=你需要先在自動安裝頁面安裝 Forge 或 LiteLoader 才能進行模組管理。
datapack=資料包
datapack.add=添加資料包
@@ -261,6 +278,7 @@ datapack.title=世界 %s - 資料包
datapack.remove=刪除
world=世界/資料包
world.add=添加世界
world.datapack=管理資料包
world.datapack.1_13=僅 Minecraft 1.13 及之後的版本支持資料包
world.description=%s. 上一次遊戲時間: %s. 遊戲版本: %s
@@ -277,6 +295,7 @@ world.game_version=遊戲版本
world.name=世界名稱
world.name.enter=輸入世界名稱
world.reveal=打開資料夾
world.show_all=顯示全部
world.time=yyyy 年 MM 月 dd 日 HH:mm:ss
profile=遊戲目錄
@@ -363,7 +382,12 @@ settings.type.global.manage=全域遊戲設定
settings.type.global.edit=編輯全域遊戲設定
settings.type.special.enable=啟用遊戲特別設定(不影響其他遊戲版本)
sponsor.bmclapi=請贊助 BMCLAPI 從而獲得穩定高速的下載服務,點擊此處查閱詳細訊息。
sponsor.hmcl=HMCL 是一個免費、開源的 Minecraft 啟動器,允許玩家方便快捷地安裝、管理、運行遊戲。您的贊助將幫助 HMCL 獲得更好的發展、支持穩定高速的遊戲安裝與文件下載服務。點擊此處查閱更多詳細訊息。
update=啟動器更新
update.accept=更新
update.changelog=更新日誌
update.channel.dev=更新到開發版
update.channel.stable=更新到推薦版本
update.checking=正在檢查更新
@@ -17,7 +17,7 @@
#
about.copyright=版权
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.copyright.statement=Copyright (c) 2019 huangyuhui.
about.author=作者
about.author.statement=huanghongxun ([email protected])
about.thanks_to=鸣谢
@@ -65,6 +65,7 @@ account.username=用户名
archive.author=作者
archive.game_version=游戏版本
archive.name=名称
archive.version=版本
assets.download=下载资源
@@ -89,11 +90,12 @@ crash.NoClassDefFound=请确认 HMCL 本体是否完整,或更新您的 Java
crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩溃,请检查您的 Java 环境或您的电脑!可以尝试重新安装 Java。
download=下载
download.code.404=远程服务器不包含需要下载的文件
download.failed=下载失败
download.failed.empty=没有可供安装的版本,点击此处返回。
download.failed.refresh=加载版本列表失败,点击此处重试。
download.provider.bmclapi=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.provider.mojang=官方(Mojang)
download.provider.mojang=官方(Forge 和 OptiFine 自动安装使用 BMCLAPI 下载源)
extension.bat=Windows 脚本
extension.mod=模组文件
@@ -122,12 +124,24 @@ input.not_empty=必填项
input.url=必须是合法的链接
install=添加游戏
install.change_version=更换版本
install.change_version.confirm=你确定要将 %s 从 %s 更新到 %s 吗?
install.failed=安装失败
install.failed.downloading=安装失败,部分文件未能完成下载
install.failed.downloading.detail=未能下载文件:%s
install.failed.downloading.timeout=下载超时:%s
install.failed.install_online=无法识别要安装的软件
install.failed.optifine_conflict=暂不支持 OptiFine 与 Forge 同时安装在 Minecraft 1.13 上
install.failed.version_mismatch=该软件需要的游戏版本为 %s,但实际的游戏版本为 %s。
install.installer.choose=选择 %s 版本
install.installer.forge=Forge
install.installer.game=游戏
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=支持安装 Forge、OptiFine、LiteLoader
install.installer.liteloader=LiteLoader
install.installer.not_installed=暂不安装 %s,可以点击此处安装
install.installer.optifine=OptiFine
@@ -251,7 +265,10 @@ mods.add=添加模组
mods.add.failed=添加模组 %s 失败。
mods.add.success=成功添加模组 %s。
mods.choose_mod=选择模组
mods.enable=启用
mods.disable=禁用
mods.remove=删除
mods.not_modded=你需要先在自动安装页面安装 Forge 或 LiteLoader 才能进行模组管理。
datapack=数据包
datapack.add=添加数据包
@@ -261,6 +278,7 @@ datapack.title=世界 %s - 数据包
datapack.remove=删除
world=世界/数据包
world.add=添加世界
world.datapack=管理数据包
world.datapack.1_13=仅 Minecraft 1.13 及之后的版本支持数据包
world.description=%s. 上一次游戏时间: %s. 游戏版本: %s
@@ -277,6 +295,7 @@ world.import.invalid=无法识别的存档压缩包
world.name=世界名称
world.name.enter=输入世界名称
world.reveal=打开文件夹
world.show_all=显示全部
world.time=yyyy 年 MM 月 dd 日 HH:mm:ss
profile=游戏目录
@@ -363,7 +382,12 @@ settings.type.global.manage=全局游戏设置
settings.type.global.edit=编辑全局版本设置
settings.type.special.enable=启用游戏特定设置(不影响其他游戏版本)
sponsor.bmclapi=请赞助 BMCLAPI 从而获得稳定高速的下载服务,点击此处查阅详细信息。
sponsor.hmcl=HMCL 是一个免费、开源的 Minecraft 启动器,允许玩家方便快捷地安装、管理、运行游戏。您的赞助将帮助 HMCL 获得更好的发展、支持稳定高速的游戏安装与文件下载服务。点击此处查阅更多详细信息。
update=启动器更新
update.accept=更新
update.changelog=更新日志
update.channel.dev=更新到开发版
update.channel.stable=更新到推荐版本
update.checking=正在检查更新
@@ -115,7 +115,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
"-Dauthlibinjector.side=client",
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
"-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
}
@Override
@@ -23,8 +23,11 @@ import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -70,7 +73,9 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
@SuppressWarnings("unchecked")
Map<String, String> properties = it;
GameProfile selected = session.getSelectedProfile();
server.getYggdrasilService().getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository = server.getYggdrasilService().getProfileRepository();
profileRepository.put(selected.getId(), new CompleteGameProfile(selected, properties));
profileRepository.invalidate(selected.getId());
});
return new AuthlibInjectorAccount(server, downloader, username, session);
@@ -127,7 +127,7 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
}
}
private class AuthlibInjectorVersionInfo {
private static class AuthlibInjectorVersionInfo {
@SerializedName("build_number")
public int buildNumber;
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.util.Immutable;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
@@ -36,10 +37,12 @@ public final class Texture {
this.metadata = metadata;
}
@Nullable
public String getUrl() {
return url;
}
@Nullable
public Map<String, String> getMetadata() {
return metadata;
}
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
import java.util.Map;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.Validation;
@@ -36,13 +37,14 @@ public final class User implements Validation {
private final String id;
@Nullable
@JsonAdapter(PropertyMapSerializer.class)
private final Map<String, String> properties;
public User(String id) {
this(id, null);
}
public User(String id, Map<String, String> properties) {
public User(String id, @Nullable Map<String, String> properties) {
this.id = id;
this.properties = properties;
}
@@ -51,6 +53,7 @@ public final class User implements Validation {
return id;
}
@Nullable
public Map<String, String> getProperties() {
return properties;
}
@@ -34,6 +34,8 @@ import org.jackhuang.hmcl.auth.NoCharacterException;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import javafx.beans.binding.ObjectBinding;
public class YggdrasilAccount extends Account {
private final YggdrasilService service;
@@ -48,6 +50,8 @@ public class YggdrasilAccount extends Account {
this.username = requireNonNull(username);
this.characterUUID = requireNonNull(session.getSelectedProfile().getId());
this.session = requireNonNull(session);
addProfilePropertiesListener();
}
protected YggdrasilAccount(YggdrasilService service, String username, String password, CharacterSelector selector) throws AuthenticationException {
@@ -73,6 +77,17 @@ public class YggdrasilAccount extends Account {
characterUUID = session.getSelectedProfile().getId();
authenticated = true;
addProfilePropertiesListener();
}
private ObjectBinding<Optional<CompleteGameProfile>> profilePropertiesBinding;
private void addProfilePropertiesListener() {
// binding() is thread-safe
// hold the binding so that it won't be garbage-collected
profilePropertiesBinding = service.getProfileRepository().binding(characterUUID, true);
// and it's safe to add a listener to an ObjectBinding which does not have any listener attached before (maybe tricky)
profilePropertiesBinding.addListener((a, b, c) -> this.invalidate());
}
@Override
@@ -20,9 +20,11 @@ package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -63,7 +65,9 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
@SuppressWarnings("unchecked")
Map<String, String> properties = it;
GameProfile selected = session.getSelectedProfile();
service.getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository = service.getProfileRepository();
profileRepository.put(selected.getId(), new CompleteGameProfile(selected, properties));
profileRepository.invalidate(selected.getId());
});
return new YggdrasilAccount(service, username, session);
@@ -165,7 +165,13 @@ public class YggdrasilService {
String encodedTextures = profile.getProperties().get("textures");
if (encodedTextures != null) {
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures), UTF_8), TextureResponse.class);
byte[] decodedBinary;
try {
decodedBinary = Base64.getDecoder().decode(encodedTextures);
} catch (IllegalArgumentException e) {
throw new ServerResponseMalformedException(e);
}
TextureResponse texturePayload = fromJson(new String(decodedBinary, UTF_8), TextureResponse.class);
return Optional.ofNullable(texturePayload.textures);
} else {
return Optional.empty();
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more