Compare commits

...
Author SHA1 Message Date
huanghongxun 8cdd8b3f70 Fix not loading authlib-injector server name 2019-11-12 18:10:09 +08:00
huanghongxun 956b4e4a6a Add fileApi option in modpackInfoPage 2019-11-12 17:49:26 +08:00
huanghongxun 4e0ffc8d1e Support Server Auto-Update Task. Closes #624 2019-11-11 17:35:03 +08:00
huanghongxun 177935241f Support download modpack from direct download url 2019-11-11 17:34:59 +08:00
huanghongxun f133fcf66c Fix NPE 2019-11-11 17:34:58 +08:00
huanghongxun 869f57cbf1 avoiding crash when recorded AI server url is malformed 2019-11-11 17:34:58 +08:00
huanghongxun de7c2e4f07 Fix typo 2019-11-11 17:34:58 +08:00
Yuhui Huang 8bc2650f7b Merge pull request #625 from poopingpenis/javafx
Updated Spanish translation
2019-11-08 10:38:00 +08:00
Yuhui Huang cef9f0b2bb Merge pull request #622 from Lenni0451/javafx
Added autoscroll and fixed typo
2019-11-08 10:36:57 +08:00
poopingpenis e36b28c957 Add files via upload 2019-11-01 14:40:35 -04:00
poopingpenis a770a7ee5c Add files via upload 2019-11-01 14:38:53 -04:00
poopingpenis e3e9a19f6f Add files via upload 2019-11-01 14:37:07 -04:00
poopingpenis 4772a144ae Add files via upload 2019-10-31 20:27:36 -04:00
poopingpenis d32ba75d5f Add files via upload 2019-10-31 09:03:36 -04:00
poopingpenis 96f869278b Add files via upload 2019-10-31 09:00:23 -04:00
poopingpenis df1f9c76ad Add files via upload 2019-10-31 08:58:22 -04:00
gitfamhub 0b934bf6b5 Merge branch 'javafx' into javafx 2019-10-30 22:35:55 -04:00
gitfamhub f5319fa94c Add files via upload 2019-10-30 22:32:08 -04:00
gitfamhub 28229471f4 Add files via upload 2019-10-30 22:28:46 -04:00
gitfamhub d00c61b89f Add files via upload 2019-10-30 22:07:16 -04:00
Lenni0451 1f7d3591b9 Revert "Increased window size by 20 to prevent sideways scrollbars to show up"
This reverts commit 4e537de894.
2019-10-28 20:36:59 +01:00
Lenni0451 4e537de894 Increased window size by 20 to prevent sideways scrollbars to show up 2019-10-26 22:49:26 +02:00
Lenni0451 e130a14ebe Added autoscroll checkbox to log window 2019-10-26 22:05:45 +02:00
Lenni0451 31aa2f3a27 Fixed typo and added autoscroll translation 2019-10-26 22:05:21 +02:00
Yuhui Huang 1f9c94dbc4 Merge pull request #616 from machinesmith42/javafx
Update I18N.properties
2019-10-10 16:56:14 +08:00
huanghongxun a2540c2e9b Fix #619 2019-10-10 16:51:01 +08:00
huanghongxun 4737240c91 Add: forgesvc for Curse Mod Detection fallback 2019-10-10 16:46:32 +08:00
huanghongxun 944d88b8af Fix: keep not completed Curse modpack game version 2019-10-10 16:39:27 +08:00
huanghongxun 529c4ed420 Fix: ConcurrentModificationException 2019-10-10 16:03:25 +08:00
huanghongxun e0ebb02530 Fix: java.nio.file.InvalidPathException 2019-10-10 15:53:47 +08:00
huanghongxun be22d55cec Fix: java.lang.IllegalArgumentException: MALFORMED[1] 2019-10-10 15:24:37 +08:00
huanghongxun 993ce660aa Fix: not recognizing java 12 2019-09-29 18:46:03 +08:00
machinesmith42 79e120dd74 Update I18N.properties 2019-09-27 10:51:55 -05:00
huanghongxun d4155bc14d Update Russian and English translations 2019-09-13 13:32:20 +08:00
huanghongxun ba699c4ed5 Fix NPE 2019-09-12 16:38:28 +08:00
huanghongxun bc96afd0cd Fix #605 2019-09-12 15:51:20 +08:00
huanghongxun cf1a66e858 load authlib-injector server from external file 2019-09-09 17:39:49 +08:00
huanghongxun 5eba896e6d Fix forge installer 404 2019-09-09 16:49:36 +08:00
huanghongxun e69d149c34 Fix HMCL import 2019-09-09 15:32:55 +08:00
huanghongxun 2f9955febd No longer report JsonParseException 2019-09-05 19:06:09 +08:00
huanghongxun df39be708f Check if given game version is malformed 2019-09-05 17:49:59 +08:00
huanghongxun ce3b9e3b1f FIx: unable to download Forge installer 2019-09-05 15:49:27 +08:00
huanghongxun a9feb65fc8 Try to fix ConcurrentModificationException 2019-08-25 15:35:17 +08:00
huanghongxun 93837a28fa No longer report imageio exception 2019-08-24 12:06:54 +08:00
huanghongxun 678f3b8eb7 Try to fix NPE 2019-08-24 12:01:04 +08:00
huanghongxun 0833fa9e7d Fix crash when typing wrong path 2019-08-24 11:59:55 +08:00
huanghongxun 369b9bb6cf Fix #603 2019-08-24 11:57:43 +08:00
huanghongxun c9b6f2f62f Collect uncaught RuntimeException thrown in task execution 2019-08-24 11:49:53 +08:00
huanghongxun e2787af044 no longer force using javaw 2019-08-20 00:54:12 +08:00
huanghongxun 30c27bbb67 MultiMC fabric and local libraries 2019-08-20 00:53:59 +08:00
huanghongxun 7aee7e33d0 Making versions whose library being removed pure patched 2019-08-20 00:53:36 +08:00
huanghongxun a26061c388 alter order if game item context menu 2019-08-19 16:08:48 +08:00
huanghongxun 11e66ac13c Support changing game version 2019-08-19 16:05:40 +08:00
huanghongxun a2ac3c1f18 Hide generated resolved patches in InstallerListPage 2019-08-19 15:39:30 +08:00
huanghongxun e7142e4032 Fix displaying "null" in GameItem 2019-08-19 14:57:42 +08:00
huanghongxun 0dd9dc6c17 Now pressing enter when at main page will launch the game 2019-08-19 00:49:48 +08:00
huanghongxun a53bfed060 Add primary and secondary click action on GameListItem 2019-08-19 00:38:31 +08:00
huanghongxun c5919f2719 Fix OptiFine installation 2019-08-19 00:18:34 +08:00
huanghongxun fb24516b90 Show unrecognized patches in InstallerListPage 2019-08-19 00:17:03 +08:00
huanghongxun 60fa4eb0a1 Fix LibrariesUniqueTask changing order of classpath 2019-08-19 00:16:02 +08:00
huanghongxun 536576084d Fix forge local installation 2019-08-19 00:12:28 +08:00
huanghongxun c62cc499c5 Show HMCL version in game main menu 2019-08-18 10:54:05 +08:00
huanghongxun c11bbf8836 Fix failure of auto-installing more than one library 2019-08-18 10:27:48 +08:00
huanghongxun 71c0cede24 Fixed unrecognizing Optifine jar file 2019-08-18 10:27:12 +08:00
huanghongxun ad16bbc96d Disable failure test 2019-08-17 12:13:05 +08:00
huanghongxun 520ffe4c09 Add missing license header 2019-08-17 00:45:47 +08:00
huanghongxun e3f603c145 Repackaging 2019-08-17 00:12:02 +08:00
huanghongxun 880b8cb072 Support exporting multimc modpack 2019-08-16 21:13:54 +08:00
huanghongxun 86bf3d473a Fixed #601 2019-08-16 14:46:20 +08:00
huanghongxun eb6baa8ad1 Support fabric auto-installation 2019-08-16 12:35:56 +08:00
huanghongxun d53d56d93a Renaming json as well as jar when the folder is renamed manually 2019-08-16 00:29:52 +08:00
huanghongxun d143ff48fa Show furnace icon when the game is patched 2019-08-16 00:29:26 +08:00
huanghongxun b11acb7762 Make auto-installing more flexible 2019-08-16 00:18:52 +08:00
huanghongxun 51aa0dd851 Fix non-idea-compilable errors 2019-08-15 12:54:39 +08:00
huanghongxun 861b0a1c62 Add exceptional functional interfaces 2019-08-15 12:53:52 +08:00
huanghongxun 27a33f567f add missing setCacheDirectory directives 2019-08-15 12:45:31 +08:00
huanghongxun e862a4b46b Fix #592 2019-07-28 20:16:12 +08:00
huanghongxun 2b3e8ebfc0 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2019-07-28 20:05:18 +08:00
Yuhui Huang 6118877ad6 Merge pull request #594 from asl97/patch-1
Fix #586
2019-07-27 18:47:10 +08:00
asl97 0454ff9bf7 Fix #586 2019-07-27 01:21:11 +08:00
huanghongxun 4faad50d99 Fix #567 2019-05-31 10:48:38 +08:00
Yuhui Huang 272599b448 Merge pull request #569 from gitfamhub/javafx
Spanish translation
2019-05-30 21:25:36 -05:00
huanghongxun c12d76e2fa Fixed wrong English localization translations. Mentioned in #569. 2019-05-31 10:20:11 +08:00
gitfamhub 55550f1773 Add files via upload 2019-05-30 22:13:25 -04:00
gitfamhub e8992d5ad7 Add files via upload 2019-05-30 22:12:50 -04:00
gitfamhub a8bd589b18 Add files via upload 2019-05-30 21:08:05 -04:00
gitfamhub 602cf7b110 Add files via upload 2019-05-30 21:01:30 -04:00
gitfamhub 1e7b731485 Delete I18N_es.properties 2019-05-30 20:47:23 -04:00
gitfamhub 824b8e410c Add files via upload 2019-05-30 09:23:51 -04:00
gitfamhub ba47953485 Add files via upload 2019-05-30 09:20:00 -04:00
gitfamhub c7e666261f Add files via upload 2019-05-30 07:24:29 -04:00
gitfamhub 0c4c98d2c9 Add files via upload 2019-05-30 07:21:06 -04:00
gitfamhub 0c63be37bb Add files via upload 2019-05-30 07:20:21 -04:00
gitfamhub 9623a311a0 Add files via upload 2019-05-30 07:09:23 -04:00
gitfamhub 13da61d895 Add files via upload 2019-05-30 07:06:43 -04:00
huanghongxun a7c02e2dea name task async 2019-05-13 00:05:08 +08:00
huanghongxun cfb9610afc Merge branch 'task-refactor' into javafx
# Conflicts:
#	HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java
#	HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java
#	HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java
#	HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java
#	HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java
2019-05-10 10:33:06 +08:00
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
222 changed files with 6729 additions and 2137 deletions

No files matched your search

+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,15 +17,17 @@
*/
package org.jackhuang.hmcl;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.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 +41,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;
@@ -75,6 +78,7 @@ public final class Launcher extends Application {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
TaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
try {
LOG.info("*** " + Metadata.TITLE + " ***");
@@ -97,7 +101,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 +115,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();
@@ -140,5 +144,5 @@ public final class Launcher extends Application {
return result;
}
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
@@ -46,7 +46,7 @@ public final class Main {
checkDirectoryPath();
// This environment check will take ~300ms
thread(() -> checkDSTRootCAX3(), "CA Certificate Check", true);
thread(Main::checkDSTRootCAX3, "CA Certificate Check", true);
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
@@ -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");
@@ -29,16 +29,16 @@ import java.util.Map;
*/
public final class HMCLGameLauncher extends DefaultLauncher {
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options) {
this(repository, versionId, authInfo, options, null);
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options) {
this(repository, version, authInfo, options, null);
}
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
this(repository, versionId, authInfo, options, listener, true);
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
this(repository, version, authInfo, options, listener, true);
}
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
super(repository, versionId, authInfo, options, listener, daemon);
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
super(repository, version, authInfo, options, listener, daemon);
}
@Override
@@ -31,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<>();
@@ -150,18 +152,20 @@ 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 ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else if (!version.getPatches().isEmpty() ||
version.getMainClass() != null &&
("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) {
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
@@ -24,6 +25,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -55,26 +57,16 @@ public class HMCLModpackExportTask extends Task<Void> {
@Override
public void execute() throws Exception {
ArrayList<String> blackList = new ArrayList<>(HMCLModpackManager.MODPACK_BLACK_LIST);
ArrayList<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(version + ".jar");
blackList.add(version + ".json");
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (Zipper zip = new Zipper(output.toPath())) {
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> {
if (path.isEmpty())
return true;
for (String s : blackList)
if (path.equals(s))
return false;
for (String s : whitelist)
if (path.equals(s))
return true;
return false;
});
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> Modpack.acceptFile(path, blackList, whitelist));
Version mv = repository.getResolvedVersion(version);
Version mv = repository.getResolvedPreservingPatchesVersion(version);
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + version));
.orElseThrow(() -> new IOException("Cannot parse the version of " + version));
zip.putTextFile(JsonUtils.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
zip.putTextFile(JsonUtils.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
}
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DependencyManager;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
@@ -40,12 +40,13 @@ public final class HMCLModpackInstallTask extends Task<Void> {
private final File zipFile;
private final String name;
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependency;
private final Modpack modpack;
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
DependencyManager dependency = profile.getDependency();
dependency = profile.getDependency();
repository = profile.getRepository();
this.zipFile = zipFile;
this.name = name;
@@ -89,8 +90,18 @@ public final class HMCLModpackInstallTask extends Task<Void> {
@Override
public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version version = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
dependencies.add(new VersionJsonSaveTask(repository, version));
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion);
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
// reinstall libraries
// libraries of Forge and OptiFine should be obtained by installation.
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
if (LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(mark.getLibraryId()))
continue;
libraryTask = libraryTask.thenComposeAsync(version -> dependency.installLibraryAsync(modpack.getGameVersion(), version, mark.getLibraryId(), mark.getLibraryVersion()));
}
dependencies.add(libraryTask.thenComposeAsync(repository::save));
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
}
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -27,51 +26,12 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.List;
/**
* @author huangyuhui
*/
public final class HMCLModpackManager {
public static final List<String> MODPACK_BLACK_LIST = Lang.immutableListOf(
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"pack.json", "launcher.jar", "hmclmc.log", "cache", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
"minetweaker.log", // Mods
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods
);
public static final List<String> MODPACK_SUGGESTED_BLACK_LIST = Lang.immutableListOf(
"fonts", // BetterFonts
"saves", "servers.dat", "options.txt", // Minecraft
"blueprints" /* BuildCraft */,
"optionsof.txt" /* OptiFine */,
"journeymap" /* JourneyMap */,
"optionsshaders.txt",
"mods/VoxelMods");
public static ModAdviser.ModSuggestion suggestMod(String fileName, boolean isDirectory) {
if (match(MODPACK_BLACK_LIST, fileName, isDirectory))
return ModAdviser.ModSuggestion.HIDDEN;
if (match(MODPACK_SUGGESTED_BLACK_LIST, fileName, isDirectory))
return ModAdviser.ModSuggestion.NORMAL;
else
return ModAdviser.ModSuggestion.SUGGESTED;
}
private static boolean match(List<String> l, String fileName, boolean isDirectory) {
for (String s : l)
if (isDirectory) {
if (fileName.startsWith(s + "/"))
return true;
} else if (fileName.equals(s))
return true;
return false;
}
/**
* Read the manifest in a HMCL modpack.
*
@@ -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;
@@ -26,14 +27,21 @@ import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
import org.jackhuang.hmcl.launch.PermissionException;
import org.jackhuang.hmcl.launch.ProcessCreationException;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.LogWindow;
@@ -118,30 +126,32 @@ public final class LauncherHelper {
private void launch0() {
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = MaintainTask.maintain(repository.getResolvedVersion(selectedVersion));
Version version = MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion));
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
TaskExecutor executor = Task.runAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.thenCompose(() -> {
.thenComposeAsync(() -> {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version);
})
.thenRun(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS))
.thenCompose(() -> {
.thenRunAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS))
.thenComposeAsync(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else if ("Server".equals(configuration.getType()))
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {
return null;
}
})
.thenRun(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN))
.thenSupply(i18n("account.methods"), () -> {
.thenRunAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN))
.thenSupplyAsync(i18n("account.methods"), () -> {
try {
return account.logIn();
} catch (CredentialExpiredException e) {
@@ -152,20 +162,20 @@ public final class LauncherHelper {
return account.playOffline().orElseThrow(() -> e);
}
})
.thenApply(Schedulers.javafx(), authInfo -> {
.thenApplyAsync(Schedulers.javafx(), authInfo -> {
emitStatus(LoadingState.LAUNCHING);
return authInfo;
})
.thenApply(authInfo -> new HMCLGameLauncher(
.thenApplyAsync(authInfo -> new HMCLGameLauncher(
repository,
selectedVersion,
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
authInfo,
setting.toLaunchOptions(profile.getGameDir()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(authInfo, gameVersion.isPresent())
))
.thenCompose(launcher -> { // launcher is prev task's result
.thenComposeAsync(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
} else {
@@ -175,7 +185,7 @@ public final class LauncherHelper {
}).setName(i18n("version.launch_script"));
}
})
.thenAccept(process -> { // process is LaunchTask's result
.thenAcceptAsync(process -> { // process is LaunchTask's result
if (scriptFile == null) {
PROCESSES.add(process);
if (launcherVisibility == LauncherVisibility.CLOSE)
@@ -290,7 +300,7 @@ public final class LauncherHelper {
}
// LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader.
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9_AND_LATER
&& version.getMainClass().contains("launchwrapper")
&& version.getLibraries().stream()
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
@@ -398,7 +408,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
@@ -20,6 +20,13 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.curse.CurseManifest;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
@@ -60,6 +67,12 @@ public final class ModpackHelper {
// ignore it, not a valid MultiMC modpack.
}
try {
return ServerModpackManifest.readManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid Server modpack.
}
throw new UnsupportedModpackException(file.toString());
}
@@ -102,9 +115,6 @@ public final class ModpackHelper {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
} else {
HMCLGameRepository repository = profile.getRepository();
repository.removeVersionFromDisk(name);
}
};
@@ -117,8 +127,11 @@ public final class ModpackHelper {
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenCompose(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
.thenComposeAsync(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else if (modpack.getManifest() instanceof ServerModpackManifest)
return new ServerModpackInstallTask(profile.getDependency(), zipFile, modpack, ((ServerModpackManifest) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
}
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
@@ -17,13 +17,21 @@
*/
package org.jackhuang.hmcl.game;
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;
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;
@@ -40,24 +48,11 @@ 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.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
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
@@ -126,6 +121,9 @@ public final class TexturesLoader {
try (InputStream in = Files.newInputStream(file)) {
img = ImageIO.read(in);
}
if (img == null)
throw new IOException("Texture is malformed");
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
metadata = emptyMap();
@@ -140,8 +138,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);
@@ -0,0 +1,78 @@
/*
* 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.setting;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class AuthlibInjectorServers implements Validation {
public static final String CONFIG_FILENAME = "authlib-injectors.json";
private final List<String> urls;
public AuthlibInjectorServers(List<String> urls) {
this.urls = urls;
}
public List<String> getUrls() {
return urls;
}
@Override
public void validate() throws JsonParseException {
if (urls == null)
throw new JsonParseException("authlib-injectors.json -> urls cannot be null");
}
private static final Path configLocation = Paths.get(CONFIG_FILENAME);
private static AuthlibInjectorServers configInstance;
public synchronized static void init() {
if (configInstance != null) {
throw new IllegalStateException("AuthlibInjectorServers is already loaded");
}
configInstance = new AuthlibInjectorServers(Collections.emptyList());
if (Files.exists(configLocation)) {
try {
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Malformed authlib-injectors.json", e);
}
}
}
public static AuthlibInjectorServers getConfigInstance() {
return configInstance;
}
}
@@ -52,6 +52,10 @@ public final class ConfigHolder {
return configInstance;
}
public static boolean isNewlyCreated() {
return newlyCreated;
}
public synchronized static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.jfoenix.utils.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -32,7 +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.ToStringBuilder;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.io.File;
@@ -40,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;
/**
*
@@ -144,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)) {
@@ -226,7 +226,7 @@ public final class Profile implements Observable {
@Override
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
JsonObject obj = (JsonObject) json;
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.setting;
import com.jfoenix.utils.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());
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -44,6 +45,15 @@ public class Settings {
}
private Settings() {
AuthlibInjectorServers.init();
if (ConfigHolder.isNewlyCreated() && !AuthlibInjectorServers.getConfigInstance().getUrls().isEmpty()) {
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
AuthlibInjectorServers.getConfigInstance().getUrls().stream()
.map(AuthlibInjectorServer::new)
.forEach(config().getAuthlibInjectorServers()::add);
}
DownloadProviders.init();
ProxyManager.init();
Accounts.init();
@@ -25,6 +25,7 @@ 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;
@@ -81,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();
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
@@ -482,7 +483,7 @@ public final class VersionSetting {
else if (isUsesCustomJavaDir()) {
try {
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
} catch (IOException e) {
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
@@ -537,6 +538,7 @@ public final class VersionSetting {
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setMinecraftArgs(getMinecraftArgs())
.setJavaArgs(getJavaArgs())
@@ -595,7 +597,7 @@ public final class VersionSetting {
@Override
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject))
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject))
return null;
JsonObject obj = (JsonObject) json;
@@ -17,10 +17,8 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.utils.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,8 +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;
@@ -58,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 {
@@ -174,7 +177,7 @@ public final class Controllers {
return node;
})
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (profile == Profiles.getSelectedProfile())
mainPage.getVersions().setAll(children);
});
@@ -210,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);
}
@@ -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,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.utils.JFXUtilities;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
@@ -29,13 +28,15 @@ 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 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();
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.utils.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,7 +45,8 @@ 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;
@@ -75,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 = "
@@ -154,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) {
@@ -252,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");
@@ -462,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);
}
@@ -17,10 +17,15 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
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 +35,47 @@ 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);
if (version != null) {
TwoLineListItem item = new TwoLineListItem();
item.setTitle(artifact);
item.setSubtitle(i18n("archive.version") + ": " + version);
setCenter(item);
} else {
Label label = new Label(artifact);
label.setStyle("-fx-font-size: 15px;");
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
setCenter(label);
}
{
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,10 +17,7 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.utils.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;
@@ -30,7 +27,6 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AddAccountPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
@@ -41,8 +37,9 @@ import org.jackhuang.hmcl.ui.versions.Versions;
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 {
@@ -70,12 +67,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()));
@@ -111,7 +108,7 @@ public final class LeftPaneController extends AdvancedListBox {
private boolean checkedModpack = false;
private void onRefreshedVersions(HMCLGameRepository repository) {
JFXUtilities.runInFX(() -> {
runInFX(() -> {
if (!checkedModpack) {
checkedModpack = true;
@@ -119,12 +116,11 @@ public final class LeftPaneController extends AdvancedListBox {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
.thenApply(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenAccept(modpack -> {
AtomicReference<Region> region = new AtomicReference<>();
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.withRun(Schedulers.javafx(), this::checkAccount).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.installing")));
.thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenApplyAsync(modpack -> ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.withRunAsync(Schedulers.javafx(), this::checkAccount).executor())
.thenAcceptAsync(Schedulers.javafx(), executor -> {
Controllers.taskDialog(executor, i18n("modpack.installing"));
executor.start();
}).start();
}
@@ -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 <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.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;
}
}
@@ -39,6 +39,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
Pane placeholder = new Pane();
VBox list = new VBox();
StackPane contentPane = new StackPane();
{
@@ -46,10 +47,8 @@ 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);
@@ -87,8 +86,13 @@ 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);
}
});
}
@@ -17,15 +17,16 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
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 +36,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 +46,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 +68,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) {
@@ -121,7 +124,8 @@ public final class LogWindow extends Stage {
pre.setTextContent(line);
div.appendChild(pre);
impl.body.appendChild(div);
impl.engine.executeScript("checkNewLog(\"" + level.name().toLowerCase() + "\");scrollToBottom();");
//impl.engine.executeScript("checkNewLog(\"" + level.name().toLowerCase() + "\");scrollToBottom();");
impl.engine.executeScript("checkNewLog(\"" + level.name().toLowerCase() + "\");" + (impl.autoscroll.isSelected() ? "scrollToBottom();" : ""));
switch (level) {
case FATAL:
@@ -165,6 +169,8 @@ public final class LogWindow extends Stage {
private ToggleButton btnDebugs;
@FXML
private ComboBox<String> cboLines;
@FXML
private CheckBox autoscroll;
final WebEngine engine;
Node body;
@@ -174,7 +180,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) {
@@ -23,7 +23,12 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
@@ -113,7 +118,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
launchButton.setPrefHeight(55);
launchButton.setButtonType(JFXButton.ButtonType.RAISED);
launchButton.getStyleClass().add("jfx-button-raised");
launchButton.setOnMouseClicked(e -> launch());
launchButton.setOnAction(e -> launch());
launchButton.setDefaultButton(true);
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
{
VBox graphic = new VBox();
@@ -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");
@@ -50,7 +50,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
import java.util.logging.Level;
@@ -118,7 +117,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
selectedItemPropertyFor(proxyConfigurationGroup, Proxy.Type.class).bindBidirectional(config().proxyTypeProperty());
// ====
fileCommonLocation.loadChildren(Arrays.asList(
fileCommonLocation.loadChildren(Collections.singletonList(
fileCommonLocation.createChildren(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT)
), EnumCommonDirectory.CUSTOM);
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
@@ -19,6 +19,7 @@ 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;
@@ -72,7 +73,7 @@ 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();
{
@@ -80,12 +81,31 @@ public abstract class SettingsView extends StackPane {
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.prefWidthProperty().bind(sponsorPane.widthProperty());
sponsorPane.getChildren().add(label);
label.setTextAlignment(TextAlignment.JUSTIFY);
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
sponsorPane.getChildren().setAll(gridPane);
settingsPane.getContent().add(sponsorPane);
}
@@ -0,0 +1,96 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import 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 <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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));
@@ -17,11 +17,9 @@
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
@@ -30,17 +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) {
@@ -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,11 +96,11 @@ 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);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
getChildren().setAll(root);
@@ -17,9 +17,10 @@
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.utils.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;
@@ -53,6 +54,7 @@ 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;
@@ -75,7 +77,7 @@ public class AddAccountPane extends StackPane {
@FXML private SpinnerPane acceptPane;
@FXML private HBox linksContainer;
private ListProperty<Hyperlink> links = new SimpleListProperty<>();;
private ListProperty<Hyperlink> links = new SimpleListProperty<>();
public AddAccountPane() {
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
@@ -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()));
}
}
}
@@ -21,35 +21,23 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
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;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
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 +45,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());
}
@@ -26,11 +26,12 @@ 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;
@@ -49,6 +50,8 @@ public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
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);
@@ -72,9 +75,7 @@ public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
}
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<Integer, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
selectedStateCallback = new SimpleObjectProperty<>(this, "selectedStateCallback");
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
@@ -101,7 +102,8 @@ public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
if (showLabel) {
setText(c.toString(item));
}
setGraphic(checkBox);
setGraphic(pane);
if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
@@ -32,7 +32,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends StackPane {
public static enum MessageType {
public enum MessageType {
ERROR,
INFORMATION,
WARNING,
@@ -71,16 +71,17 @@ public class RipplerContainer extends StackPane {
setContainer(container);
getStyleClass().add(DEFAULT_STYLE_CLASS);
buttonRippler.setPosition(JFXRippler.RipplerPos.BACK);
buttonContainer.getChildren().add(buttonRippler);
focusedProperty().addListener((a, b, newValue) -> {
if (newValue) {
if (!isPressed())
buttonRippler.setOverlayVisible(true);
buttonRippler.showOverlay();
} else {
buttonRippler.setOverlayVisible(false);
buttonRippler.hideOverlay();
}
});
pressedProperty().addListener(o -> buttonRippler.setOverlayVisible(false));
pressedProperty().addListener(o -> buttonRippler.hideOverlay());
setPickOnBounds(false);
buttonContainer.setPickOnBounds(false);
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.utils.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));
}
}
@@ -26,11 +26,17 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
@@ -83,6 +89,8 @@ public final class TaskListPane extends StackPane {
if (task instanceof GameAssetDownloadTask) {
task.setName(i18n("assets.download_all"));
} else if (task instanceof GameInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
} else if (task instanceof ForgeInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
} else if (task instanceof LiteLoaderInstallTask) {
@@ -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
@@ -123,16 +123,13 @@ public class DecoratorSkin extends SkinBase<Decorator> {
{
StackPane container = new StackPane();
Bindings.bindContent(container.getChildren(), skinnable.containerProperty());
ListChangeListener<Node> listener = new ListChangeListener<Node>() {
@Override
public void onChanged(Change<? extends Node> c) {
if (skinnable.getContainer().isEmpty()) {
container.setMouseTransparent(true);
container.setVisible(false);
} else {
container.setMouseTransparent(false);
container.setVisible(true);
}
ListChangeListener<Node> listener = c -> {
if (skinnable.getContainer().isEmpty()) {
container.setMouseTransparent(true);
container.setVisible(false);
} else {
container.setMouseTransparent(false);
container.setVisible(true);
}
};
skinnable.containerProperty().addListener(listener);
@@ -23,6 +23,7 @@ import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -33,6 +34,7 @@ import org.jackhuang.hmcl.util.Lang;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class AdditionalInstallersPage extends StackPane implements WizardPage {
@@ -42,6 +44,8 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
@FXML
private VBox list;
@FXML
private JFXButton btnFabric;
@FXML
private JFXButton btnForge;
@FXML
private JFXButton btnLiteLoader;
@@ -52,6 +56,8 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
@FXML
private Label lblVersionName;
@FXML
private Label lblFabric;
@FXML
private Label lblForge;
@FXML
private Label lblLiteLoader;
@@ -69,26 +75,17 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
lblGameVersion.setText(provider.getGameVersion());
lblVersionName.setText(provider.getVersion().getId());
btnForge.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 0);
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.forge")), provider.getGameVersion(), downloadProvider, "forge", () -> {
controller.onPrev(false);
}));
});
JFXButton[] buttons = new JFXButton[]{btnFabric, btnForge, btnLiteLoader, btnOptiFine};
String[] libraryIds = new String[]{"fabric", "forge", "liteloader", "optifine"};
btnLiteLoader.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 1);
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.liteloader")), provider.getGameVersion(), downloadProvider, "liteloader", () -> {
controller.onPrev(false);
}));
});
btnOptiFine.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 2);
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.optifine")), provider.getGameVersion(), downloadProvider, "optifine", () -> {
controller.onPrev(false);
}));
});
for (int i = 0; i < libraryIds.length; ++i) {
String libraryId = libraryIds[i];
buttons[i].setOnMouseClicked(e -> {
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), provider.getGameVersion(), downloadProvider, libraryId, () -> {
controller.onPrev(false);
}));
});
}
btnInstall.setOnMouseClicked(e -> onInstall());
}
@@ -109,30 +106,27 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
@Override
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText(i18n("install.new_game.current_game_version") + ": " + provider.getGameVersion());
btnForge.setDisable(provider.getForge() != null);
if (provider.getForge() != null || controller.getSettings().containsKey("forge"))
lblForge.setText(i18n("install.installer.version", i18n("install.installer.forge")) + ": " + Lang.nonNull(provider.getForge(), getVersion("forge")));
else
lblForge.setText(i18n("install.installer.not_installed", i18n("install.installer.forge")));
btnLiteLoader.setDisable(provider.getLiteLoader() != null);
if (provider.getLiteLoader() != null || controller.getSettings().containsKey("liteloader"))
lblLiteLoader.setText(i18n("install.installer.version", i18n("install.installer.liteloader")) + ": " + Lang.nonNull(provider.getLiteLoader(), getVersion("liteloader")));
else
lblLiteLoader.setText(i18n("install.installer.not_installed", i18n("install.installer.liteloader")));
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(provider.getVersion().resolvePreservingPatches(provider.getProfile().getRepository()));
String fabric = analyzer.getVersion(FABRIC).orElse(null);
String forge = analyzer.getVersion(FORGE).orElse(null);
String liteLoader = analyzer.getVersion(LITELOADER).orElse(null);
String optiFine = analyzer.getVersion(OPTIFINE).orElse(null);
btnOptiFine.setDisable(provider.getOptiFine() != null);
if (provider.getOptiFine() != null || controller.getSettings().containsKey("optifine"))
lblOptiFine.setText(i18n("install.installer.version", i18n("install.installer.optifine")) + ": " + Lang.nonNull(provider.getOptiFine(), getVersion("optifine")));
else
lblOptiFine.setText(i18n("install.installer.not_installed", i18n("install.installer.optifine")));
Label[] labels = new Label[]{lblFabric, lblForge, lblLiteLoader, lblOptiFine};
String[] libraryIds = new String[]{"fabric", "forge", "liteloader", "optifine"};
String[] versions = new String[]{fabric, forge, liteLoader, optiFine};
for (int i = 0; i < libraryIds.length; ++i) {
String libraryId = libraryIds[i];
if (versions[i] != null || controller.getSettings().containsKey(libraryId))
labels[i].setText(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + Lang.nonNull(getVersion(libraryId), versions[i]));
else
labels[i].setText(i18n("install.installer.not_installed", i18n("install.installer." + libraryId)));
}
}
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(INSTALLER_TYPE);
}
public static final String INSTALLER_TYPE = "INSTALLER_TYPE";
}
@@ -18,11 +18,13 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
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.fabric.FabricInstallTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.DownloadException;
@@ -32,7 +34,10 @@ 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.util.i18n.I18n.i18n;
@@ -41,19 +46,11 @@ public final class InstallerWizardProvider implements WizardProvider {
private final Profile profile;
private final String gameVersion;
private final Version version;
private final String forge;
private final String liteLoader;
private final String optiFine;
public InstallerWizardProvider(Profile profile, String gameVersion, Version version) {
this.profile = profile;
this.gameVersion = gameVersion;
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);
}
public Profile getProfile() {
@@ -68,18 +65,6 @@ public final class InstallerWizardProvider implements WizardProvider {
return version;
}
public String getForge() {
return forge;
}
public String getLiteLoader() {
return liteLoader;
}
public String getOptiFine() {
return optiFine;
}
@Override
public void start(Map<String, Object> settings) {
}
@@ -91,16 +76,12 @@ public final class InstallerWizardProvider implements WizardProvider {
Task<Version> ret = Task.supplyAsync(() -> version);
if (settings.containsKey("forge"))
ret = ret.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("forge")));
for (Object value : settings.values()) {
if (value instanceof RemoteVersion)
ret = ret.thenComposeAsync(profile.getDependency().installLibraryAsync((RemoteVersion) value));
}
if (settings.containsKey("liteloader"))
ret = ret.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("liteloader")));
if (settings.containsKey("optifine"))
ret = ret.thenCompose(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("optifine")));
return ret.thenCompose(profile.getRepository().refreshVersionsAsync());
return ret.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
}
@Override
@@ -123,7 +104,26 @@ public final class InstallerWizardProvider implements WizardProvider {
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) {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
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 ||
exception instanceof FabricInstallTask.UnsupportedFabricInstallationException) {
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageType.ERROR, next);
} else if (exception instanceof DefaultDependencyManager.UnsupportedLibraryInstallerException) {
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);
}
@@ -30,8 +30,11 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.nio.file.Paths;
import java.util.Map;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -42,6 +45,9 @@ public class InstallersPage extends StackPane implements WizardPage {
@FXML
private VBox list;
@FXML
private JFXButton btnFabric;
@FXML
private JFXButton btnForge;
@@ -54,6 +60,9 @@ public class InstallersPage extends StackPane implements WizardPage {
@FXML
private Label lblGameVersion;
@FXML
private Label lblFabric;
@FXML
private Label lblForge;
@@ -77,24 +86,20 @@ public class InstallersPage extends StackPane implements WizardPage {
String gameVersion = ((RemoteVersion) controller.getSettings().get("game")).getGameVersion();
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
hasVersion.setMessage(i18n("install.new_game.already_exists"));
txtName.getValidators().add(hasVersion);
Validator nameValidator = new Validator(OperatingSystem::isNameValid);
nameValidator.setMessage(i18n("install.new_game.malformed"));
txtName.getValidators().addAll(hasVersion, nameValidator);
txtName.textProperty().addListener(e -> btnInstall.setDisable(!txtName.validate()));
txtName.setText(gameVersion);
btnForge.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 0);
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.forge")), gameVersion, downloadProvider, "forge", () -> controller.onPrev(false)));
});
JFXButton[] buttons = new JFXButton[]{btnFabric, btnForge, btnLiteLoader, btnOptiFine};
String[] libraryIds = new String[]{"fabric", "forge", "liteloader", "optifine"};
btnLiteLoader.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 1);
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.liteloader")), gameVersion, downloadProvider, "liteloader", () -> controller.onPrev(false)));
});
btnOptiFine.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 2);
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.optifine")), gameVersion, downloadProvider, "optifine", () -> controller.onPrev(false)));
});
for (int i = 0; i < libraryIds.length; ++i) {
String libraryId = libraryIds[i];
buttons[i].setOnMouseClicked(e ->
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false))));
}
}
@Override
@@ -109,25 +114,21 @@ public class InstallersPage extends StackPane implements WizardPage {
@Override
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText(i18n("install.new_game.current_game_version") + ": " + getVersion("game"));
if (controller.getSettings().containsKey("forge"))
lblForge.setText(i18n("install.installer.version", i18n("install.installer.forge")) + ": " + getVersion("forge"));
else
lblForge.setText(i18n("install.installer.not_installed", i18n("install.installer.forge")));
if (controller.getSettings().containsKey("liteloader"))
lblLiteLoader.setText(i18n("install.installer.version", i18n("install.installer.liteloader")) + ": " + getVersion("liteloader"));
else
lblLiteLoader.setText(i18n("install.installer.not_installed", i18n("install.installer.liteloader")));
Label[] labels = new Label[]{lblFabric, lblForge, lblLiteLoader, lblOptiFine};
String[] libraryIds = new String[]{"fabric", "forge", "liteloader", "optifine"};
if (controller.getSettings().containsKey("optifine"))
lblOptiFine.setText(i18n("install.installer.version", i18n("install.installer.optifine")) + ": " + getVersion("optifine"));
else
lblOptiFine.setText(i18n("install.installer.not_installed", i18n("install.installer.optifine")));
for (int i = 0; i < libraryIds.length; ++i) {
String libraryId = libraryIds[i];
if (controller.getSettings().containsKey(libraryId))
labels[i].setText(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + getVersion(libraryId));
else
labels[i].setText(i18n("install.installer.not_installed", i18n("install.installer." + libraryId)));
}
}
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(INSTALLER_TYPE);
}
@FXML
@@ -135,6 +136,4 @@ public class InstallersPage extends StackPane implements WizardPage {
controller.getSettings().put("name", txtName.getText());
controller.onFinish();
}
public static final String INSTALLER_TYPE = "INSTALLER_TYPE";
}
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
@@ -93,7 +93,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
return null;
} else {
return ModpackHelper.getInstallTask(profile, selected, name, modpack)
.thenRun(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
}
}
@@ -122,6 +122,8 @@ public class ModpackInstallWizardProvider implements WizardProvider {
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
switch (step) {
case 0:
return new ModpackSelectionPage(controller);
case 1:
return new ModpackPage(controller);
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
@@ -110,7 +110,7 @@ public final class ModpackPage extends StackPane implements WizardPage {
spinnerPane.showSpinner();
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
.thenApply(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
.thenApplyAsync(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
.whenComplete(Schedulers.javafx(), manifest -> {
spinnerPane.hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
@@ -151,7 +151,7 @@ public final class ModpackPage extends StackPane implements WizardPage {
if (manifest != null) {
WebStage stage = new WebStage();
stage.getWebView().getEngine().loadContent(manifest.getDescription());
stage.setTitle(i18n("modpack.wizard.step.3"));
stage.setTitle(i18n("modpack.description"));
stage.showAndWait();
}
}
@@ -0,0 +1,118 @@
/*
* 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 com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.download.ModpackPage.MODPACK_FILE;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackSelectionPage extends StackPane implements WizardPage {
private final WizardController controller;
@FXML private JFXButton btnLocal;
@FXML private JFXButton btnRemote;
public ModpackSelectionPage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/download/modpack-source.fxml");
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
if (filePath.isPresent()) {
controller.getSettings().put(MODPACK_FILE, filePath.get());
Platform.runLater(controller::onNext);
}
FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
controller.getSettings().put(MODPACK_FILE, modpack);
controller.onNext();
});
}
@FXML
private void onChooseLocalFile() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile == null) {
Platform.runLater(controller::onEnd);
return;
}
controller.getSettings().put(MODPACK_FILE, selectedFile);
controller.onNext();
}
@FXML
private void onChooseRemoteFile() {
Controllers.inputDialog(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> {
try {
URL url = new URL(urlString);
Path modpack = Files.createTempFile("forge-installer", ".jar");
resolve.run();
Controllers.taskDialog(
new FileDownloadTask(url, modpack.toFile(), null)
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
resolve.run();
controller.getSettings().put(MODPACK_FILE, modpack.toFile());
controller.onNext();
} else {
reject.accept(e.getMessage());
}
}).executor(true),
i18n("message.downloading")
);
} catch (IOException e) {
reject.accept(e.getMessage());
}
});
}
@Override
public void cleanup(Map<String, Object> settings) {
}
@Override
public String getTitle() {
return i18n("modpack.task.install");
}
}
@@ -0,0 +1,90 @@
/*
* 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.RemoteVersion;
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 org.jetbrains.annotations.NotNull;
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 String oldLibraryVersion;
public UpdateInstallerWizardProvider(@NotNull Profile profile, @NotNull String gameVersion, @NotNull Version version, @NotNull String libraryId, @NotNull String oldLibraryVersion) {
this.profile = profile;
this.gameVersion = gameVersion;
this.version = version;
this.libraryId = libraryId;
this.oldLibraryVersion = oldLibraryVersion;
}
@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.
return profile.getDependency().installLibraryAsync(version, (RemoteVersion) settings.get(libraryId))
.thenComposeAsync(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), oldLibraryVersion, ((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;
}
}
@@ -50,17 +50,12 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
builder.name(name);
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
if (settings.containsKey("forge"))
builder.version((RemoteVersion) settings.get("forge"));
if (settings.containsKey("liteloader"))
builder.version((RemoteVersion) settings.get("liteloader"));
if (settings.containsKey("optifine"))
builder.version((RemoteVersion) settings.get("optifine"));
for (Map.Entry<String, Object> entry : settings.entrySet())
if (!"game".equals(entry.getKey()) && entry.getValue() instanceof RemoteVersion)
builder.version((RemoteVersion) entry.getValue());
return builder.buildAsync().whenComplete(any -> profile.getRepository().refreshVersions())
.thenRun(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
}
@Override
@@ -20,14 +20,19 @@ package org.jackhuang.hmcl.ui.export;
import javafx.scene.Node;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackManager;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
@@ -57,30 +62,43 @@ public final class ExportWizardProvider implements WizardProvider {
public Object finish(Map<String, Object> settings) {
@SuppressWarnings("unchecked")
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
String modpackName = (String) settings.get(ModpackInfoPage.MODPACK_NAME);
String modpackAuthor = (String) settings.get(ModpackInfoPage.MODPACK_AUTHOR);
String modpackFileApi = (String) settings.get(ModpackInfoPage.MODPACK_FILE_API);
String modpackVersion = (String) settings.get(ModpackInfoPage.MODPACK_VERSION);
String modpackDescription = (String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION);
String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER);
switch (modpackType) {
case ModpackTypeSelectionPage.MODPACK_TYPE_HMCL:
return exportAsHMCL(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, includeLauncher);
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
return exportAsMultiMC(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription);
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
return exportAsServer(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi);
default:
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
}
}
private Task<?> exportAsHMCL(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, boolean includeLauncherRaw) {
List<File> launcherJar = Launcher.getCurrentJarFiles();
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER) && launcherJar != null;
boolean includeLauncher = includeLauncherRaw && launcherJar != null;
return new Task<Void>() {
Task<?> dependency = null;
@Override
public void execute() throws Exception {
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
File tempModpack = includeLauncher ? Files.createTempFile("hmcl", ".zip").toFile() : modpackFile;
dependency = new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
new Modpack(
(String) settings.get(ModpackInfoPage.MODPACK_NAME),
(String) settings.get(ModpackInfoPage.MODPACK_AUTHOR),
(String) settings.get(ModpackInfoPage.MODPACK_VERSION),
null,
(String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION),
StandardCharsets.UTF_8,
null
), tempModpack);
new Modpack(modpackName, modpackAuthor, modpackVersion, null, modpackDescription, StandardCharsets.UTF_8, null), tempModpack);
if (includeLauncher) {
dependency = dependency.thenRun(() -> {
dependency = dependency.thenRunAsync(() -> {
try (Zipper zip = new Zipper(modpackFile.toPath())) {
Config exported = new Config();
@@ -120,11 +138,70 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
@Override
private Task<?> exportAsMultiMC(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription) {
return new Task<Void>() {
Task<?> dependency;
@Override
public void execute() {
VersionSetting vs = profile.getVersionSetting(version);
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, whitelist,
new MultiMCInstanceConfiguration(
"OneSix",
modpackName + "-" + modpackVersion,
null,
Lang.toIntOrNull(vs.getPermSize()),
vs.getWrapper(),
vs.getPreLaunchCommand(),
null,
modpackDescription,
null,
vs.getJavaArgs(),
vs.isFullscreen(),
vs.getWidth(),
vs.getHeight(),
vs.getMaxMemory(),
vs.getMinMemory(),
vs.isShowLogs(),
/* showConsoleOnError */ true,
/* autoCloseConsole */ false,
/* overrideMemory */ true,
/* overrideJavaLocation */ false,
/* overrideJavaArgs */ true,
/* overrideConsole */ true,
/* overrideCommands */ true,
/* overrideWindow */ true
), modpackFile);
}
@Override
public Collection<Task<?>> getDependencies() {
return Collections.singleton(dependency);
}
};
}
private Task<?> exportAsServer(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, String modpackFileApi) {
return new Task<Void>() {
Task<?> dependency;
@Override
public void execute() {
dependency = new ServerModpackExportTask(profile.getRepository(), version, whitelist, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi, modpackFile);
}
@Override
public Collection<Task<?>> getDependencies() {
return Collections.singleton(dependency);
}
};
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
switch (step) {
case 0: return new ModpackInfoPage(controller, version);
case 1: return new ModpackFileSelectionPage(controller, profile, version, HMCLModpackManager::suggestMod);
case 0: return new ModpackTypeSelectionPage(controller);
case 1: return new ModpackInfoPage(controller, version);
case 2: return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
default: throw new IllegalArgumentException("step");
}
}
@@ -25,7 +25,7 @@ import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.ModAdviser;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
@@ -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;
@@ -156,6 +156,7 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
public static final String MODPACK_FILE_SELECTION = "modpack.accepted";
private static final Map<String, String> TRANSLATION = mapOf(
pair("minecraft/hmclversion.cfg", i18n("modpack.files.hmclversion_cfg")),
pair("minecraft/servers.dat", i18n("modpack.files.servers_dat")),
pair("minecraft/saves", i18n("modpack.files.saves")),
pair("minecraft/mods", i18n("modpack.files.mods")),
@@ -21,63 +21,69 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextArea;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import com.jfoenix.validation.RequiredFieldValidator;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackInfoPage extends StackPane implements WizardPage {
public final class ModpackInfoPage extends Control implements WizardPage {
private final WizardController controller;
@FXML
private Label lblVersionName;
@FXML
private JFXTextField txtModpackName;
@FXML
private JFXTextField txtModpackAuthor;
@FXML
private JFXTextField txtModpackVersion;
@FXML
private JFXTextArea txtModpackDescription;
@FXML
private JFXToggleButton chkIncludeLauncher;
@FXML
private JFXButton btnNext;
@FXML
private ScrollPane scroll;
private final boolean canIncludeLauncher;
private final boolean showFileApi;
private SimpleStringProperty versionName = new SimpleStringProperty();
private SimpleStringProperty modpackName = new SimpleStringProperty();
private SimpleStringProperty modpackFileApi = new SimpleStringProperty();
private SimpleStringProperty modpackAuthor = new SimpleStringProperty();
private SimpleStringProperty modpackVersion = new SimpleStringProperty("1.0");
private SimpleStringProperty modpackDescription = new SimpleStringProperty();
private SimpleBooleanProperty includingLauncher = new SimpleBooleanProperty();
private ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
public ModpackInfoPage(WizardController controller, String version) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/modpack/info.fxml");
FXUtils.smoothScrolling(scroll);
txtModpackName.setText(version);
txtModpackName.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.textProperty().addListener(e -> checkValidation());
txtModpackVersion.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.setText(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
lblVersionName.setText(version);
modpackName.set(version);
modpackAuthor.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
versionName.set(version);
List<File> launcherJar = Launcher.getCurrentJarFiles();
if (launcherJar == null)
chkIncludeLauncher.setDisable(true);
}
canIncludeLauncher = launcherJar != null;
showFileApi = controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER;
private void checkValidation() {
btnNext.setDisable(!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate());
next.set(e -> onNext());
}
@FXML
@@ -90,12 +96,13 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
controller.onEnd();
return;
}
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
controller.getSettings().put(MODPACK_VERSION, txtModpackVersion.getText());
controller.getSettings().put(MODPACK_AUTHOR, txtModpackAuthor.getText());
controller.getSettings().put(MODPACK_NAME, modpackName.get());
controller.getSettings().put(MODPACK_FILE_API, modpackFileApi.get());
controller.getSettings().put(MODPACK_VERSION, modpackVersion.get());
controller.getSettings().put(MODPACK_AUTHOR, modpackAuthor.get());
controller.getSettings().put(MODPACK_FILE, file);
controller.getSettings().put(MODPACK_DESCRIPTION, txtModpackDescription.getText());
controller.getSettings().put(MODPACK_INCLUDE_LAUNCHER, chkIncludeLauncher.isSelected());
controller.getSettings().put(MODPACK_DESCRIPTION, modpackDescription.get());
controller.getSettings().put(MODPACK_INCLUDE_LAUNCHER, includingLauncher.get());
controller.onNext();
}
@@ -114,10 +121,175 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
return i18n("modpack.wizard.step.1.title");
}
@Override
protected Skin<?> createDefaultSkin() {
return new ModpackInfoPageSkin(this);
}
public static final String MODPACK_NAME = "modpack.name";
public static final String MODPACK_FILE_API = "modpack.file_api";
public static final String MODPACK_VERSION = "modpack.version";
public static final String MODPACK_AUTHOR = "archive.author";
public static final String MODPACK_DESCRIPTION = "modpack.description";
public static final String MODPACK_INCLUDE_LAUNCHER = "modpack.include_launcher";
public static final String MODPACK_FILE = "modpack.file";
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
private final JFXTextField txtModpackName;
private final JFXTextField txtModpackFileApi;
private final JFXTextField txtModpackAuthor;
private final JFXTextField txtModpackVersion;
public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
super(skinnable);
Insets insets = new Insets(5, 0, 12, 0);
Insets componentListMargin = new Insets(16, 0, 16, 0);
ScrollPane scroll = new ScrollPane();
scroll.setFitToWidth(true);
scroll.setFitToHeight(true);
getChildren().setAll(scroll);
{
BorderPane borderPane = new BorderPane();
borderPane.setStyle("-fx-padding: 16;");
scroll.setContent(borderPane);
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER) {
Hyperlink hyperlink = new Hyperlink(i18n("modpack.wizard.step.initialization.server"));
hyperlink.setOnMouseClicked(e -> {
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/server-modpack");
});
borderPane.setTop(hyperlink);
} else {
Label label = new Label(i18n("modpack.wizard.step.initialization.warning"));
label.setWrapText(true);
label.setTextAlignment(TextAlignment.JUSTIFY);
borderPane.setTop(label);
}
{
ComponentList list = new ComponentList();
BorderPane.setMargin(list, componentListMargin);
borderPane.setCenter(list);
{
BorderPane borderPane1 = new BorderPane();
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
Label versionNameLabel = new Label();
versionNameLabel.textProperty().bind(skinnable.versionName);
borderPane1.setRight(versionNameLabel);
list.getContent().add(borderPane1);
}
{
txtModpackName = new JFXTextField();
txtModpackName.textProperty().bindBidirectional(skinnable.modpackName);
txtModpackName.setLabelFloat(true);
txtModpackName.setPromptText(i18n("modpack.name"));
RequiredFieldValidator validator = new RequiredFieldValidator();
validator.setMessage(i18n("modpack.not_a_valid_name"));
txtModpackName.getValidators().add(validator);
StackPane.setMargin(txtModpackName, insets);
list.getContent().add(txtModpackName);
}
if (skinnable.showFileApi) {
txtModpackFileApi = new JFXTextField();
txtModpackFileApi.textProperty().bindBidirectional(skinnable.modpackFileApi);
txtModpackFileApi.setLabelFloat(true);
txtModpackFileApi.setPromptText(i18n("modpack.file_api"));
RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackFileApi.getValidators().add(validator);
txtModpackFileApi.getValidators().add(new Validator(s -> {
try {
new URL(s).toURI();
return true;
} catch (IOException | URISyntaxException e) {
return false;
}
}));
StackPane.setMargin(txtModpackFileApi, insets);
list.getContent().add(txtModpackFileApi);
} else {
txtModpackFileApi = null;
}
{
txtModpackAuthor = new JFXTextField();
txtModpackAuthor.textProperty().bindBidirectional(skinnable.modpackAuthor);
txtModpackAuthor.setLabelFloat(true);
txtModpackAuthor.setPromptText(i18n("archive.author"));
RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackAuthor.getValidators().add(validator);
StackPane.setMargin(txtModpackAuthor, insets);
list.getContent().add(txtModpackAuthor);
}
{
txtModpackVersion = new JFXTextField();
txtModpackVersion.textProperty().bindBidirectional(skinnable.modpackVersion);
txtModpackVersion.setLabelFloat(true);
txtModpackVersion.setPromptText(i18n("archive.version"));
RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackVersion.getValidators().add(validator);
StackPane.setMargin(txtModpackVersion, insets);
list.getContent().add(txtModpackVersion);
}
{
JFXTextArea area = new JFXTextArea();
area.textProperty().bindBidirectional(skinnable.modpackDescription);
area.setLabelFloat(true);
area.setPromptText(i18n("modpack.desc"));
area.setMinHeight(400);
StackPane.setMargin(area, insets);
list.getContent().add(area);
}
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_HMCL) {
BorderPane borderPane1 = new BorderPane();
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
list.getContent().add(borderPane1);
JFXToggleButton button = new JFXToggleButton();
button.setDisable(!skinnable.canIncludeLauncher);
button.selectedProperty().bindBidirectional(skinnable.includingLauncher);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
borderPane1.setRight(button);
}
}
{
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
borderPane.setBottom(hbox);
JFXButton nextButton = new JFXButton();
nextButton.onMouseClickedProperty().bind(skinnable.next);
nextButton.setPrefWidth(100);
nextButton.setPrefHeight(40);
nextButton.setButtonType(JFXButton.ButtonType.RAISED);
nextButton.setText(i18n("wizard.next"));
nextButton.getStyleClass().add("jfx-button-raised");
if (skinnable.showFileApi) {
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate() || !txtModpackFileApi.validate(),
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty(), txtModpackFileApi.textProperty()));
} else {
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate(),
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty()));
}
hbox.getChildren().add(nextButton);
}
}
FXUtils.smoothScrolling(scroll);
}
}
}
@@ -0,0 +1,69 @@
/*
* 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.export;
import com.jfoenix.controls.JFXButton;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.Map;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackTypeSelectionPage extends StackPane implements WizardPage {
private final WizardController controller;
@FXML
private JFXButton btnHMCL;
@FXML
private JFXButton btnMultiMC;
@FXML
private JFXButton btnServer;
public ModpackTypeSelectionPage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/modpack/type.fxml");
JFXButton[] buttons = new JFXButton[]{btnHMCL, btnMultiMC, btnServer};
String[] types = new String[]{MODPACK_TYPE_HMCL, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
for (int i = 0; i < types.length; ++i) {
String type = types[i];
buttons[i].setOnMouseClicked(e -> {
controller.getSettings().put(MODPACK_TYPE, type);
controller.onNext();
});
}
}
@Override
public void cleanup(Map<String, Object> settings) {
}
@Override
public String getTitle() {
return i18n("modpack.wizard.step.3.title");
}
public static final String MODPACK_TYPE = "modpack.type";
public static final String MODPACK_TYPE_HMCL = "hmcl";
public static final String MODPACK_TYPE_MULTIMC = "multimc";
public static final String MODPACK_TYPE_SERVER = "server";
}
@@ -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);
@@ -72,7 +73,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
right.getChildren().add(btnRemove);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
@@ -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,26 +58,32 @@ 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 -> {
try {
Datapack zip = new Datapack(it.toPath());
zip.loadFromZip();
zip.installTo(worldDir);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e);
}
});
}, datapack::loadFromDir);
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
}
private void installSingleDatapack(File datapack) {
try {
Datapack zip = new Datapack(datapack.toPath());
zip.loadFromZip();
zip.installTo(worldDir);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + datapack, e);
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new DatapackListPageSkin(this);
}
public void refresh() {
setLoading(true);
Task.runAsync(datapack::loadFromDir)
.withRunAsync(Schedulers.javafx(), () -> setLoading(false))
.start();
}
@Override
@@ -77,7 +91,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"));
@@ -85,16 +98,36 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
if (res != null)
res.forEach(it -> {
try {
Datapack zip = new Datapack(it.toPath());
zip.loadFromZip();
zip.installTo(worldDir);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e);
}
});
res.forEach(this::installSingleDatapack);
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"));
}
});
@@ -18,24 +18,29 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.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);
@@ -51,13 +56,21 @@ public class GameItem extends Control {
this.version = id;
// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
CompletableFuture.supplyAsync(() -> GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse("Unknown"), POOL_VERSION_RESOLVE)
CompletableFuture.supplyAsync(() -> GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse(i18n("message.unknown")), POOL_VERSION_RESOLVE)
.thenAcceptAsync(game -> {
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", ""))));
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id));
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
if (libraryId.equals(MINECRAFT.getPatchId())) continue;
if (I18n.hasKey("install.installer." + libraryId)) {
libraries.append(", ").append(i18n("install.installer." + libraryId));
if (libraryVersion != null)
libraries.append(": ").append(modifyVersion("", libraryVersion.replaceAll("(?i)" + libraryId, "")));
}
}
subtitle.set(libraries.toString());
}, Platform::runLater)
.exceptionally(handleUncaught);
@@ -17,12 +17,10 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.utils.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,13 +17,14 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.SkinBase;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
@@ -33,6 +34,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> {
@@ -49,22 +51,24 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
chkSelected.setToggleGroup(skinnable.getToggleGroup());
root.setLeft(chkSelected);
root.setCenter(new GameItem(skinnable.getProfile(), skinnable.getVersion()));
GameItem gameItem = new GameItem(skinnable.getProfile(), skinnable.getVersion());
gameItem.setMouseTransparent(true);
root.setCenter(gameItem);
PopupMenu menu = new PopupMenu();
JFXPopup popup = new JFXPopup(menu);
menu.getContent().setAll(
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(skinnable::launch, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(skinnable::generateLaunchScript, popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.gear(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.manage"), FXUtils.withJFXPopupClosing(skinnable::modifyGameSettings, popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(skinnable::rename, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(skinnable::remove, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(skinnable::export, popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(skinnable::browse, popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(skinnable::launch, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(skinnable::generateLaunchScript, popup)));
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(skinnable::browse, popup)));
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
@@ -73,7 +77,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);
}
@@ -87,9 +91,20 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
right.getChildren().add(btnManage);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
getChildren().setAll(root);
root.setCursor(Cursor.HAND);
root.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
if (e.getClickCount() == 1) {
skinnable.modifyGameSettings();
}
} else if (e.getButton() == MouseButton.SECONDARY) {
popup.show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
}
});
}
}
@@ -1,116 +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.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.SVG;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
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().add("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);
}
{
SpinnerPane center = new SpinnerPane();
center.loadingProperty().bind(skinnable.loadingProperty());
center.getStyleClass().add("large-spinner-pane");
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);
center.setContent(scrollPane);
root.setCenter(center);
}
getChildren().setAll(root);
}
}
@@ -17,61 +17,140 @@
*/
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;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.ToolbarListPageSkin;
import org.jackhuang.hmcl.ui.download.InstallerWizardProvider;
import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.util.LinkedList;
import java.util.Optional;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
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.version = profile.getRepository().getVersion(versionId);
this.gameVersion = null;
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
Task.supplyAsync(() -> {
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))
.thenCompose(maintainedVersion -> new VersionJsonSaveTask(profile.getRepository(), maintainedVersion))
.withCompose(profile.getRepository().refreshVersionsAsync())
.withRun(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
.start();
};
return LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(versionId));
}).thenAcceptAsync(Schedulers.javafx(), analyzer -> {
Function<String, Consumer<InstallerItem>> removeAction = libraryId -> x -> {
profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::save)
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
.withRunAsync(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();
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
String title = I18n.hasKey("install.installer." + libraryId) ? i18n("install.installer." + libraryId) : libraryId;
Consumer<InstallerItem> action = "game".equals(libraryId) ? null : removeAction.apply(libraryId);
if (libraryVersion != null && Lang.test(() -> profile.getDependency().getVersionList(libraryId)))
itemsProperty().add(
new InstallerItem(title, libraryVersion, () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
}, action));
else
itemsProperty().add(new InstallerItem(title, libraryVersion, null, action));
}
}).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())
.thenComposeAsync(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.getException() == null)
return;
InstallerWizardProvider.alertFailureMessage(executor.getException(), 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));
}
}
}
@@ -17,18 +17,14 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.utils.JFXUtilities;
import com.jfoenix.controls.JFXTabPane;
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;
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;
@@ -36,6 +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.ListPageBase;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -44,17 +41,19 @@ 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 Control {
private final ListProperty<ModListPageSkin.ModInfoObject> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
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() {
@@ -80,14 +79,16 @@ public final class ModListPage extends Control {
}
public void loadVersion(Profile profile, String id) {
libraryAnalyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id));
modded.set(libraryAnalyzer.hasModLoader());
loadMods(profile.getRepository().getModManager(id));
}
public void loadMods(ModManager modManager) {
private void loadMods(ModManager modManager) {
this.modManager = modManager;
Task.supplyAsync(() -> {
synchronized (ModListPage.this) {
JFXUtilities.runInFX(() -> loadingProperty().set(true));
runInFX(() -> loadingProperty().set(true));
modManager.refreshMods();
return new LinkedList<>(modManager.getMods());
}
@@ -123,7 +124,7 @@ public final class ModListPage extends Control {
// Actually addMod will not throw exceptions because FileChooser has already filtered files.
}
}
}).withRun(Schedulers.javafx(), () -> {
}).withRunAsync(Schedulers.javafx(), () -> {
List<String> prompt = new LinkedList<>();
if (!succeeded.isEmpty())
prompt.add(i18n("mods.add.success", String.join(", ", succeeded)));
@@ -138,10 +139,12 @@ public final class ModListPage extends Control {
this.parentTab = parentTab;
}
public void removeSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
public void removeSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
try {
modManager.removeMods(selectedItems.stream()
.filter(Objects::nonNull)
.map(TreeItem::getValue)
.filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.toArray(ModInfo[]::new));
loadMods(modManager);
@@ -150,41 +153,29 @@ public final class ModListPage extends Control {
}
}
public void enableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
public void enableSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(true));
}
public void disableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
public void disableSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
selectedItems.stream()
.map(TreeItem::getValue)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(false));
}
public ObservableList<ModListPageSkin.ModInfoObject> getItems() {
return items.get();
public boolean isModded() {
return modded.get();
}
public void setItems(ObservableList<ModListPageSkin.ModInfoObject> items) {
this.items.set(items);
public BooleanProperty moddedProperty() {
return modded;
}
public ListProperty<ModListPageSkin.ModInfoObject> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
public void setModded(boolean modded) {
this.modded.set(modded);
}
}
@@ -17,42 +17,43 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
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.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.*;
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 javafx.util.Callback;
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.JFXCheckBoxTreeTableCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import java.util.function.Function;
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;
public class ModListPageSkin extends SkinBase<ModListPage> {
class ModListPageSkin extends SkinBase<ModListPage> {
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 ModListPageSkin(ModListPage skinnable) {
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<>();
@@ -62,46 +63,14 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
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("button.refresh"));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
toolbar.getChildren().add(btnRefresh);
JFXButton btnAddMod = new JFXButton();
btnAddMod.getStyleClass().add("jfx-tool-bar-button");
btnAddMod.textFillProperty().bind(Theme.foregroundFillBinding());
btnAddMod.setGraphic(wrap(SVG.plus(Theme.foregroundFillBinding(), -1, -1)));
btnAddMod.setText(i18n("mods.add"));
btnAddMod.setOnMouseClicked(e -> skinnable.add());
toolbar.getChildren().add(btnAddMod);
JFXButton btnRemove = new JFXButton();
btnRemove.getStyleClass().add("jfx-tool-bar-button");
btnRemove.textFillProperty().bind(Theme.foregroundFillBinding());
btnRemove.setGraphic(wrap(SVG.delete(Theme.foregroundFillBinding(), -1, -1)));
btnRemove.setText(i18n("mods.remove"));
btnRemove.setOnMouseClicked(e -> skinnable.removeSelectedMods(tableView.getSelectionModel().getSelectedItems()));
toolbar.getChildren().add(btnRemove);
JFXButton btnEnable = new JFXButton();
btnEnable.getStyleClass().add("jfx-tool-bar-button");
btnEnable.textFillProperty().bind(Theme.foregroundFillBinding());
btnEnable.setGraphic(wrap(SVG.check(Theme.foregroundFillBinding(), -1, -1)));
btnEnable.setText(i18n("mods.enable"));
btnEnable.setOnMouseClicked(e -> skinnable.enableSelectedMods(tableView.getSelectionModel().getSelectedItems()));
toolbar.getChildren().add(btnEnable);
JFXButton btnDisable = new JFXButton();
btnDisable.getStyleClass().add("jfx-tool-bar-button");
btnDisable.textFillProperty().bind(Theme.foregroundFillBinding());
btnDisable.setGraphic(wrap(SVG.close(Theme.foregroundFillBinding(), -1, -1)));
btnDisable.setText(i18n("mods.disable"));
btnDisable.setOnMouseClicked(e -> skinnable.disableSelectedMods(tableView.getSelectionModel().getSelectedItems()));
toolbar.getChildren().add(btnDisable);
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);
}
@@ -110,6 +79,7 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
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);
@@ -122,65 +92,54 @@ public class ModListPageSkin extends SkinBase<ModListPage> {
activeColumn.setMaxWidth(40);
activeColumn.setMinWidth(40);
JFXTreeTableColumn<ModInfoObject, String> fileNameColumn = new JFXTreeTableColumn<>();
fileNameColumn.setText(i18n("archive.name"));
setupCellValueFactory(fileNameColumn, ModInfoObject::fileNameProperty);
fileNameColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(40).multiply(0.8));
JFXTreeTableColumn<ModInfoObject, Node> detailColumn = new JFXTreeTableColumn<>();
setupCellValueFactory(detailColumn, ModInfoObject::nodeProperty);
JFXTreeTableColumn<ModInfoObject, String> versionColumn = new JFXTreeTableColumn<>();
versionColumn.setText(i18n("archive.version"));
versionColumn.setPrefWidth(100);
setupCellValueFactory(versionColumn, ModInfoObject::versionProperty);
versionColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(40).multiply(0.2));
tableView.getColumns().setAll(activeColumn, fileNameColumn, versionColumn);
tableView.getColumns().setAll(activeColumn, detailColumn);
tableView.setColumnResizePolicy(JFXTreeTableView.CONSTRAINED_RESIZE_POLICY);
center.setContent(tableView);
root.setCenter(center);
}
getChildren().setAll(root);
}
Label label = new Label(i18n("mods.not_modded"));
label.prefWidthProperty().bind(pane.widthProperty().add(-100));
private <T> void setupCellValueFactory(JFXTreeTableColumn<ModInfoObject, T> column, Function<ModInfoObject, ObservableValue<T>> mapper) {
column.setCellValueFactory(new Callback<TreeTableColumn.CellDataFeatures<ModInfoObject, T>, ObservableValue<T>>() {
@Override
public ObservableValue<T> call(TreeTableColumn.CellDataFeatures<ModInfoObject, T> param) {
if (column.validateValue(param))
return mapper.apply(param.getValue().getValue());
else
return column.getComputedValue(param);
}
FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> {
if (modded) pane.getChildren().setAll(root);
else pane.getChildren().setAll(label);
});
getChildren().setAll(pane);
}
public static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> {
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> {
private final BooleanProperty active;
private final StringProperty fileName;
private final StringProperty version;
private final ModInfo modInfo;
private final ObjectProperty<Node> node;
public ModInfoObject(ModInfo modInfo) {
ModInfoObject(ModInfo modInfo) {
this.modInfo = modInfo;
this.active = modInfo.activeProperty();
this.fileName = new SimpleStringProperty(modInfo.getFileName());
this.version = new SimpleStringProperty(modInfo.getVersion());
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)));
}
public BooleanProperty activeProperty() {
BooleanProperty activeProperty() {
return active;
}
public StringProperty fileNameProperty() {
return fileName;
ObjectProperty<Node> nodeProperty() {
return node;
}
public StringProperty versionProperty() {
return version;
}
public ModInfo getModInfo() {
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;
@@ -61,6 +57,7 @@ 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;
@@ -118,7 +115,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.smoothScrolling(scroll);
Task.supplyAsync(JavaVersion::getJavas).thenAccept(Schedulers.javafx(), list -> {
Task.supplyAsync(JavaVersion::getJavas).thenAcceptAsync(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))
@@ -274,7 +271,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
if (versionSetting == null)
return;
Task.supplyAsync(versionSetting::getJavaVersion)
.thenAccept(Schedulers.javafx(), javaVersion -> javaItem.setSubtitle(Optional.ofNullable(javaVersion)
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaItem.setSubtitle(Optional.ofNullable(javaVersion)
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
.start();
}
@@ -316,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;
}
@@ -324,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);
}
@@ -21,7 +21,6 @@ import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
@@ -49,9 +48,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);
}
@@ -79,8 +76,7 @@ public class Versions {
}
public static void updateGameAssets(Profile profile, String version) {
Version resolvedVersion = profile.getRepository().getResolvedVersion(version);
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), resolvedVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY)
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version), 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,48 +17,85 @@
*/
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.InvalidPathException;
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.supplyAsync(() -> World.getWorlds(savesDir).parallel().collect(Collectors.toList()))
.whenComplete(Schedulers.javafx(), (result, exception) -> {
setLoading(false);
if (exception == null)
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
.runAsync(() -> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse(null))
.thenSupplyAsync(() -> World.getWorlds(savesDir).parallel().collect(Collectors.toList()))
.whenComplete(Schedulers.javafx(), (result, exception) -> {
worlds = result;
setLoading(false);
if (exception == null)
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"));
@@ -82,6 +119,8 @@ public class WorldListPage extends ListPage<WorldListItem> {
}, e -> {
if (e instanceof FileAlreadyExistsException)
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
reject.accept(i18n("world.import.failed", i18n("install.new_game.malformed")));
else
reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
}).start();
@@ -91,4 +130,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));
}
}
}
@@ -33,7 +33,7 @@ public interface AbstractWizardDisplayer extends WizardDisplayer {
@Override
default void handleTask(Map<String, Object> settings, Task<?> task) {
TaskExecutor executor = task.withRun(Schedulers.javafx(), this::navigateToSuccess).executor();
TaskExecutor executor = task.withRunAsync(Schedulers.javafx(), this::navigateToSuccess).executor();
TaskListPane pane = new TaskListPane();
pane.setExecutor(executor);
navigateTo(pane, Navigation.NavigationDirection.FINISH);
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.wizard;
import com.jfoenix.utils.JFXUtilities;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -30,6 +29,7 @@ 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,12 +59,11 @@ 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, MessageType.FINE, () -> 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,6 +26,7 @@ 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.UpgradeDialog;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -37,10 +38,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -91,41 +89,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();
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 {
Exception e = executor.getException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR));
return;
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageType.ERROR));
}
} else {
Exception e = executor.getException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageType.ERROR));
}
});
});
}));
}
private static void applyUpdate(Path target) throws IOException {
@@ -159,9 +158,7 @@ public final class UpdateHandler {
commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
commandline.add("-jar");
commandline.add(jar.toAbsolutePath().toString());
for (String arg : appArgs) {
commandline.add(arg);
}
commandline.addAll(Arrays.asList(appArgs));
LOG.info("Starting process: " + commandline);
new ProcessBuilder(commandline)
.directory(Paths.get("").toAbsolutePath().toFile())
@@ -204,11 +201,7 @@ public final class UpdateHandler {
StackTraceElement element = stacktrace[i];
if (Main.class.getName().equals(element.getClassName())) {
// we've reached the main method
if (i + 1 == stacktrace.length) {
return false;
} else {
return true;
}
return i + 1 != stacktrace.length;
}
}
return false;
@@ -20,7 +20,6 @@ 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.upgrade.IntegrityChecker;
@@ -52,9 +51,11 @@ 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"));
put("javax.imageio.IIOException", i18n("crash.NoClassDefFound"));
put("netscape.javascript.JSException", i18n("crash.NoClassDefFound"));
put("java.lang.IncompatibleClassChangeError", i18n("crash.NoClassDefFound"));
put("java.lang.ClassFormatError", i18n("crash.NoClassDefFound"));
@@ -86,6 +87,12 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
private static Set<String> CAUGHT_EXCEPTIONS = newSetFromMap(new ConcurrentHashMap<>());
private final boolean showCrashWindow;
public CrashReporter(boolean showCrashWindow) {
this.showCrashWindow = showCrashWindow;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
LOG.log(Level.SEVERE, "Uncaught exception in thread " + t.getName(), e);
@@ -116,7 +123,9 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
LOG.log(Level.SEVERE, text);
if (checkThrowable(e)) {
Platform.runLater(() -> new CrashWindow(text).show());
if (showCrashWindow) {
Platform.runLater(() -> new CrashWindow(text).show());
}
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
reportToServer(text);
}
@@ -0,0 +1,41 @@
/*
* 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.util;
import java.io.InputStream;
/**
* Suppress the throwable when we make sure the resource cannot miss.
* @see CrashReporter
*/
public class ResourceNotFoundError extends Error {
public ResourceNotFoundError(String message) {
super(message);
}
public ResourceNotFoundError(String message, Throwable cause) {
super(message, cause);
}
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;
}
}
}
@@ -47,16 +47,16 @@ public final class Locales {
public static final SupportedLocale ZH_CN = new SupportedLocale(Locale.SIMPLIFIED_CHINESE);
/**
* Vietnamese
* Spanish
*/
public static final SupportedLocale VI = new SupportedLocale(new Locale("vi"));
public static final SupportedLocale ES = new SupportedLocale(new Locale("es"));
/**
* Russian
*/
public static final SupportedLocale RU = new SupportedLocale(new Locale("ru"));
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH);
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH, ES, RU);
public static SupportedLocale getLocaleByName(String name) {
if (name == null) return DEFAULT;
@@ -64,7 +64,7 @@ public final class Locales {
case "en": return EN;
case "zh": return ZH;
case "zh_cn": return ZH_CN;
case "vi": return VI;
case "es": return ES;
case "ru": return RU;
default: return DEFAULT;
}
@@ -74,7 +74,7 @@ public final class Locales {
if (locale == EN) return "en";
else if (locale == ZH) return "zh";
else if (locale == ZH_CN) return "zh_CN";
else if (locale == VI) return "vi";
else if (locale == ES) return "es";
else if (locale == RU) return "ru";
else if (locale == DEFAULT) return "def";
else throw new IllegalArgumentException("Unknown locale: " + locale);
+24 -6
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 {
@@ -379,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 {
@@ -521,6 +526,12 @@
-fx-background-color: -fx-base-color;
}
.jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: white;
-jfx-mask-type: CIRCLE;
-fx-padding: 0.0;
}
.jfx-button-raised, .jfx-button-raised * {
-fx-text-fill: -fx-base-text-fill;
-fx-font-size: 14px;
@@ -992,7 +1003,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 {
@@ -1010,7 +1022,7 @@
}
.white-background {
-fx-background-color: rgba(255, 255, 255);
-fx-background-color: rgb(255, 255, 255);
}
/*******************************************************************************
@@ -1019,6 +1031,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);
@@ -1069,7 +1087,7 @@
.tree-table-view .tree-table-cell {
-fx-border-width: 0 0 0 0;
-fx-padding: 8 0 8 0;
-fx-padding: 0 0 0 0;
/*-fx-alignment: top-center;*/
}
@@ -1115,7 +1133,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 {
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.*?>
@@ -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>
@@ -15,11 +15,23 @@
</top>
<center>
<VBox fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300">
<JFXButton fx:id="btnFabric" prefWidth="${list.width}">
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblFabric"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
</right>
</BorderPane>
</graphic>
</JFXButton>
<JFXButton fx:id="btnForge" prefWidth="${list.width}">
<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 +43,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 +55,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"/>
@@ -15,11 +15,23 @@
</top>
<center>
<VBox fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300">
<JFXButton fx:id="btnFabric" prefWidth="${list.width}">
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblFabric"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
</right>
</BorderPane>
</graphic>
</JFXButton>
<JFXButton fx:id="btnForge" prefWidth="${list.width}">
<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 +43,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 +55,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"/>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.TwoLineListItem?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<fx:define>
<Insets fx:id="insets" topRightBottomLeft="8" />
</fx:define>
<VBox fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="400">
<Label padding="${insets}" text="%install.modpack" />
<JFXButton fx:id="btnLocal" prefWidth="${list.width}" onMouseClicked="#onChooseLocalFile">
<graphic>
<BorderPane mouseTransparent="true">
<left>
<TwoLineListItem title="%modpack.choose.local" subtitle="%modpack.choose.local.detail" />
</left>
<right>
<fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/>
</right>
</BorderPane>
</graphic>
</JFXButton>
<JFXButton fx:id="btnRemote" prefWidth="${list.width}" onMouseClicked="#onChooseRemoteFile">
<graphic>
<BorderPane mouseTransparent="true">
<left>
<TwoLineListItem title="%modpack.choose.remote" subtitle="%modpack.choose.remote.detail" />
</left>
<right>
<fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/>
</right>
</BorderPane>
</graphic>
</JFXButton>
</VBox>
</fx:root>
@@ -24,7 +24,7 @@
<BorderPane><left><Label text="%archive.version"/></left><right><Label fx:id="lblVersion" /></right></BorderPane>
<BorderPane><left><Label text="%archive.author"/></left><right><Label fx:id="lblAuthor" /></right></BorderPane>
<BorderPane>
<left><JFXButton fx:id="btnDescription" onMouseClicked="#onDescribe" text="%modpack.wizard.step.3" styleClass="jfx-button-border" /></left>
<left><JFXButton fx:id="btnDescription" onMouseClicked="#onDescribe" text="%modpack.description" styleClass="jfx-button-border" /></left>
<right><JFXButton buttonType="RAISED" fx:id="btnInstall" onMouseClicked="#onInstall" text="%button.install" styleClass="jfx-button-raised" /></right>
</BorderPane>
</ComponentList>
@@ -24,11 +24,11 @@
<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 fx:id="failedPane" styleClass="notice-pane">
<Label onMouseClicked="#onRefresh" 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 fx:id="emptyPane" styleClass="notice-pane">
<Label onMouseClicked="#onBack" text="%download.failed.empty" />
</StackPane>
</StackPane>
</center>
+20 -15
View File
@@ -1,20 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import com.jfoenix.controls.JFXCheckBox?>
<?import com.jfoenix.controls.JFXComboBox?>
<?import javafx.collections.FXCollections?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.web.WebView?>
<?import java.lang.String?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane"
style="-fx-background-color: white; -fx-padding: 3 0 3 0;">
<?import javafx.collections.FXCollections?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.web.WebView?>
<fx:root style="-fx-background-color: white; -fx-padding: 3 0 3 0;" type="StackPane" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1">
<VBox spacing="3">
<BorderPane style="-fx-padding: 0 3 0 3;">
<left>
<HBox alignment="CENTER_LEFT" style="-fx-padding: 0 0 0 4;" spacing="3">
<HBox alignment="CENTER_LEFT" spacing="3" style="-fx-padding: 0 0 0 4;">
<Label text="%logwindow.show_lines" />
<JFXComboBox fx:id="cboLines">
<items>
@@ -29,19 +33,19 @@
</left>
<right>
<HBox spacing="3">
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #F7A699;" fx:id="btnFatals" text="0 fatals" selected="true">
<ToggleButton fx:id="btnFatals" selected="true" style="-fx-background-color: #F7A699;" styleClass="log-toggle" text="0 fatals">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #FFCCBB;" fx:id="btnErrors" text="0 errors" selected="true">
<ToggleButton fx:id="btnErrors" selected="true" style="-fx-background-color: #FFCCBB;" styleClass="log-toggle" text="0 errors">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #FFEECC;" fx:id="btnWarns" text="0 warns" selected="true">
<ToggleButton fx:id="btnWarns" selected="true" style="-fx-background-color: #FFEECC;" styleClass="log-toggle" text="0 warns">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #FBFBFB;" fx:id="btnInfos" text="0 infos" selected="true">
<ToggleButton fx:id="btnInfos" selected="true" style="-fx-background-color: #FBFBFB;" styleClass="log-toggle" text="0 infos">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #EEE9E0;" fx:id="btnDebugs" text="0 debugs" selected="true">
<ToggleButton fx:id="btnDebugs" selected="true" style="-fx-background-color: #EEE9E0;" styleClass="log-toggle" text="0 debugs">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
</HBox>
@@ -50,7 +54,8 @@
<StackPane style="-fx-border: 1 0 1 0; -fx-border-color: #dddddd;" VBox.vgrow="ALWAYS">
<WebView fx:id="webView" />
</StackPane>
<HBox alignment="CENTER_RIGHT" style="-fx-padding: 0 3 0 3;" spacing="3">
<HBox alignment="CENTER_RIGHT" spacing="3" style="-fx-padding: 0 3 0 3;">
<JFXCheckBox fx:id="autoscroll" selected="true" text="%logwindow.autoscroll" />
<JFXButton onMouseClicked="#onTerminateGame" text="%logwindow.terminate_game" />
<JFXButton onMouseClicked="#onClear" text="%button.clear" />
</HBox>
@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.*?>
<?import com.jfoenix.validation.RequiredFieldValidator?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.ComponentList?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<fx:define>
<Insets fx:id="insets" top="5" bottom="12"/>
<Insets fx:id="componetListMargin" top="16" bottom="16" />
</fx:define>
<ScrollPane fx:id="scroll" fitToWidth="true" fitToHeight="true">
<BorderPane style="-fx-padding: 16;">
<top>
<Label wrapText="true" textAlignment="JUSTIFY" text="%modpack.wizard.step.initialization.warning"/>
</top>
<center>
<ComponentList BorderPane.margin="$componetListMargin">
<BorderPane>
<left>
<Label text="%modpack.wizard.step.initialization.exported_version" />
</left>
<right>
<Label fx:id="lblVersionName"/>
</right>
</BorderPane>
<JFXTextField labelFloat="true" promptText="%modpack.name" fx:id="txtModpackName"
StackPane.margin="$insets">
<validators>
<RequiredFieldValidator message="%modpack.not_a_valid_name"/>
</validators>
</JFXTextField>
<JFXTextField labelFloat="true" promptText="%archive.author" fx:id="txtModpackAuthor"
StackPane.margin="$insets">
<validators>
<RequiredFieldValidator/>
</validators>
</JFXTextField>
<JFXTextField labelFloat="true" promptText="%archive.version" fx:id="txtModpackVersion" text="1.0"
StackPane.margin="$insets">
<validators>
<RequiredFieldValidator/>
</validators>
</JFXTextField>
<JFXTextArea labelFloat="true" promptText="%modpack.desc" fx:id="txtModpackDescription"
StackPane.margin="$insets" minHeight="400" />
<BorderPane>
<left>
<Label text="%modpack.wizard.step.initialization.include_launcher"/>
</left>
<right>
<JFXToggleButton fx:id="chkIncludeLauncher" size="7" maxHeight="10.0" minHeight="10.0"/>
</right>
</BorderPane>
</ComponentList>
</center>
<bottom>
<HBox alignment="CENTER_RIGHT">
<JFXButton fx:id="btnNext" onMouseClicked="#onNext" prefWidth="100" prefHeight="40" buttonType="RAISED"
text="%wizard.next" styleClass="jfx-button-raised"/>
</HBox>
</bottom>
</BorderPane>
</ScrollPane>
</fx:root>
@@ -17,7 +17,7 @@
<bottom>
<HBox alignment="CENTER_RIGHT" style="-fx-padding: 16 16 16 0;">
<JFXButton fx:id="btnNext" onMouseClicked="#onNext" prefWidth="100" prefHeight="40" buttonType="RAISED"
text="%wizard.finish" styleClass="jfx-button-raised"/>
text="%wizard.next" styleClass="jfx-button-raised"/>
</HBox>
</bottom>
</BorderPane>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.TwoLineListItem?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<fx:define>
<Insets fx:id="insets" topRightBottomLeft="8" />
</fx:define>
<VBox fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300">
<Label padding="${insets}" text="%modpack.export.as" />
<JFXButton fx:id="btnHMCL" prefWidth="${list.width}">
<graphic>
<BorderPane mouseTransparent="true">
<left>
<TwoLineListItem title="%modpack.type.hmcl" subtitle="%modpack.type.hmcl.export" />
</left>
<right>
<fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/>
</right>
</BorderPane>
</graphic>
</JFXButton>
<JFXButton fx:id="btnMultiMC" prefWidth="${list.width}">
<graphic>
<BorderPane mouseTransparent="true">
<left>
<TwoLineListItem title="%modpack.type.multimc" subtitle="%modpack.type.multimc.export" />
</left>
<right>
<fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/>
</right>
</BorderPane>
</graphic>
</JFXButton>
<JFXButton fx:id="btnServer" prefWidth="${list.width}">
<graphic>
<BorderPane mouseTransparent="true">
<left>
<TwoLineListItem title="%modpack.type.server" subtitle="%modpack.type.server.export" />
</left>
<right>
<fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/>
</right>
</BorderPane>
</graphic>
</JFXButton>
</VBox>
</fx:root>
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.*?>
<?import javafx.collections.FXCollections?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.Image?>
Loaded 100 of 222 files, more files were not shown because too many files have changed in this diff. Show more