Compare commits

..
18 Commits
Author SHA1 Message Date
huanghongxun bc3ae5cc7d Add link to hmcl documentation page 2018-09-16 14:12:52 +08:00
huanghongxun 9752dea0e1 Merge pull request #454 from yushijinhun/javafx
重构
2018-09-16 13:46:31 +08:00
huanghongxun 9afdc99157 Support single datapack zip 2018-09-16 13:15:54 +08:00
huanghongxun 07febc18d4 World and datapacks management 2018-09-16 12:38:15 +08:00
yushijinhun 68fdfb36fd Add selectedItemPropertyFor(ComboBox) 2018-09-16 00:52:03 +08:00
yushijinhun 09287f9c72 Rename WeakListenerHelper to WeakListenerHolder 2018-09-15 23:24:42 +08:00
yushijinhun 4a5ac1b899 Remove Settings.logLines 2018-09-15 23:18:59 +08:00
yushijinhun afad520669 Move ProxyManager.proxyProperty initialization to init() 2018-09-15 23:13:25 +08:00
yushijinhun 9950bef260 Change type of downloadType to string 2018-09-15 23:05:37 +08:00
Yuhui Huang 1753b4d27e Datapack resolving 2018-09-14 15:44:40 +08:00
Yellow Fish 2bfc85b18f Fixed NPE mentioned in #451 2018-09-13 01:13:20 +08:00
Yellow Fish 24f3dec453 Fixed #450 2018-09-12 21:37:27 +08:00
huangyuhui 4e6237c062 Fixed #448 2018-09-11 19:26:36 +08:00
huangyuhui 5cbde9cc33 Fix not on FX user thread 2018-09-09 19:38:14 +08:00
huangyuhui 25d79a94f8 Move specialize and globalize VersionSetting to HMCLGameRepository 2018-09-09 18:55:01 +08:00
huangyuhui a5997be9c4 Fix crash when selectedVersion set to empty string 2018-09-09 18:35:10 +08:00
huangyuhui c679667079 Fix crash when the game version is too long 2018-09-09 18:24:05 +08:00
huangyuhui 6b311c679d Fixed #446 2018-09-08 22:14:06 +08:00
57 changed files with 1386 additions and 264 deletions

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ import org.tukaani.xz.XZOutputStream
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
def versionroot = System.getenv("VERSION_ROOT") ?: "3.1"
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
version = versionroot + '.' + buildnumber
dependencies {
@@ -21,12 +21,14 @@ import com.jfoenix.concurrency.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.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URISyntaxException;
import java.net.URL;
@@ -37,6 +39,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Launcher extends Application {
@@ -44,6 +47,12 @@ public final class Launcher extends Application {
public void start(Stage primaryStage) {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
try {
ConfigHolder.init();
} catch (IOException e) {
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
try {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
@@ -49,12 +49,6 @@ public final class Main {
return;
}
try {
ConfigHolder.init();
} catch (IOException e) {
showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
Launcher.main(args);
}
@@ -100,7 +94,7 @@ public final class Main {
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
private static void showErrorAndExit(String message) {
static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
@@ -110,7 +104,7 @@ public final class Main {
/**
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
*/
private static void showWarningAndContinue(String message) {
static void showWarningAndContinue(String message) {
System.err.println(message);
System.err.println("Potential issues have been detected.");
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
@@ -31,5 +31,6 @@ 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 PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
}
@@ -164,6 +164,27 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
}
/**
* Make version use self version settings instead of the global one.
* @param id the version id.
* @return specialized version setting, null if given version does not exist.
*/
public VersionSetting specializeVersionSetting(String id) {
VersionSetting vs = getVersionSetting(id);
if (vs == null)
vs = createVersionSetting(id);
if (vs == null)
return null;
vs.setUsesGlobal(false);
return vs;
}
public void globalizeVersionSetting(String id) {
VersionSetting vs = getVersionSetting(id);
if (vs != null)
vs.setUsesGlobal(true);
}
public boolean forbidsVersion(String id) {
return FORBIDDEN.contains(id);
}
@@ -32,7 +32,6 @@ import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
@@ -50,6 +49,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -443,7 +443,7 @@ public final class LauncherHelper {
System.out.print(log);
logs.add(pair(log, level));
if (logs.size() > Settings.instance().getLogLines())
if (logs.size() > config().getLogLines())
logs.removeFirst();
if (setting.isShowLogs()) {
@@ -87,9 +87,10 @@ public final class ModpackHelper {
FinalizedCallback finalizeTask = (variables, isDependentsSucceeded) -> {
if (isDependentsSucceeded) {
profile.getRepository().refreshVersions();
VersionSetting vs = profile.specializeVersionSetting(name);
profile.getRepository().undoMark(name);
HMCLGameRepository repository = profile.getRepository();
repository.refreshVersions();
VersionSetting vs = repository.specializeVersionSetting(name);
repository.undoMark(name);
if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
}
@@ -44,7 +44,7 @@ public final class MultiMCInstallVersionSettingTask extends Task {
@Override
public void execute() {
VersionSetting vs = Objects.requireNonNull(profile.specializeVersionSetting(version));
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
}
}
@@ -59,7 +59,7 @@ public final class Accounts {
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.instance().getDownloadProvider())::getArtifactInfo,
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), DownloadProviders::getDownloadProvider)::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer);
private static final String TYPE_OFFLINE = "offline";
@@ -115,8 +115,8 @@ public final class Config implements Cloneable, Observable {
@SerializedName("localization")
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
@SerializedName("downloadtype")
private IntegerProperty downloadType = new SimpleIntegerProperty(1);
@SerializedName("downloadType")
private StringProperty downloadType = new SimpleStringProperty("bmclapi");
@SerializedName("configurations")
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@@ -359,15 +359,15 @@ public final class Config implements Cloneable, Observable {
return localization;
}
public int getDownloadType() {
public String getDownloadType() {
return downloadType.get();
}
public void setDownloadType(int downloadType) {
public void setDownloadType(String downloadType) {
this.downloadType.set(downloadType);
}
public IntegerProperty downloadTypeProperty() {
public StringProperty downloadTypeProperty() {
return downloadType;
}
@@ -86,5 +86,17 @@ final class ConfigUpgrader {
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
if (!rawJson.containsKey("hasProxyAuth"))
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
if (!rawJson.containsKey("downloadType")) {
tryCast(rawJson.get("downloadtype"), Number.class)
.map(Number::intValue)
.ifPresent(id -> {
if (id == 0) {
deserialized.setDownloadType("mojang");
} else if (id == 1) {
deserialized.setDownloadType("bmclapi");
}
});
}
}
}
@@ -17,20 +17,44 @@
*/
package org.jackhuang.hmcl.setting;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import java.util.Map;
import java.util.Optional;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.CurseCDNDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MojangDownloadProvider;
import org.jackhuang.hmcl.util.Lang;
import java.util.List;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
public final class DownloadProviders {
private DownloadProviders() {}
public static final List<DownloadProvider> DOWNLOAD_PROVIDERS = Lang.immutableListOf(new MojangDownloadProvider(), BMCLAPIDownloadProvider.INSTANCE, CurseCDNDownloadProvider.INSTANCE);
public static final Map<String, DownloadProvider> providersById = mapOf(
pair("mojang", new MojangDownloadProvider()),
pair("bmclapi", new BMCLAPIDownloadProvider()));
public static DownloadProvider getDownloadProvider(int index) {
return Lang.get(DOWNLOAD_PROVIDERS, index).orElse(DOWNLOAD_PROVIDERS.get(0));
public static final String DEFAULT_PROVIDER_ID = "bmclapi";
private static ObjectBinding<DownloadProvider> downloadProviderProperty;
static void init() {
downloadProviderProperty = Bindings.createObjectBinding(
() -> Optional.ofNullable(providersById.get(config().getDownloadType()))
.orElse(providersById.get(DEFAULT_PROVIDER_ID)),
config().downloadTypeProperty());
}
public static DownloadProvider getDownloadProvider() {
return downloadProviderProperty.get();
}
public static ObservableObjectValue<DownloadProvider> downloadProviderProperty() {
return downloadProviderProperty;
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
@@ -28,7 +29,7 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.ui.WeakListenerHelper;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.util.*;
import java.io.File;
@@ -42,7 +43,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
* @author huangyuhui
*/
public final class Profile implements Observable {
private final WeakListenerHelper helper = new WeakListenerHelper();
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private final HMCLGameRepository repository;
private final ModManager modManager;
@@ -135,7 +136,7 @@ public final class Profile implements Observable {
gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
this.selectedVersion.addListener(o -> checkSelectedVersion());
helper.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion()));
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion()));
addPropertyChangedListener(onInvalidating(this::invalidate));
}
@@ -147,7 +148,7 @@ public final class Profile implements Observable {
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
if (version.isPresent())
selectedVersion.setValue(version.get());
else if (StringUtils.isNotBlank(newValue))
else if (newValue != null)
selectedVersion.setValue(null);
}
}
@@ -161,7 +162,7 @@ public final class Profile implements Observable {
}
public DefaultDependencyManager getDependency() {
return new DefaultDependencyManager(repository, Settings.instance().getDownloadProvider(), HMCLCacheRepository.REPOSITORY);
return new DefaultDependencyManager(repository, DownloadProviders.getDownloadProvider(), HMCLCacheRepository.REPOSITORY);
}
public VersionSetting getVersionSetting(String id) {
@@ -174,32 +175,6 @@ public final class Profile implements Observable {
return vs;
}
public boolean isVersionGlobal(String id) {
VersionSetting vs = repository.getVersionSetting(id);
return vs == null || vs.isUsesGlobal();
}
/**
* Make version use self version settings instead of the global one.
* @param id the version id.
* @return specialized version setting, null if given version does not exist.
*/
public VersionSetting specializeVersionSetting(String id) {
VersionSetting vs = repository.getVersionSetting(id);
if (vs == null)
vs = repository.createVersionSetting(id);
if (vs == null)
return null;
vs.setUsesGlobal(false);
return vs;
}
public void globalizeVersionSetting(String id) {
VersionSetting vs = repository.getVersionSetting(id);
if (vs != null)
vs.setUsesGlobal(true);
}
@Override
public String toString() {
return new ToStringBuilder(this)
@@ -230,8 +205,8 @@ public final class Profile implements Observable {
observableHelper.removeListener(listener);
}
protected void invalidate() {
observableHelper.invalidate();
private void invalidate() {
Platform.runLater(observableHelper::invalidate);
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
@@ -87,7 +88,7 @@ public final class Profiles {
if (profiles.isEmpty()) {
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true);
Profile home = new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY);
profiles.addAll(current, home);
Platform.runLater(() -> profiles.addAll(current, home));
}
}
@@ -35,20 +35,7 @@ public final class ProxyManager {
private ProxyManager() {
}
private static ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
() -> {
String host = config().getProxyHost();
Integer port = Lang.toIntOrNull(config().getProxyPort());
if (!config().hasProxy() || StringUtils.isBlank(host) || port == null || config().getProxyType() == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY;
} else {
return new Proxy(config().getProxyType(), new InetSocketAddress(host, port));
}
},
config().proxyTypeProperty(),
config().proxyHostProperty(),
config().proxyPortProperty(),
config().hasProxyProperty());
private static ObjectBinding<Proxy> proxyProperty;
public static Proxy getProxy() {
return proxyProperty.get();
@@ -59,12 +46,25 @@ public final class ProxyManager {
}
static void init() {
proxyProperty.addListener(observable -> updateSystemProxy());
proxyProperty = Bindings.createObjectBinding(
() -> {
String host = config().getProxyHost();
Integer port = Lang.toIntOrNull(config().getProxyPort());
if (!config().hasProxy() || StringUtils.isBlank(host) || port == null || config().getProxyType() == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY;
} else {
return new Proxy(config().getProxyType(), new InetSocketAddress(host, port));
}
},
config().proxyTypeProperty(),
config().proxyHostProperty(),
config().proxyPortProperty(),
config().hasProxyProperty());
proxyProperty.addListener(any -> updateSystemProxy());
updateSystemProxy();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (config().hasProxyAuth()) {
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.setting;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
@@ -44,6 +43,7 @@ public class Settings {
}
private Settings() {
DownloadProviders.init();
ProxyManager.init();
Accounts.init();
Profiles.init();
@@ -61,14 +61,6 @@ public class Settings {
config().setFontSize(font.getSize());
}
public int getLogLines() {
return Math.max(config().getLogLines(), 100);
}
public void setLogLines(int logLines) {
config().setLogLines(logLines);
}
public static String getDefaultCommonDirectory() {
return Launcher.MINECRAFT_DIRECTORY.getAbsolutePath();
}
@@ -83,19 +75,4 @@ public class Settings {
return null;
}
}
/****************************************
* DOWNLOAD PROVIDERS *
****************************************/
public DownloadProvider getDownloadProvider() {
return DownloadProviders.getDownloadProvider(config().getDownloadType());
}
public void setDownloadProvider(DownloadProvider downloadProvider) {
int index = DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(downloadProvider);
if (index == -1)
throw new IllegalArgumentException("Unknown download provider: " + downloadProvider);
config().setDownloadType(index);
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
@@ -55,6 +56,7 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public final class FXUtils {
@@ -209,27 +211,29 @@ public final class FXUtils {
}
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
try {
// Java 8
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
behaviorConstructor.setAccessible(true);
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installMethod.setAccessible(true);
installMethod.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
JFXUtilities.runInFX(() -> {
try {
// Java 9
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
} catch (ReflectiveOperationException e2) {
e.addSuppressed(e2);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
// Java 8
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
behaviorConstructor.setAccessible(true);
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installMethod.setAccessible(true);
installMethod.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
try {
// Java 9
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
} catch (ReflectiveOperationException e2) {
e.addSuppressed(e2);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
}
Tooltip.install(node, tooltip);
}
Tooltip.install(node, tooltip);
}
});
}
public static void openFolder(File file) {
@@ -249,11 +253,15 @@ public final class FXUtils {
}
break;
default:
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
thread(() -> {
if (java.awt.Desktop.isDesktopSupported()) {
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
}
});
}
}
@@ -265,17 +273,21 @@ public final class FXUtils {
public static void openLink(String link) {
if (link == null)
return;
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
thread(() -> {
if (java.awt.Desktop.isDesktopSupported()) {
try {
Runtime.getRuntime().exec(new String[] { "/usr/bin/open", link });
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
}
}
});
}
public static void bindInt(JFXTextField textField, Property<Number> property) {
@@ -316,8 +328,10 @@ public final class FXUtils {
* @param comboBox the combo box being bound with {@code property}.
* @param property the property being bound with {@code combo box}.
* @see #unbindEnum(JFXComboBox)
* @deprecated Use {@link SelectionModelSelectedItemProperty#selectedItemPropertyFor(ComboBox)}
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum> property) {
unbindEnum(comboBox);
ChangeListener<Number> listener = (a, b, newValue) ->
@@ -334,6 +348,7 @@ public final class FXUtils {
* @see #bindEnum(JFXComboBox, Property)
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void unbindEnum(JFXComboBox<?> comboBox) {
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
if (listener == null) return;
@@ -181,7 +181,7 @@ public final class LogWindow extends Stage {
if (newValue == Worker.State.SUCCEEDED) {
document = engine.getDocument();
body = document.getElementsByTagName("body").item(0);
engine.executeScript("limitedLogs=" + Settings.instance().getLogLines());
engine.executeScript("limitedLogs=" + config().getLogLines());
latch.countDown();
onDone.fireEvent(new Event(LogWindow.this));
}
@@ -189,14 +189,14 @@ public final class LogWindow extends Stage {
boolean flag = false;
for (String i : cboLines.getItems())
if (Integer.toString(Settings.instance().getLogLines()).equals(i)) {
if (Integer.toString(config().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
flag = true;
}
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
Settings.instance().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
engine.executeScript("limitedLogs=" + Settings.instance().getLogLines());
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
engine.executeScript("limitedLogs=" + config().getLogLines());
});
if (!flag)
@@ -146,4 +146,8 @@ public final class SVG {
public static Node export(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z", fill, width, height);
}
public static Node openInNew(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
}
@@ -25,12 +25,10 @@ import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -48,6 +46,7 @@ import java.util.Objects;
import java.util.Optional;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.SelectionModelSelectedItemProperty.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class SettingsPage extends SettingsView implements DecoratorPage {
@@ -60,10 +59,12 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
public SettingsPage() {
FXUtils.smoothScrolling(scroll);
chkEnableGameList.selectedProperty().bindBidirectional(config().enableMainPageGameListProperty());
// ==== Download sources ====
cboDownloadSource.getItems().setAll(DownloadProviders.providersById.keySet());
selectedItemPropertyFor(cboDownloadSource).bindBidirectional(config().downloadTypeProperty());
// ====
cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.instance().getDownloadProvider()));
cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.instance().setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue())));
chkEnableGameList.selectedProperty().bindBidirectional(config().enableMainPageGameListProperty());
cboFont.initValue(Settings.instance().getFont());
cboFont.valueProperty().addListener((a, b, newValue) -> {
@@ -84,13 +85,10 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
lblDisplay.setStyle("-fx-font: " + Settings.instance().getFont().getSize() + " \"" + Settings.instance().getFont().getFamily() + "\";");
ObservableList<Label> list = FXCollections.observableArrayList();
for (Locales.SupportedLocale locale : Locales.LOCALES)
list.add(new Label(locale.getName(config().getLocalization().getResourceBundle())));
cboLanguage.setItems(list);
cboLanguage.getSelectionModel().select(Locales.LOCALES.indexOf(config().getLocalization()));
cboLanguage.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> config().setLocalization(Locales.getLocale(newValue.intValue())));
// ==== Languages ====
cboLanguage.getItems().setAll(Locales.LOCALES);
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
// ====
// ==== Proxy ====
txtProxyHost.textProperty().bindBidirectional(config().proxyHostProperty());
@@ -250,4 +248,9 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
}
UpdateHandler.updateFrom(target);
}
@Override
protected void onHelp() {
FXUtils.openLink(Metadata.HELP_URL);
}
}
@@ -26,11 +26,17 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
public abstract class SettingsView extends StackPane {
protected final JFXTextField txtProxyHost;
@@ -38,8 +44,8 @@ public abstract class SettingsView extends StackPane {
protected final JFXTextField txtProxyUsername;
protected final JFXPasswordField txtProxyPassword;
protected final JFXTextField txtFontSize;
protected final JFXComboBox<Label> cboLanguage;
protected final JFXComboBox<Label> cboDownloadSource;
protected final JFXComboBox<SupportedLocale> cboLanguage;
protected final JFXComboBox<String> cboDownloadSource;
protected final FontComboBox cboFont;
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
protected final Label lblDisplay;
@@ -73,12 +79,12 @@ public abstract class SettingsView extends StackPane {
ComponentList settingsPane = new ComponentList();
{
ComponentSublist updatePane = new ComponentSublist();
updatePane.setTitle(I18n.i18n("update"));
updatePane.setTitle(i18n("update"));
updatePane.setHasSubtitle(true);
{
VBox headerLeft = new VBox();
lblUpdate = new Label(I18n.i18n("update"));
lblUpdate = new Label(i18n("update"));
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().setAll("subtitle-label");
@@ -99,12 +105,12 @@ public abstract class SettingsView extends StackPane {
VBox content = new VBox();
content.setSpacing(8);
chkUpdateStable = new JFXRadioButton(I18n.i18n("update.channel.stable"));
chkUpdateDev = new JFXRadioButton(I18n.i18n("update.channel.dev"));
chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
VBox noteWrapper = new VBox();
noteWrapper.setStyle("-fx-padding: 10 0 0 0;");
lblUpdateNote = new Text(I18n.i18n("update.note"));
lblUpdateNote = new Text(i18n("update.note"));
noteWrapper.getChildren().setAll(lblUpdateNote);
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
@@ -114,11 +120,35 @@ public abstract class SettingsView extends StackPane {
settingsPane.getContent().add(updatePane);
}
{
BorderPane updatePane = new BorderPane();
{
VBox headerLeft = new VBox();
Label help = new Label(i18n("help"));
Label helpSubtitle = new Label(i18n("help.detail"));
helpSubtitle.getStyleClass().setAll("subtitle-label");
headerLeft.getChildren().setAll(help, helpSubtitle);
updatePane.setLeft(headerLeft);
}
{
JFXButton btnExternal = new JFXButton();
btnExternal.setOnMouseClicked(e -> onHelp());
btnExternal.getStyleClass().setAll("toggle-icon4");
btnExternal.setGraphic(SVG.openInNew(Theme.blackFillBinding(), -1, -1));
updatePane.setRight(btnExternal);
}
settingsPane.getContent().add(updatePane);
}
{
fileCommonLocation = new MultiFileItem<>(true);
fileCommonLocation.setTitle(I18n.i18n("launcher.common_directory"));
fileCommonLocation.setTitle(i18n("launcher.common_directory"));
fileCommonLocation.setDirectory(true);
fileCommonLocation.setChooserTitle(I18n.i18n("launcher.common_directory.choose"));
fileCommonLocation.setChooserTitle(i18n("launcher.common_directory.choose"));
fileCommonLocation.setHasSubtitle(true);
fileCommonLocation.setCustomText("settings.custom");
@@ -127,10 +157,10 @@ public abstract class SettingsView extends StackPane {
{
backgroundItem = new MultiFileItem<>(true);
backgroundItem.setTitle(I18n.i18n("launcher.background"));
backgroundItem.setChooserTitle(I18n.i18n("launcher.background.choose"));
backgroundItem.setTitle(i18n("launcher.background"));
backgroundItem.setChooserTitle(i18n("launcher.background.choose"));
backgroundItem.setHasSubtitle(true);
backgroundItem.setCustomText(I18n.i18n("settings.custom"));
backgroundItem.setCustomText(i18n("settings.custom"));
settingsPane.getContent().add(backgroundItem);
}
@@ -138,7 +168,7 @@ public abstract class SettingsView extends StackPane {
{
BorderPane borderPane = new BorderPane();
Label left = new Label(I18n.i18n("settings.launcher.enable_game_list"));
Label left = new Label(i18n("settings.launcher.enable_game_list"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
borderPane.setLeft(left);
@@ -155,18 +185,14 @@ public abstract class SettingsView extends StackPane {
{
BorderPane downloadSourcePane = new BorderPane();
{
Label label = new Label(I18n.i18n("settings.launcher.download_source"));
Label label = new Label(i18n("settings.launcher.download_source"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
downloadSourcePane.setLeft(label);
}
{
cboDownloadSource = new JFXComboBox<>();
FXUtils.setLimitWidth(cboDownloadSource, 400);
cboDownloadSource.getItems().setAll(
new Label(I18n.i18n("download.mojang")),
new Label(I18n.i18n("download.BMCL"))
);
cboDownloadSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
downloadSourcePane.setRight(cboDownloadSource);
}
settingsPane.getContent().add(downloadSourcePane);
@@ -175,11 +201,12 @@ public abstract class SettingsView extends StackPane {
{
BorderPane languagePane = new BorderPane();
Label left = new Label(I18n.i18n("settings.launcher.language"));
Label left = new Label(i18n("settings.launcher.language"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
languagePane.setLeft(left);
cboLanguage = new JFXComboBox<>();
cboLanguage.setConverter(stringConverter(locale -> locale.getName(config().getLocalization().getResourceBundle())));
FXUtils.setLimitWidth(cboLanguage, 400);
languagePane.setRight(cboLanguage);
@@ -188,13 +215,13 @@ public abstract class SettingsView extends StackPane {
{
ComponentList proxyList = new ComponentList();
proxyList.setTitle(I18n.i18n("settings.launcher.proxy"));
proxyList.setTitle(i18n("settings.launcher.proxy"));
VBox proxyWrapper = new VBox();
proxyWrapper.setSpacing(10);
{
chkDisableProxy = new JFXCheckBox(I18n.i18n("settings.launcher.proxy.disable"));
chkDisableProxy = new JFXCheckBox(i18n("settings.launcher.proxy.disable"));
proxyWrapper.getChildren().add(chkDisableProxy);
}
@@ -207,8 +234,8 @@ public abstract class SettingsView extends StackPane {
{
HBox hBox = new HBox();
chkProxyHttp = new JFXRadioButton(I18n.i18n("settings.launcher.proxy.http"));
chkProxySocks = new JFXRadioButton(I18n.i18n("settings.launcher.proxy.socks"));
chkProxyHttp = new JFXRadioButton(i18n("settings.launcher.proxy.http"));
chkProxySocks = new JFXRadioButton(i18n("settings.launcher.proxy.socks"));
hBox.getChildren().setAll(chkProxyHttp, chkProxySocks);
proxyPane.getChildren().add(hBox);
}
@@ -222,7 +249,7 @@ public abstract class SettingsView extends StackPane {
gridPane.getRowConstraints().setAll(new RowConstraints(), new RowConstraints());
{
Label host = new Label(I18n.i18n("settings.launcher.proxy.host"));
Label host = new Label(i18n("settings.launcher.proxy.host"));
GridPane.setRowIndex(host, 1);
GridPane.setColumnIndex(host, 0);
GridPane.setHalignment(host, HPos.RIGHT);
@@ -238,7 +265,7 @@ public abstract class SettingsView extends StackPane {
}
{
Label port = new Label(I18n.i18n("settings.launcher.proxy.port"));
Label port = new Label(i18n("settings.launcher.proxy.port"));
GridPane.setRowIndex(port, 2);
GridPane.setColumnIndex(port, 0);
GridPane.setHalignment(port, HPos.RIGHT);
@@ -251,7 +278,7 @@ public abstract class SettingsView extends StackPane {
GridPane.setRowIndex(txtProxyPort, 2);
GridPane.setColumnIndex(txtProxyPort, 1);
FXUtils.setValidateWhileTextChanged(txtProxyPort, true);
txtProxyHost.getValidators().setAll(new NumberValidator(I18n.i18n("input.number"), false));
txtProxyHost.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
gridPane.getChildren().add(txtProxyPort);
}
proxyPane.getChildren().add(gridPane);
@@ -261,7 +288,7 @@ public abstract class SettingsView extends StackPane {
VBox vBox = new VBox();
vBox.setStyle("-fx-padding: 20 0 20 5;");
chkProxyAuthentication = new JFXCheckBox(I18n.i18n("settings.launcher.proxy.authentication"));
chkProxyAuthentication = new JFXCheckBox(i18n("settings.launcher.proxy.authentication"));
vBox.getChildren().setAll(chkProxyAuthentication);
proxyPane.getChildren().add(vBox);
@@ -276,7 +303,7 @@ public abstract class SettingsView extends StackPane {
authPane.getRowConstraints().setAll(new RowConstraints(), new RowConstraints());
{
Label username = new Label(I18n.i18n("settings.launcher.proxy.username"));
Label username = new Label(i18n("settings.launcher.proxy.username"));
GridPane.setRowIndex(username, 0);
GridPane.setColumnIndex(username, 0);
authPane.getChildren().add(username);
@@ -291,7 +318,7 @@ public abstract class SettingsView extends StackPane {
}
{
Label password = new Label(I18n.i18n("settings.launcher.proxy.password"));
Label password = new Label(i18n("settings.launcher.proxy.password"));
GridPane.setRowIndex(password, 1);
GridPane.setColumnIndex(password, 0);
authPane.getChildren().add(password);
@@ -316,7 +343,7 @@ public abstract class SettingsView extends StackPane {
{
BorderPane themePane = new BorderPane();
Label left = new Label(I18n.i18n("settings.launcher.theme"));
Label left = new Label(i18n("settings.launcher.theme"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
themePane.setLeft(left);
@@ -336,7 +363,7 @@ public abstract class SettingsView extends StackPane {
BorderPane borderPane = new BorderPane();
fontPane.getChildren().add(borderPane);
{
Label left = new Label(I18n.i18n("settings.launcher.log_font"));
Label left = new Label(i18n("settings.launcher.log_font"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
borderPane.setLeft(left);
}
@@ -383,65 +410,65 @@ public abstract class SettingsView extends StackPane {
gridPane.getRowConstraints().setAll(row, row, row, row, row, row);
{
Label label = new Label(I18n.i18n("about.copyright"));
Label label = new Label(i18n("about.copyright"));
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.copyright.statement"));
Label label = new Label(i18n("about.copyright.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.author"));
Label label = new Label(i18n("about.author"));
GridPane.setRowIndex(label, 1);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.author.statement"));
Label label = new Label(i18n("about.author.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 1);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.thanks_to"));
Label label = new Label(i18n("about.thanks_to"));
GridPane.setRowIndex(label, 2);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.thanks_to.statement"));
Label label = new Label(i18n("about.thanks_to.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 2);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.dependency"));
Label label = new Label(i18n("about.dependency"));
GridPane.setRowIndex(label, 3);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.dependency.statement"));
Label label = new Label(i18n("about.dependency.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 3);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.claim"));
Label label = new Label(i18n("about.claim"));
GridPane.setRowIndex(label, 4);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.claim.statement"));
Label label = new Label(i18n("about.claim.statement"));
label.setWrapText(true);
label.setTextAlignment(TextAlignment.JUSTIFY);
GridPane.setRowIndex(label, 4);
@@ -449,13 +476,13 @@ public abstract class SettingsView extends StackPane {
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.open_source"));
Label label = new Label(i18n("about.open_source"));
GridPane.setRowIndex(label, 5);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(I18n.i18n("about.open_source.statement"));
Label label = new Label(i18n("about.open_source.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 5);
GridPane.setColumnIndex(label, 1);
@@ -471,4 +498,5 @@ public abstract class SettingsView extends StackPane {
}
protected abstract void onUpdate();
protected abstract void onHelp();
}
@@ -27,10 +27,10 @@ import javafx.collections.WeakListChangeListener;
import java.util.LinkedList;
import java.util.List;
public class WeakListenerHelper {
List<Object> refs = new LinkedList<>();
public class WeakListenerHolder {
private List<Object> refs = new LinkedList<>();
public WeakListenerHelper() {
public WeakListenerHolder() {
}
public WeakInvalidationListener weak(InvalidationListener listener) {
@@ -100,13 +100,15 @@ public class ComponentList extends Control {
}
protected static class Skin extends SkinBase<ComponentList> {
private final ObservableList<Node> list;
protected Skin(ComponentList control) {
super(control);
list = MappedObservableList.create(control.getContent(), this::mapper);
VBox vbox = new VBox();
Bindings.bindContent(vbox.getChildren(),
MappedObservableList.create(control.getContent(), this::mapper));
Bindings.bindContent(vbox.getChildren(), list);
getChildren().setAll(vbox);
}
@@ -0,0 +1,43 @@
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.installTooltip(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());
}
}
@@ -0,0 +1,103 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.scene.input.TransferMode;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.MappedObservableList;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListPage extends ListPage<DatapackListItem> implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty();
private final ObservableList<DatapackListItem> list; // Hold weak references
private final Path worldDir;
private final Datapack datapack;
public DatapackListPage(String worldName, Path worldDir) {
this.worldDir = worldDir;
title.set(i18n("datapack.title", worldName));
datapack = new Datapack(worldDir.resolve("datapacks"));
datapack.loadFromDir();
list = MappedObservableList.create(datapack.getInfo(), pack -> new DatapackListItem(pack, item -> {
try {
datapack.deletePack(pack.getId());
} catch (IOException e) {
Logging.LOG.warning("Failed to delete datapack");
}
}));
itemsProperty().bindContent(list);
setOnDragOver(event -> {
if (event.getGestureSource() != this && event.getDragboard().hasFiles())
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
event.consume();
});
setOnDragDropped(event -> {
List<File> files = event.getDragboard().getFiles();
if (files != null) {
Collection<File> mods = files.stream()
.filter(it -> Objects.equals("zip", FileUtils.getExtension(it)))
.collect(Collectors.toList());
if (!mods.isEmpty()) {
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);
}
});
event.setDropCompleted(true);
}
}
datapack.loadFromDir();
event.consume();
});
}
@Override
public StringProperty titleProperty() {
return title;
}
@Override
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.choose_datapack"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip"));
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);
}
});
datapack.loadFromDir();
}
}
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.WeakListenerHelper;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import java.io.File;
@@ -32,16 +32,16 @@ import java.io.File;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final WeakListenerHelper helper = new WeakListenerHelper();
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private Profile profile;
private InvalidationListener listener = o -> loadVersion();
public GameAdvancedListItem() {
Profiles.selectedProfileProperty().addListener(helper.weak((a, b, newValue) -> {
Profiles.selectedProfileProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
JFXUtilities.runInFX(() -> loadProfile(newValue));
}));
helper.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
JFXUtilities.runInFX(() -> {
if (profile != null && profile.getRepository() == event.getSource())
loadVersion();
@@ -31,7 +31,7 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.WeakListenerHelper;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
import org.jackhuang.hmcl.util.VersionNumber;
@@ -70,8 +70,8 @@ public class GameList extends Control implements DecoratorPage {
private void loadVersions(HMCLGameRepository repository) {
toggleGroup = new ToggleGroup();
WeakListenerHelper helper = new WeakListenerHelper();
toggleGroup.getProperties().put("ReferenceHolder", helper);
WeakListenerHolder listenerHolder = new WeakListenerHolder();
toggleGroup.getProperties().put("ReferenceHolder", listenerHolder);
List<GameListItem> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId())))
@@ -83,7 +83,7 @@ public class GameList extends Control implements DecoratorPage {
items.setAll(children);
children.forEach(GameListItem::checkSelection);
profile.selectedVersionProperty().addListener(helper.weak((a, b, newValue) -> {
profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
Platform.runLater(() -> {
children.forEach(it -> it.selectedProperty().set(false));
children.stream()
@@ -59,7 +59,7 @@ public class GameListItem extends Control {
title.set(id);
subtitle.set(libraries.toString());
selected.set(profile.getSelectedVersion().equals(id));
selected.set(id.equals(profile.getSelectedVersion()));
File iconFile = profile.getRepository().getVersionIcon(version);
if (iconFile.exists())
@@ -92,7 +92,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
};
menu.getChildren().setAll(
new IconedMenuItem(limitWidth.apply(SVG.gear(Theme.blackFillBinding(), 14, 14)), i18n("settings"), wrap.apply(skinnable::modifyGameSettings)),
new IconedMenuItem(limitWidth.apply(SVG.gear(Theme.blackFillBinding(), 14, 14)), i18n("version.manage.manage"), wrap.apply(skinnable::modifyGameSettings)),
new IconedMenuItem(limitWidth.apply(SVG.pencil(Theme.blackFillBinding(), 14, 14)), i18n("version.manage.rename"), wrap.apply(skinnable::rename)),
new IconedMenuItem(limitWidth.apply(SVG.delete(Theme.blackFillBinding(), 14, 14)), i18n("version.manage.remove"), wrap.apply(skinnable::remove)),
new IconedMenuItem(limitWidth.apply(SVG.export(Theme.blackFillBinding(), 14, 14)), i18n("modpack.export"), wrap.apply(skinnable::export)),
@@ -60,6 +60,7 @@ public class InstallerListPage extends ListPage<InstallerItem> {
.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))));
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
@@ -25,6 +25,8 @@ import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.StringUtils;
@@ -28,7 +28,6 @@ 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.ModItem;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
@@ -48,6 +48,8 @@ public final class VersionPage extends StackPane implements DecoratorPage {
@FXML
private InstallerListPage installer;
@FXML
private WorldListPage world;
@FXML
private JFXListView<?> browseList;
@FXML
private JFXListView<?> managementList;
@@ -90,13 +92,14 @@ public final class VersionPage extends StackPane implements DecoratorPage {
this.version = id;
this.profile = profile;
title.set(i18n("settings.game") + " - " + id);
title.set(i18n("version.manage.manage") + " - " + id);
versionSettings.loadVersionSetting(profile, id);
mod.setParentTab(tabPane);
modTab.setUserData(mod);
mod.loadMods(profile.getModManager(), id);
installer.loadVersion(profile, id);
world.loadVersion(profile, id);
}
@FXML
@@ -134,9 +134,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
// because versionSettings can be the global one.
// global versionSettings.usesGlobal is always true.
if (newValue)
profile.specializeVersionSetting(versionId);
profile.getRepository().specializeVersionSetting(versionId);
else
profile.globalizeVersionSetting(versionId);
profile.getRepository().globalizeVersionSetting(versionId);
Platform.runLater(() -> loadVersionSetting(profile, versionId));
});
@@ -0,0 +1,62 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Skin;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardSinglePage;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldExportPage extends WizardSinglePage {
private final StringProperty path = new SimpleStringProperty();
private final StringProperty gameVersion = new SimpleStringProperty();
private final StringProperty worldName = new SimpleStringProperty();
private final World world;
public WorldExportPage(World world, Path export, Runnable onFinish) {
super(onFinish);
this.world = world;
path.set(export.toString());
gameVersion.set(world.getGameVersion());
worldName.set(world.getWorldName());
}
@Override
protected Skin<?> createDefaultSkin() {
return new WorldExportPageSkin(this);
}
public StringProperty pathProperty() {
return path;
}
public StringProperty gameVersionProperty() {
return gameVersion;
}
public StringProperty worldNameProperty() {
return worldName;
}
public void export() {
onFinish.run();
}
@Override
public String getTitle() {
return i18n("world.export.wizard", world.getFileName());
}
@Override
protected Object finish() {
return Task.of(i18n("world.export.wizard", worldName.get()), () -> world.export(Paths.get(path.get()), worldName.get()));
}
}
@@ -0,0 +1,81 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
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.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.FileItem;
import java.nio.file.Files;
import java.nio.file.Paths;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldExportPageSkin extends SkinBase<WorldExportPage> {
public WorldExportPageSkin(WorldExportPage skinnable) {
super(skinnable);
Insets insets = new Insets(0, 0, 12, 0);
VBox container = new VBox();
container.setSpacing(16);
container.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(container, 500);
{
HBox labelContainer = new HBox();
labelContainer.setPadding(new Insets(0, 0, 0, 5));
Label label = new Label(i18n("world.export"));
labelContainer.getChildren().setAll(label);
container.getChildren().add(labelContainer);
}
ComponentList list = new ComponentList();
FileItem fileItem = new FileItem();
fileItem.setName(i18n("world.export.location"));
fileItem.pathProperty().bindBidirectional(skinnable.pathProperty());
list.getContent().add(fileItem);
JFXTextField txtWorldName = new JFXTextField();
txtWorldName.textProperty().bindBidirectional(skinnable.worldNameProperty());
txtWorldName.setLabelFloat(true);
txtWorldName.setPromptText(i18n("world.name"));
StackPane.setMargin(txtWorldName, insets);
list.getContent().add(txtWorldName);
Label lblGameVersionTitle = new Label(i18n("world.game_version"));
Label lblGameVersion = new Label();
lblGameVersion.textProperty().bind(skinnable.gameVersionProperty());
BorderPane gameVersionPane = new BorderPane();
gameVersionPane.setPadding(new Insets(4, 0, 4, 0));
gameVersionPane.setLeft(lblGameVersionTitle);
gameVersionPane.setRight(lblGameVersion);
list.getContent().add(gameVersionPane);
container.getChildren().add(list);
JFXButton btnExport = new JFXButton(i18n("button.export"));
btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(fileItem.getPath())),
txtWorldName.textProperty().isEmpty(), fileItem.pathProperty()));
btnExport.setButtonType(JFXButton.ButtonType.RAISED);
btnExport.getStyleClass().add("jfx-button-raised");
btnExport.setOnMouseClicked(e -> skinnable.export());
HBox bottom = new HBox();
bottom.setAlignment(Pos.CENTER_RIGHT);
bottom.getChildren().setAll(btnExport);
container.getChildren().add(bottom);
getChildren().setAll(container);
}
}
@@ -0,0 +1,63 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.*;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import java.io.File;
import java.util.Date;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListItem extends Control {
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
private final World world;
public WorldListItem(World world) {
this.world = world;
title.set(world.getWorldName());
subtitle.set(i18n("world.description", world.getFileName(), new Date(world.getLastPlayed()).toString(), world.getGameVersion()));
}
@Override
protected Skin<?> createDefaultSkin() {
return new WorldListItemSkin(this);
}
public StringProperty titleProperty() {
return title;
}
public StringProperty subtitleProperty() {
return subtitle;
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public void export() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("world.export.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
fileChooser.setInitialFileName(world.getWorldName());
File file = fileChooser.showSaveDialog(Controllers.getStage());
if (file == null) {
return;
}
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> new WorldExportPage(world, file.toPath(), controller::onFinish)));
}
public void manageDatapacks() {
Controllers.navigate(new DatapackListPage(world.getWorldName(), world.getFile()));
}
}
@@ -0,0 +1,89 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import java.util.function.Function;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListItemSkin extends SkinBase<WorldListItem> {
public WorldListItemSkin(WorldListItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
HBox center = new HBox();
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().bind(skinnable.imageProperty());
imageViewContainer.getChildren().setAll(imageView);
TwoLineListItem item = new TwoLineListItem();
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
root.setCenter(center);
VBox menu = new VBox();
JFXPopup popup = new JFXPopup(menu);
Function<Runnable, Runnable> wrap = r -> () -> {
r.run();
popup.hide();
};
Function<Node, Node> limitWidth = node -> {
StackPane pane = new StackPane(node);
pane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(pane, 14);
FXUtils.setLimitHeight(pane, 14);
return pane;
};
menu.getChildren().setAll(
new IconedMenuItem(limitWidth.apply(SVG.gear(Theme.blackFillBinding(), 14, 14)), i18n("world.datapack"), wrap.apply(skinnable::manageDatapacks)),
new IconedMenuItem(limitWidth.apply(SVG.export(Theme.blackFillBinding(), 14, 14)), i18n("world.export"), wrap.apply(skinnable::export)));
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnManage = new JFXButton();
btnManage.setOnMouseClicked(e -> {
popup.show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
});
btnManage.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnManage, Pos.CENTER);
btnManage.setGraphic(SVG.dotsVertical(Theme.blackFillBinding(), -1, -1));
right.getChildren().add(btnManage);
root.setRight(right);
root.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());
getChildren().setAll(root);
}
}
@@ -0,0 +1,57 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.util.Logging;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListPage extends ListPage<WorldListItem> {
private Path savesDir;
public WorldListPage() {
}
public void loadVersion(Profile profile, String id) {
this.savesDir = profile.getRepository().getRunDirectory(id).toPath().resolve("saves");
itemsProperty().clear();
try {
if (Files.exists(savesDir))
for (Path worldDir : Files.newDirectoryStream(savesDir)) {
itemsProperty().add(new WorldListItem(new World(worldDir)));
}
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read saves", e);
}
}
@Override
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("world.choose_world"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("world.extension"), "*.zip"));
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
if (res == null) return;
res.forEach(it -> {
try {
World world = new World(it.toPath());
world.install(savesDir, world.getWorldName());
itemsProperty().add(new WorldListItem(new World(savesDir.resolve(world.getWorldName()))));
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e);
}
});
}
}
@@ -0,0 +1,37 @@
package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import java.util.Map;
import java.util.function.Function;
public class SinglePageWizardProvider implements WizardProvider {
private final Function<WizardController, WizardSinglePage> provider;
private WizardSinglePage page;
public SinglePageWizardProvider(Function<WizardController, WizardSinglePage> provider) {
this.provider = provider;
}
@Override
public void start(Map<String, Object> settings) {
}
@Override
public Object finish(Map<String, Object> settings) {
return page.finish();
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
if (step != 0) throw new IllegalStateException("Step must be 0");
return page = provider.apply(controller);
}
@Override
public boolean cancel() {
return true;
}
}
@@ -0,0 +1,19 @@
package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.control.Control;
import java.util.Map;
public abstract class WizardSinglePage extends Control implements WizardPage {
protected final Runnable onFinish;
protected WizardSinglePage(Runnable onFinish) {
this.onFinish = onFinish;
}
protected abstract Object finish();
@Override
public void cleanup(Map<String, Object> settings) {
}
}
@@ -7,6 +7,7 @@
<?import org.jackhuang.hmcl.ui.versions.VersionSettingsPage?>
<?import org.jackhuang.hmcl.ui.versions.ModListPage?>
<?import org.jackhuang.hmcl.ui.versions.InstallerListPage?>
<?import org.jackhuang.hmcl.ui.versions.WorldListPage?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:id="rootPane"
@@ -23,6 +24,9 @@
<Tab text="%settings.tabs.installers">
<InstallerListPage fx:id="installer" />
</Tab>
<Tab text="%world">
<WorldListPage fx:id="world" />
</Tab>
</JFXTabPane>
<HBox alignment="TOP_RIGHT" style="-fx-padding: 2 2 2 2;" spacing="3" pickOnBounds="false">
@@ -70,6 +70,7 @@ button.clear=Clear
button.delete=Delete
button.edit=Edit
button.install=Install
button.export=Export
button.no=No
button.ok=OK
button.refresh=Refresh
@@ -83,11 +84,11 @@ crash.NoClassDefFound=Please check "HMCL" software is complete.
crash.user_fault=Your OS or Java environment may not be properly installed resulting in crashing of this software, please check your Java Environment or your computer!
download=Download
download.BMCL=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
download.failed=Failed to download
download.failed.empty=No candidates. Click here to return.
download.failed.refresh=Unable to load version list. Click here to retry.
download.mojang=Mojang
download.provider.bmclapi=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
download.provider.mojang=Mojang
extension.bat=Windows Bat file
extension.mod=Mod file
@@ -110,6 +111,9 @@ folder.screenshots=Screenshots
general.prompt.empty=(None)
help=Documentations
help.detail=For datapacks, modpacks, etc. makers.
input.email=The username must be an e-mail.
input.number=Must be a number.
input.not_empty=Required field
@@ -240,6 +244,24 @@ mods.add.success=Successfully added mods %s.
mods.choose_mod=Choose your mods
mods.remove=Remove
datapack=Data packs
datapack.add=Add data pack
datapack.choose_datapack=Choose the datapack zip to be imported
datapack.title=World %s - Datapacks
datapack.remove=Remove
world=Worlds/Datapacks
world.choose_world=Choose the save zip to be imported
world.datapack=Manage data packs
world.description=%s. Last played time: %s. Game version: %s.
world.export=Export this world
world.export.title=Choose a file location to hold your world
world.export.location=Export to
world.export.wizard=Export world %s
world.extension=World zip
world.game_version=Game Version
world.name=World Name
profile=Game Directories
profile.default=Current directory
profile.home=User home
@@ -343,6 +365,7 @@ version.launch_script.failed=Unable to make launch script.
version.launch_script.save=Save the launch script
version.launch_script.success=Finished script creation, %s.
version.manage=Game List
version.manage.manage=Game Management
version.manage.redownload_assets_index=Redownload Assets Index
version.manage.remove=Delete this game
version.manage.remove.confirm=Sure to remove game %s? You cannot restore this game again!
@@ -70,6 +70,7 @@ button.clear=清除
button.delete=刪除
button.edit=編輯
button.install=安裝
button.export=導出
button.no=否
button.ok=確定
button.refresh=重新整理
@@ -83,11 +84,11 @@ crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟體崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下載失敗
download.failed.empty=沒有可供安裝的版本,點擊此處返回。
download.failed.refresh=載入版本列表失敗,點擊此處重試。
download.mojang=官方伺服器(Mojang)
download.provider.bmclapi=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.provider.mojang=官方伺服器(Mojang)
extension.bat=Windows 腳本
extension.mod=模組檔案
@@ -110,6 +111,9 @@ folder.screenshots=截圖資料夾
general.prompt.empty=(無)
help=HMCL 幫助文檔
help.detail=可查閱資料包、整合包製作指南等內容。
input.email=「使用者名稱」必須是電子信箱格式
input.number=必須是數字
input.not_empty=必填
@@ -240,6 +244,24 @@ mods.add.success=成功新增模組 %s。
mods.choose_mod=選擇模組
mods.remove=刪除
datapack=數據檔
datapack.add=添加數據檔
datapack.choose_datapack=選擇要導入的數據包壓縮檔
datapack.title=世界 %s - 數據檔
datapack.remove=刪除
world=世界/數據檔
world.choose_world=選擇要導入的存檔壓縮檔
world.datapack=管理數據檔
world.description=%s. 上一次遊戲時間: %s. 遊戲版本: %s
world.export=導出此世界
world.export.title=選擇該世界的存儲位置
world.export.location=保存到
world.export.wizard=導出世界 %s
world.extension=存檔壓縮檔
world.game_version=遊戲版本
world.name=世界名稱
profile=遊戲目錄
profile.default=目前目錄
profile.home=主資料夾
@@ -343,6 +365,7 @@ version.launch_script.failed=生成啟動腳本失敗
version.launch_script.save=儲存啟動腳本
version.launch_script.success=啟動腳本已生成完畢:%s
version.manage=遊戲列表
version.manage.manage=游戏管理
version.manage.redownload_assets_index=重新下載資源設定(assets_index.json)
version.manage.remove=刪除該版本
version.manage.remove.confirm=真的要刪除版本 %s 嗎?你將無法找回被刪除的檔案!
@@ -70,6 +70,7 @@ button.clear=清除
button.delete=删除
button.edit=修改
button.install=安装
button.export=导出
button.no=否
button.ok=确定
button.refresh=刷新
@@ -83,11 +84,11 @@ crash.NoClassDefFound=请确认 HMCL 本体是否完整,或更新您的 Java
crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩溃,请检查您的 Java 环境或您的电脑!可以尝试重新安装 Java。
download=下载
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下载失败
download.failed.empty=没有可供安装的版本,点击此处返回。
download.failed.refresh=加载版本列表失败,点击此处重试。
download.mojang=官方(Mojang)
download.provider.bmclapi=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.provider.mojang=官方(Mojang)
extension.bat=Windows 脚本
extension.mod=模组文件
@@ -110,6 +111,9 @@ folder.screenshots=截图文件夹
general.prompt.empty=(无)
help=HMCL 帮助文档
help.detail=可查阅数据包、整合包制作指南等内容。
input.email=用户名必须是邮箱
input.number=必须是数字
input.not_empty=必填项
@@ -240,6 +244,24 @@ mods.add.success=成功添加模组 %s。
mods.choose_mod=选择模组
mods.remove=删除
datapack=数据包
datapack.add=添加数据包
datapack.choose_datapack=选择要导入的数据包压缩包
datapack.title=世界 %s - 数据包
datapack.remove=删除
world=世界/数据包
world.choose_world=选择要导入的存档压缩包
world.datapack=管理数据包
world.description=%s. 上一次游戏时间: %s. 游戏版本: %s
world.export=导出此世界
world.export.title=选择该世界的存储位置
world.export.location=保存到
world.export.wizard=导出世界 %s
world.extension=世界压缩包
world.game_version=游戏版本
world.name=世界名称
profile=游戏目录
profile.default=当前目录
profile.home=主文件夹
@@ -343,6 +365,7 @@ version.launch_script.failed=生成启动脚本失败
version.launch_script.save=保存启动脚本
version.launch_script.success=启动脚本已生成完毕:%s
version.manage=游戏列表
version.manage.manage=游戏管理
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
version.manage.remove=删除该版本
version.manage.remove.confirm=真的要删除版本 %s 吗?你将无法找回被删除的文件!
@@ -28,11 +28,6 @@ import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
*/
public class BMCLAPIDownloadProvider implements DownloadProvider {
public static final BMCLAPIDownloadProvider INSTANCE = new BMCLAPIDownloadProvider();
private BMCLAPIDownloadProvider() {
}
@Override
public String getVersionListURL() {
return "https://bmclapi2.bangbang93.com/mc/game/version_manifest.json";
@@ -1,27 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download;
public class CurseCDNDownloadProvider extends MojangDownloadProvider {
public static final CurseCDNDownloadProvider INSTANCE = new CurseCDNDownloadProvider();
@Override
public String injectURL(String baseURL) {
return baseURL == null ? null : baseURL.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://ftb.cursecdn.com/FTB2/maven");
}
}
@@ -55,7 +55,10 @@ public class DefaultCacheRepository extends CacheRepository {
lock.writeLock().lock();
try {
index = Constants.GSON.fromJson(FileUtils.readText(indexFile.toFile()), Index.class);
if (Files.isRegularFile(indexFile))
index = Constants.GSON.fromJson(FileUtils.readText(indexFile.toFile()), Index.class);
else
index = new Index();
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to read index file", e);
index = new Index();
@@ -23,7 +23,7 @@ import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
/**
* @see <a href="http://wiki.vg">http://wiki,vg</a>
* @see <a href="http://wiki.vg">http://wiki.vg</a>
* @author huangyuhui
*/
public class MojangDownloadProvider implements DownloadProvider {
@@ -56,6 +56,6 @@ public class MojangDownloadProvider implements DownloadProvider {
@Override
public String injectURL(String baseURL) {
return baseURL;
return baseURL;
}
}
@@ -15,6 +15,7 @@ import org.tukaani.xz.XZInputStream;
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.jar.JarEntry;
@@ -75,7 +76,7 @@ public class LibraryDownloadTask extends Task {
else
throw new LibraryDownloadException(library, t);
} else {
if (xz) unpackLibrary(jar, FileUtils.readBytes(xzFile));
if (xz) unpackLibrary(jar, Files.readAllBytes(xzFile.toPath()));
if (!checksumValid(jar, library.getChecksums())) {
jar.delete();
throw new IOException("Checksum failed for " + library);
@@ -125,7 +126,7 @@ public class LibraryDownloadTask extends Task {
if (checksums == null || checksums.isEmpty()) {
return true;
}
byte[] fileData = FileUtils.readBytes(libPath);
byte[] fileData = Files.readAllBytes(libPath.toPath());
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
if (!valid && libPath.getName().endsWith(".jar")) {
valid = validateJar(fileData, checksums);
@@ -0,0 +1,162 @@
package org.jackhuang.hmcl.game;
import com.github.steveice10.opennbt.NBTIO;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.LongTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Unzipper;
import org.jackhuang.hmcl.util.Zipper;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class World {
private final Path file;
private String fileName;
private String worldName;
private String gameVersion;
private long lastPlayed, seed;
public World(Path file) throws IOException {
this.file = file;
if (Files.isDirectory(file))
loadFromDirectory();
else if (Files.isRegularFile(file))
loadFromZip();
else
throw new IOException("Path " + file + " cannot be recognized as a Minecraft world");
}
private void loadFromDirectory() throws IOException {
fileName = FileUtils.getName(file);
Path levelDat = file.resolve("level.dat");
getWorldName(levelDat);
}
public Path getFile() {
return file;
}
public String getFileName() {
return fileName;
}
public String getWorldName() {
return worldName;
}
public long getLastPlayed() {
return lastPlayed;
}
public long getSeed() {
return seed;
}
public String getGameVersion() {
return gameVersion;
}
private void loadFromZip() throws IOException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file)) {
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid world zip file"));
Path levelDat = root.resolve("level.dat");
if (!Files.exists(levelDat))
throw new IllegalArgumentException("Not a valid world zip file since level.dat cannot be found.");
fileName = FileUtils.getName(root);
getWorldName(levelDat);
}
}
private void getWorldName(Path levelDat) throws IOException {
CompoundTag nbt = parseLevelDat(levelDat);
CompoundTag data = nbt.get("Data");
String name = data.<StringTag>get("LevelName").getValue();
lastPlayed = data.<LongTag>get("LastPlayed").getValue();
seed = data.<LongTag>get("RandomSeed").getValue();
CompoundTag version = data.get("Version");
gameVersion = version.<StringTag>get("Name").getValue();
worldName = name;
}
public void rename(String newName) throws IOException {
if (!Files.isDirectory(file))
throw new IOException("Not a valid world directory");
// Change the name recorded in level.dat
Path levelDat = file.resolve("level.dat");
CompoundTag nbt = parseLevelDat(levelDat);
CompoundTag data = nbt.get("Data");
data.put(new StringTag("LevelName", newName));
NBTIO.writeTag(new GZIPOutputStream(Files.newOutputStream(levelDat)), nbt);
// then change the folder's name
Files.move(file, file.resolveSibling(newName));
}
public void install(Path savesDir, String name) throws IOException {
Path worldDir = savesDir.resolve(name);
if (Files.isDirectory(worldDir)) {
throw new FileAlreadyExistsException("World already exists");
}
if (Files.isRegularFile(file)) {
String subDirectoryName;
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file)) {
List<Path> subDirs = Files.list(fs.getPath("/")).collect(Collectors.toList());
if (subDirs.size() != 1) {
throw new IOException("World zip malformed");
}
subDirectoryName = FileUtils.getName(subDirs.get(0));
}
new Unzipper(file, savesDir)
.setSubDirectory("/" + subDirectoryName + "/")
.unzip();
} else if (Files.isDirectory(file)) {
FileUtils.copyDirectory(file, worldDir);
}
}
public void export(Path zip, String worldName) throws IOException {
if (!Files.isDirectory(file))
throw new IOException();
try (Zipper zipper = new Zipper(zip)) {
zipper.putDirectory(file, "/" + worldName + "/");
}
}
private static CompoundTag parseLevelDat(Path path) throws IOException {
Tag nbt = NBTIO.readTag(new GZIPInputStream(Files.newInputStream(path)));
if (nbt instanceof CompoundTag)
return (CompoundTag) nbt;
else
throw new IOException("level.dat malformed");
}
public static List<World> getWorlds(Path worldDir) throws IOException {
List<World> worlds = new ArrayList<>();
for (Path world : Files.newDirectoryStream(worldDir)) {
worlds.add(new World(world));
}
return worlds;
}
}
@@ -0,0 +1,222 @@
package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.util.*;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Level;
public class Datapack {
private boolean isMultiple;
private final Path path;
private final ObservableList<Pack> info = FXCollections.observableArrayList();
public Datapack(Path path) {
this.path = path;
}
public Path getPath() {
return path;
}
public ObservableList<Pack> getInfo() {
return info;
}
public void installTo(Path worldPath) throws IOException {
Path datapacks = worldPath.resolve("datapacks");
Set<String> packs = new HashSet<>();
for (Pack pack : info) packs.add(pack.getId());
if (Files.isDirectory(datapacks))
for (Path datapack : Files.newDirectoryStream(datapacks)) {
if (packs.contains(FileUtils.getName(datapack)))
FileUtils.deleteDirectory(datapack.toFile());
}
if (isMultiple) {
new Unzipper(path, worldPath).setReplaceExistentFile(true).unzip();
} else {
new Unzipper(path, worldPath.resolve("datapacks").resolve(FileUtils.getNameWithoutExtension(path))).unzip();
}
}
public void deletePack(String pack) throws IOException {
FileUtils.deleteDirectory(path.resolve(pack).toFile());
Platform.runLater(() -> info.removeIf(p -> p.getId().equals(pack)));
}
public void loadFromZip() throws IOException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(path)) {
Path datapacks = fs.getPath("/datapacks/");
Path mcmeta = fs.getPath("pack.mcmeta");
if (Files.exists(datapacks)) { // multiple datapacks
isMultiple = true;
loadFromDir(datapacks);
} else if (Files.exists(mcmeta)) { // single datapack
isMultiple = false;
try {
PackMcMeta pack = JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class);
info.add(new Pack(mcmeta, FileUtils.getNameWithoutExtension(path), pack.getPackInfo().getDescription(), this));
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read datapack " + path, e);
}
} else {
throw new IOException("Malformed datapack zip");
}
}
}
public void loadFromDir() {
try {
loadFromDir(path);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read datapacks " + path, e);
}
}
private void loadFromDir(Path dir) throws IOException {
List<Pack> info = new ArrayList<>();
if (Files.isDirectory(dir))
for (Path subDir : Files.newDirectoryStream(dir)) {
Path mcmeta = subDir.resolve("pack.mcmeta");
Path mcmetaDisabled = subDir.resolve("pack.mcmeta.disabled");
if (!Files.exists(mcmeta) && !Files.exists(mcmetaDisabled))
continue;
boolean enabled = Files.exists(mcmeta);
try {
PackMcMeta pack = enabled ? JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class)
: JsonUtils.fromNonNullJson(FileUtils.readText(mcmetaDisabled), PackMcMeta.class);
info.add(new Pack(enabled ? mcmeta : mcmetaDisabled, FileUtils.getName(subDir), pack.getPackInfo().getDescription(), this));
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read datapack " + subDir, e);
}
}
this.info.setAll(info);
}
public static class Pack {
private Path packMcMeta;
private final BooleanProperty active;
private final String id;
private final String description;
private final Datapack datapack;
public Pack(Path packMcMeta, String id, String description, Datapack datapack) {
this.packMcMeta = packMcMeta;
this.id = id;
this.description = description;
this.datapack = datapack;
active = new SimpleBooleanProperty(this, "active", !DISABLED_EXT.equals(FileUtils.getExtension(packMcMeta))) {
@Override
protected void invalidated() {
Path f = Pack.this.packMcMeta.toAbsolutePath(), newF;
if (DISABLED_EXT.equals(FileUtils.getExtension(f)))
newF = f.getParent().resolve(FileUtils.getNameWithoutExtension(f));
else
newF = f.getParent().resolve(FileUtils.getName(f) + "." + DISABLED_EXT);
try {
Files.move(f, newF);
Pack.this.packMcMeta = newF;
} catch (IOException e) {
// Mod file is occupied.
Logging.LOG.warning("Unable to rename file " + f + " to " + newF);
}
}
};
}
public String getId() {
return id;
}
public String getDescription() {
return description;
}
public Datapack getDatapack() {
return datapack;
}
public BooleanProperty activeProperty() {
return active;
}
public boolean isActive() {
return active.get();
}
public void setActive(boolean active) {
this.active.set(active);
}
}
private static class PackMcMeta implements Validation {
@SerializedName("pack")
private final PackInfo pack;
public PackMcMeta() {
this(new PackInfo());
}
public PackMcMeta(PackInfo packInfo) {
this.pack = packInfo;
}
public PackInfo getPackInfo() {
return pack;
}
@Override
public void validate() throws JsonParseException {
if (pack == null)
throw new JsonParseException("pack cannot be null");
}
public static class PackInfo {
@SerializedName("pack_format")
private final int packFormat;
@SerializedName("description")
private final String description;
public PackInfo() {
this(0, "");
}
public PackInfo(int packFormat, String description) {
this.packFormat = packFormat;
this.description = description;
}
public int getPackFormat() {
return packFormat;
}
public String getDescription() {
return description;
}
}
}
private static final String DISABLED_EXT = "disabled";
}
@@ -65,7 +65,7 @@ public final class ModInfo implements Comparable<ModInfo> {
if (DISABLED_EXTENSION.equals(FileUtils.getExtension(f)))
newF = new File(f.getParentFile(), FileUtils.getNameWithoutExtension(f));
else
newF = new File(f.getParentFile(), f.getName() + ".disabled");
newF = new File(f.getParentFile(), f.getName() + "." + DISABLED_EXTENSION);
if (f.renameTo(newF))
ModInfo.this.file = newF;
@@ -43,10 +43,18 @@ public final class FileUtils {
return StringUtils.substringBeforeLast(file.getName(), '.');
}
public static String getNameWithoutExtension(Path file) {
return StringUtils.substringBeforeLast(getName(file), '.');
}
public static String getExtension(File file) {
return StringUtils.substringAfterLast(file.getName(), '.');
}
public static String getExtension(Path file) {
return StringUtils.substringAfterLast(getName(file), '.');
}
/**
* This method is for normalizing ZipPath since Path.normalize of ZipFileSystem does not work properly.
*/
@@ -63,11 +71,15 @@ public final class FileUtils {
}
public static String readText(File file, Charset charset) throws IOException {
return new String(readBytes(file), charset);
return new String(Files.readAllBytes(file.toPath()), charset);
}
public static byte[] readBytes(File file) throws IOException {
return Files.readAllBytes(file.toPath());
public static String readText(Path file) throws IOException {
return readText(file, UTF_8);
}
public static String readText(Path file, Charset charset) throws IOException {
return new String(Files.readAllBytes(file), charset);
}
public static void writeText(File file, String text) throws IOException {
@@ -33,8 +33,17 @@ public final class IntVersionNumber extends VersionNumber {
final List<Integer> version;
public static boolean isIntVersionNumber(String version) {
return version.chars().noneMatch(ch -> ch != '.' && (ch < '0' || ch > '9'))
&& !version.contains("..") && StringUtils.isNotBlank(version);
if (version.chars().noneMatch(ch -> ch != '.' && (ch < '0' || ch > '9'))
&& !version.contains("..") && StringUtils.isNotBlank(version)) {
String[] arr = version.split("\\.");
for (String str : arr)
if (str.length() > 9)
// Numbers which are larger than 1e9 cannot be stored as integer.
return false;
return true;
} else {
return false;
}
}
IntVersionNumber(String version) {
@@ -0,0 +1,42 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ComboBox;
import javafx.scene.control.SelectionModel;
public class SelectionModelSelectedItemProperty<T> extends SimpleObjectProperty<T> {
public static <T> SelectionModelSelectedItemProperty<T> selectedItemPropertyFor(ComboBox<T> comboBox) {
return new SelectionModelSelectedItemProperty<>(comboBox.getSelectionModel());
}
private SelectionModel<T> model;
public SelectionModelSelectedItemProperty(SelectionModel<T> model) {
this.model = model;
model.selectedItemProperty().addListener((observable, oldValue, newValue) -> set(newValue));
set(model.getSelectedItem());
}
@Override
protected void invalidated() {
model.select(get());
}
}
+4
View File
@@ -36,6 +36,8 @@ subprojects {
repositories {
mavenCentral()
jcenter()
maven { url 'https://jitpack.io' }
}
sourceCompatibility = 1.8
@@ -45,6 +47,8 @@ subprojects {
compile group: 'org.tukaani', name: 'xz', version: '1.8'
compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0'
compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
compile group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
testCompile group: 'junit', name: 'junit', version: '4.12'
}