Compare commits

..
36 Commits
Author SHA1 Message Date
huanghongxun 129f6bb5cc Merge pull request #407 from yushijinhun/config-permission
重构 Config
2018-07-29 14:44:23 +08:00
yushijinhun 545c17d052 Print current directory & hmcl directory when bootstrap 2018-07-29 14:27:50 +08:00
yushijinhun add00ddba1 Use configured locale as soon as possible 2018-07-29 14:21:43 +08:00
yushijinhun d9aeb0cc2b Use SupportedLocale in Config 2018-07-29 14:15:32 +08:00
yushijinhun 49e70e7195 Add ConfigHolder.isNewlyCreated() 2018-07-29 14:02:32 +08:00
yushijinhun f8abebf110 Remove isFirstLaunch 2018-07-29 13:45:22 +08:00
yushijinhun 8e2dfc7341 Show error dialog when hmcl.json is not accessible 2018-07-29 13:40:59 +08:00
yushijinhun e650ffffce Initialize ConfigHolder & Settings manually 2018-07-29 13:40:59 +08:00
yushijinhun d549a300ab Replace Settings.INSTANCE with Settings.instance() 2018-07-29 13:40:59 +08:00
yushijinhun 82a62e73c1 Replace CONFIG with config() 2018-07-29 13:40:57 +08:00
yushijinhun 45c7e0c231 Remove Settings.save() 2018-07-29 13:39:32 +08:00
huangyuhui b98a4b11a3 Fix #406 2018-07-29 13:31:18 +08:00
huangyuhui 484a103f27 Fix always showing the welcome page when upgrading from old versions 2018-07-29 13:25:15 +08:00
huangyuhui e31120be21 Friendly prompt that it cannot download some libraries successfully. 2018-07-22 18:50:44 +08:00
huangyuhui 247f02d423 Friendly prompt to ServerResponseMalformedException 2018-07-22 18:22:28 +08:00
huangyuhui 3da0c1d8e3 Abandon Launch4j which will not find Java inst in PATH or JAVA_HOME 2018-07-22 18:15:39 +08:00
huangyuhui c830bbc703 reorder version in main page 2018-07-21 21:58:46 +08:00
huangyuhui 7b619fef1d Fix always using BMCLAPIDownloadProvider 2018-07-21 20:24:54 +08:00
huanghongxun bac265a077 Merge pull request #399 from yushijinhun/refactor-account
重构账户部分
2018-07-21 19:50:46 +08:00
huanghongxun 39df0f8d43 Merge branch 'javafx' into refactor-account 2018-07-21 19:50:16 +08:00
huangyuhui 6af7f916ee Add an option to disable common directory globally 2018-07-21 19:45:59 +08:00
yushijinhun ae64097edf Automatically select the newly added account 2018-07-21 12:34:12 +08:00
yushijinhun cb6d8a88f1 Fix the old account isn't replaced when adding a same account 2018-07-21 12:31:08 +08:00
huangyuhui 12838994ad Reconstruct MultiFileItem 2018-07-21 11:39:41 +08:00
yushijinhun d30758a2a8 Throw NPE when selected server is null 2018-07-21 09:57:47 +08:00
yushijinhun b820aad358 Use MappedObservableList in AuthlibInjectorServersPage 2018-07-20 20:26:27 +08:00
yushijinhun 170189c344 Refactor accounts 2018-07-20 20:26:27 +08:00
yushijinhun f9e9c9d38b Use LinkedHashMap for mapOf 2018-07-20 20:26:27 +08:00
yushijinhun 088af981df Turn properties in SettingsPage from local variables into fields 2018-07-20 20:26:27 +08:00
yushijinhun 357fef8672 Call initProxy() manually 2018-07-20 20:26:27 +08:00
yushijinhun c38848ea0e Rename fields in Accounts 2018-07-20 20:26:27 +08:00
yushijinhun be8cd13b08 Throw IAE when getAccountType() receives an unexpected param 2018-07-20 20:26:27 +08:00
yushijinhun 487816ab74 Use AccountFactory in cboType 2018-07-20 20:26:27 +08:00
yushijinhun bcd9f8240c Extract account factories to static fields 2018-07-20 20:26:27 +08:00
huangyuhui 1b62972f06 Fix 1.13 version parsing 2018-07-20 11:44:50 +08:00
huangyuhui 1117c3bfab Add missing data binding 2018-07-19 18:30:47 +08:00
76 changed files with 1589 additions and 1010 deletions

No files matched your search

Binary file not shown.
+22 -18
View File
@@ -5,10 +5,6 @@ import java.util.jar.JarFile
import java.util.jar.Pack200
import java.util.zip.GZIPOutputStream
plugins {
id "edu.sc.seis.launch4j" version "2.4.4"
}
if (!hasProperty('mainClass')) {
ext.mainClass = 'org.jackhuang.hmcl.Main'
}
@@ -97,21 +93,29 @@ task makePackGZ(dependsOn: jar) doLast {
fileEx.append sha1Hex
}
launch4j {
mainClassName = mainClass
icon = "${projectDir}/icon.ico"
task makeExecutable(dependsOn: jar) doLast {
ext {
jar.classifier = ''
makeExecutableinjar = jar.archivePath
jar.classifier = ''
makeExecutableoutjar = jar.archivePath
jar.classifier = ''
}
def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length() - 4) + ".exe")
def fos = new FileOutputStream(loc)
def is = new FileInputStream(new File(project.buildDir, '../HMCLauncher.exe'))
int read
def bytes = new byte[8192]
while((read = is.read(bytes)) != -1)
fos.write(bytes, 0, read)
is.close()
is = new FileInputStream(makeExecutableoutjar)
while((read = is.read(bytes)) != -1)
fos.write(bytes, 0, read)
is.close()
fos.close()
jvmOptions = ["-XX:MinHeapFreeRatio=5", "-XX:MaxHeapFreeRatio=15"]
version = mavenVersion
productName = "HMCL"
internalName = "HMCL-${mavenVersion}"
outfile = "HMCL-${mavenVersion}.exe"
outputDir = "libs"
copyright = "Copyright (C) 2018 huangyuhui <[email protected]>"
}
build.dependsOn makePackGZ
build.dependsOn createExe
build.dependsOn makeExecutable
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl;
import static org.jackhuang.hmcl.util.Logging.LOG;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
@@ -34,6 +36,7 @@ import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
@@ -79,11 +82,13 @@ public final class Launcher extends Application {
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
Logging.LOG.info("*** " + TITLE + " ***");
Logging.LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
Logging.LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
Logging.LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
Logging.LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("*** " + TITLE + " ***");
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
LOG.info("HMCL Directory: " + HMCL_DIRECTORY);
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
@@ -92,7 +97,7 @@ public final class Launcher extends Application {
}
public static void stopApplication() {
Logging.LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
@@ -106,7 +111,7 @@ public final class Launcher extends Application {
}
public static void stopWithoutPlatform() {
Logging.LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
@@ -21,6 +21,8 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -32,12 +34,17 @@ import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.swing.JOptionPane;
import org.jackhuang.hmcl.setting.ConfigHolder;
public final class Main {
public static void main(String[] args) {
checkJavaFX();
checkDirectoryPath();
checkDSTRootCAX3();
checkConfigPermission();
ConfigHolder.init();
Launcher.main(args);
}
@@ -80,6 +87,22 @@ public final class Main {
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
}
private static void checkConfigPermission() {
Path config = ConfigHolder.CONFIG_PATH;
if (Files.exists(config)) {
if (Files.isReadable(config) && Files.isWritable(config)) {
// we are able to read & write the existent config
return;
}
} else {
if (Files.isWritable(config.getParent())) {
// we are able to create a new config
return;
}
}
showErrorAndExit(i18n("fatal.config_access_denied", config));
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
@@ -1,57 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
/**
* This event gets fired when added accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountAddedEvent extends Event {
private final Account account;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountAddedEvent(Object source, Account account) {
super(source);
this.account = account;
}
public Account getAccount() {
return account;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("account", account)
.toString();
}
}
@@ -1,60 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Collection;
import java.util.Collections;
/**
* This event gets fired when loading accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountLoadingEvent extends Event {
private final Collection<Account> accounts;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountLoadingEvent(Object source, Collection<Account> accounts) {
super(source);
this.accounts = Collections.unmodifiableCollection(accounts);
}
public Collection<Account> getAccounts() {
return accounts;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("accounts", accounts)
.toString();
}
}
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Scheduler;
import org.jackhuang.hmcl.task.Schedulers;
@@ -42,7 +42,7 @@ public final class AccountHelper {
public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
public static void loadSkins() {
for (Account account : Settings.INSTANCE.getAccounts()) {
for (Account account : Accounts.getAccounts()) {
if (account instanceof YggdrasilAccount) {
new SkinLoadTask((YggdrasilAccount) account, false).start();
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
@@ -29,9 +30,10 @@ import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/**
* @author huangyuhui
@@ -59,7 +61,11 @@ public class HMCLGameDownloadTask extends Task {
public void execute() {
File jar = profile.getRepository().getVersionJar(version);
File cache = new File(CONFIG.getCommonDirectory(), "jars/" + gameVersion + ".jar");
// Force using common directory will not affect the behaviour that repository acts
// Since we always copy the downloaded jar to .minecraft/versions/<version>/
File cache = new File(Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(Settings.getDefaultCommonDirectory()),
"jars/" + gameVersion + ".jar");
if (cache.exists())
try {
FileUtils.copyFile(cache, jar);
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
@@ -33,7 +34,7 @@ import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
@@ -51,17 +52,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
return profile;
}
private boolean useSelf(String version, String assetId) {
VersionSetting vs = profile.getVersionSetting(version);
return new File(getBaseDirectory(), "assets/indexes/" + assetId + ".json").exists() || vs.isNoCommon();
private boolean useSelf(String assetId) {
return new File(getBaseDirectory(), "assets/indexes/" + assetId + ".json").exists();
}
@Override
public File getAssetDirectory(String version, String assetId) {
if (useSelf(version, assetId))
if (Settings.instance().isCommonDirectoryDisabled() || useSelf(assetId))
return super.getAssetDirectory(version, assetId);
else
return new File(CONFIG.getCommonDirectory(), "assets");
return new File(Settings.instance().getCommonDirectory(), "assets");
}
@Override
@@ -83,10 +83,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
public File getLibraryFile(Version version, Library lib) {
VersionSetting vs = profile.getVersionSetting(version.getId());
File self = super.getLibraryFile(version, lib);
if (self.exists() || vs.isNoCommon())
if (Settings.instance().isCommonDirectoryDisabled() || self.exists())
return self;
else
return new File(CONFIG.getCommonDirectory(), "libraries/" + lib.getPath());
return new File(Settings.instance().getCommonDirectory(), "libraries/" + lib.getPath());
}
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
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;
@@ -190,6 +191,8 @@ public final class LauncherHelper {
message = i18n("launch.failed.creating_process") + ex.getLocalizedMessage();
} else if (ex instanceof NotDecompressingNativesException) {
message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage();
} else if (ex instanceof LibraryDownloadException) {
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(ex.getCause());
} else {
message = StringUtils.getStackTrace(ex);
}
@@ -375,7 +378,7 @@ public final class LauncherHelper {
System.out.print(log);
logs.add(pair(log, level));
if (logs.size() > Settings.INSTANCE.getLogLines())
if (logs.size() > Settings.instance().getLogLines())
logs.removeFirst();
if (setting.isShowLogs()) {
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
*
* 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
@@ -29,14 +29,27 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author huangyuhui
@@ -44,35 +57,162 @@ import static org.jackhuang.hmcl.util.Pair.pair;
public final class Accounts {
private Accounts() {}
public static final String OFFLINE_ACCOUNT_KEY = "offline";
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector";
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,
Accounts::getOrCreateAuthlibInjectorServer);
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = mapOf(
pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
pair(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)),
pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer))
);
private static final String TYPE_OFFLINE = "offline";
private static final String TYPE_YGGDRASIL_ACCOUNT = "yggdrasil";
private static final String TYPE_AUTHLIB_INJECTOR = "authlibInjector";
public static String getAccountType(Account account) {
if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY;
else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY;
else if (account instanceof YggdrasilAccount) return YGGDRASIL_ACCOUNT_KEY;
else return YGGDRASIL_ACCOUNT_KEY;
private static Map<String, AccountFactory<?>> type2factory = mapOf(
pair(TYPE_OFFLINE, FACTORY_OFFLINE),
pair(TYPE_YGGDRASIL_ACCOUNT, FACTORY_YGGDRASIL),
pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR));
private static String accountType(Account account) {
if (account instanceof OfflineAccount)
return TYPE_OFFLINE;
else if (account instanceof AuthlibInjectorAccount)
return TYPE_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount)
return TYPE_YGGDRASIL_ACCOUNT;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
static String getAccountId(Account account) {
return getAccountId(account.getUsername(), account.getCharacter());
public static AccountFactory<?> getAccountFactory(Account account) {
return type2factory.get(accountType(account));
}
static String getAccountId(String username, String character) {
return username + ":" + character;
private static String accountId(Account account) {
return account.getUsername() + ":" + account.getCharacter();
}
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListProperty<Account> accountsWrapper = new ReadOnlyListWrapper<>(accounts);
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
// this methods first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account selected = get();
if (accounts.isEmpty()) {
if (selected == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
set(null);
return;
}
} else {
if (accounts.contains(selected)) {
// valid
} else {
// the previously selected account is gone
set(accounts.get(0));
return;
}
}
// selection is valid, store it
if (!initialized)
return;
config().setSelectedAccount(selected == null ? "" : accountId(selected));
}
};
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
private static void updateAccountStorages() {
// don't update the underlying storage before data loading is completed
// otherwise it might cause data loss
if (!initialized)
return;
// update storage
config().getAccountStorages().setAll(
accounts.stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
return storage;
})
.collect(toList()));
}
/**
* Called when it's ready to load accounts from {@link ConfigHolder#config()}.
*/
static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
// load accounts
config().getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return;
}
Account account;
try {
account = factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return;
}
accounts.add(account);
});
initialized = true;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
// load selected account
selectedAccount.set(
accounts.stream()
.filter(it -> accountId(it).equals(config().getSelectedAccount()))
.findFirst()
.orElse(null));
}
public static ObservableList<Account> getAccounts() {
return accounts;
}
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper;
}
public static Account getSelectedAccount() {
return selectedAccount.get();
}
public static void setSelectedAccount(Account selectedAccount) {
Accounts.selectedAccount.set(selectedAccount);
}
public static ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
// ==== authlib-injector ====
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return CONFIG.getAuthlibInjectorServers().stream()
return config().getAuthlibInjectorServers().stream()
.filter(server -> url.equals(server.getUrl()))
.findFirst()
.orElseGet(() -> {
@@ -86,8 +226,38 @@ public final class Accounts {
LOG.log(Level.WARNING, "Failed to migrate authlib injector server " + url, e);
}
CONFIG.getAuthlibInjectorServers().add(server);
config().getAuthlibInjectorServers().add(server);
return server;
});
}
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
*/
private static void removeDanglingAuthlibInjectorAccounts() {
accounts.stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.map(AuthlibInjectorAccount.class::cast)
.filter(it -> !config().getAuthlibInjectorServers().contains(it.getServer()))
.collect(toList())
.forEach(accounts::remove);
}
// ====
// ==== Login type name i18n ===
private static Map<AccountFactory<?>, String> loginType2name = mapOf(
pair(Accounts.FACTORY_OFFLINE, i18n("account.methods.offline")),
pair(Accounts.FACTORY_YGGDRASIL, i18n("account.methods.yggdrasil")),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, i18n("account.methods.authlib_injector")));
public static String getAccountTypeName(AccountFactory<?> factory) {
return Optional.ofNullable(loginType2name.get(factory))
.orElseThrow(() -> new IllegalArgumentException("No corresponding login type name"));
}
public static String getAccountTypeName(Account account) {
return getAccountTypeName(getAccountFactory(account));
}
// ====
}
@@ -33,6 +33,8 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.util.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.FileTypeAdapter;
import org.jackhuang.hmcl.util.ObservableHelper;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -67,6 +69,7 @@ public final class Config implements Cloneable, Observable {
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.registerTypeAdapter(Theme.class, new Theme.TypeAdapter())
.registerTypeAdapter(SupportedLocale.class, new SupportedLocale.TypeAdapter())
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.setPrettyPrinting()
@@ -89,6 +92,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("bgpath")
private StringProperty backgroundImage = new SimpleStringProperty();
@SerializedName("commonDirType")
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
@SerializedName("commonpath")
private StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
@@ -117,7 +123,7 @@ public final class Config implements Cloneable, Observable {
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
@SerializedName("localization")
private StringProperty localization = new SimpleStringProperty();
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
@SerializedName("downloadtype")
private IntegerProperty downloadType = new SimpleIntegerProperty(1);
@@ -126,7 +132,7 @@ public final class Config implements Cloneable, Observable {
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@SerializedName("accounts")
private ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("selectedAccount")
private StringProperty selectedAccount = new SimpleStringProperty("");
@@ -140,12 +146,12 @@ public final class Config implements Cloneable, Observable {
@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(100);
@SerializedName("firstLaunch")
private BooleanProperty firstLaunch = new SimpleBooleanProperty(true);
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
private transient ObservableHelper helper = new ObservableHelper(this);
public Config() {
@@ -225,6 +231,18 @@ public final class Config implements Cloneable, Observable {
return backgroundImage;
}
public EnumCommonDirectory getCommonDirType() {
return commonDirType.get();
}
public ObjectProperty<EnumCommonDirectory> commonDirTypeProperty() {
return commonDirType;
}
public void setCommonDirType(EnumCommonDirectory commonDirType) {
this.commonDirType.set(commonDirType);
}
public String getCommonDirectory() {
return commonDirectory.get();
}
@@ -333,15 +351,15 @@ public final class Config implements Cloneable, Observable {
return theme;
}
public String getLocalization() {
public SupportedLocale getLocalization() {
return localization.get();
}
public void setLocalization(String localization) {
public void setLocalization(SupportedLocale localization) {
this.localization.set(localization);
}
public StringProperty localizationProperty() {
public ObjectProperty<SupportedLocale> localizationProperty() {
return localization;
}
@@ -361,8 +379,8 @@ public final class Config implements Cloneable, Observable {
return configurations;
}
public ObservableList<Map<Object, Object>> getAccounts() {
return accounts;
public ObservableList<Map<Object, Object>> getAccountStorages() {
return accountStorages;
}
public String getSelectedAccount() {
@@ -413,18 +431,6 @@ public final class Config implements Cloneable, Observable {
return logLines;
}
public boolean isFirstLaunch() {
return firstLaunch.get();
}
public void setFirstLaunch(boolean firstLaunch) {
this.firstLaunch.set(firstLaunch);
}
public BooleanProperty firstLaunchProperty() {
return firstLaunch;
}
public ObservableList<AuthlibInjectorServer> getAuthlibInjectorServers() {
return authlibInjectorServers;
}
@@ -24,8 +24,10 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.logging.Level;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
public final class ConfigHolder {
@@ -34,32 +36,72 @@ public final class ConfigHolder {
public static final String CONFIG_FILENAME = "hmcl.json";
public static final Path CONFIG_PATH = Paths.get(CONFIG_FILENAME).toAbsolutePath();
public static final Config CONFIG = initSettings();
private static Config initSettings() {
Config config = new Config();
if (Files.exists(CONFIG_PATH)) {
try {
Config deserialized = Config.fromJson(new String(Files.readAllBytes(CONFIG_PATH), UTF_8));
if (deserialized == null) {
LOG.finer("Settings file is empty, use the default settings.");
} else {
config = deserialized;
}
LOG.finest("Initialized settings.");
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Something happened wrongly when load settings.", e);
}
private static Config configInstance;
private static boolean newlyCreated;
public static Config config() {
if (configInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return config;
return configInstance;
}
static void saveConfig(Config config) {
public static boolean isNewlyCreated() {
if (configInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return newlyCreated;
}
public synchronized static void init() {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
}
configInstance = loadConfig();
configInstance.addListener(source -> saveConfig());
Settings.init();
if (newlyCreated) {
saveConfig();
}
}
private static Config loadConfig() {
if (Files.exists(CONFIG_PATH)) {
try {
String content = new String(Files.readAllBytes(CONFIG_PATH), UTF_8);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
return upgradeConfig(deserialized, raw);
}
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Something went wrong when loading config.", e);
}
}
LOG.info("Creating an empty config");
newlyCreated = true;
return new Config();
}
static void saveConfig() {
LOG.info("Saving config");
try {
Files.write(CONFIG_PATH, config.toJson().getBytes(UTF_8));
Files.write(CONFIG_PATH, configInstance.toJson().getBytes(UTF_8));
} catch (IOException ex) {
LOG.log(Level.SEVERE, "Failed to save config", ex);
}
}
private static Config upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
return deserialized;
}
}
@@ -0,0 +1,33 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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.setting;
public enum EnumCommonDirectory {
/**
* same to game directory
*/
DISABLED,
/**
* %appdata%/.minecraft or ~/.minecraft
*/
DEFAULT,
/**
* user customized common directory.
*/
CUSTOM
}
@@ -122,7 +122,7 @@ public final class Profile {
}
public HMCLDependencyManager getDependency() {
return new HMCLDependencyManager(this, Settings.INSTANCE.getDownloadProvider());
return new HMCLDependencyManager(this, Settings.instance().getDownloadProvider());
}
public VersionSetting getVersionSetting(String id) {
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.setting;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.net.Authenticator;
import java.net.InetSocketAddress;
@@ -36,20 +36,20 @@ public final class ProxyManager {
private ProxyManager() {
}
private static final ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
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) {
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));
return new Proxy(config().getProxyType(), new InetSocketAddress(host, port));
}
},
CONFIG.proxyTypeProperty(),
CONFIG.proxyHostProperty(),
CONFIG.proxyPortProperty(),
CONFIG.hasProxyProperty());
config().proxyTypeProperty(),
config().proxyHostProperty(),
config().proxyPortProperty(),
config().hasProxyProperty());
public static Proxy getProxy() {
return proxyProperty.get();
@@ -59,11 +59,7 @@ public final class ProxyManager {
return proxyProperty;
}
static {
initProxy();
}
private static void initProxy() {
static void init() {
proxyProperty.addListener(observable -> updateSystemProxy());
updateSystemProxy();
@@ -72,9 +68,9 @@ public final class ProxyManager {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (CONFIG.hasProxyAuth()) {
String username = CONFIG.getProxyUser();
String password = CONFIG.getProxyPass();
if (config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
if (username != null && password != null) {
return new PasswordAuthentication(username, password.toCharArray());
}
@@ -17,149 +17,89 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class Settings {
public static final Settings INSTANCE = new Settings();
private static Settings instance;
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
public static Settings instance() {
if (instance == null) {
throw new IllegalStateException("Settings hasn't been initialized");
}
return instance;
}
private final boolean firstLaunch;
private InvalidationListener accountChangeListener =
source -> CONFIG.getAccounts().setAll(
accounts.values().stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
return storage;
})
.collect(toList()));
/**
* Should be called from {@link ConfigHolder#init()}.
*/
static void init() {
instance = new Settings();
}
private Settings() {
firstLaunch = CONFIG.isFirstLaunch();
CONFIG.setFirstLaunch(false);
ProxyManager.getProxy(); // init ProxyManager
for (Iterator<Map<Object, Object>> iterator = CONFIG.getAccounts().iterator(); iterator.hasNext();) {
Map<Object, Object> settings = iterator.next();
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(tryCast(settings.get("type"), String.class).orElse(""));
if (factory == null) {
LOG.warning("Unrecognized account type, removing: " + settings);
iterator.remove();
continue;
}
Account account;
try {
account = factory.fromStorage(settings);
} catch (Exception e) {
LOG.log(Level.WARNING, "Malformed account storage, removing: " + settings, e);
iterator.remove();
continue;
}
accounts.put(Accounts.getAccountId(account), account);
account.addListener(accountChangeListener);
}
CONFIG.getAuthlibInjectorServers().addListener(onInvalidating(this::removeDanglingAuthlibInjectorAccounts));
this.selectedAccount.set(accounts.get(CONFIG.getSelectedAccount()));
ProxyManager.init();
Accounts.init();
checkProfileMap();
save();
for (Map.Entry<String, Profile> profileEntry : getProfileMap().entrySet()) {
profileEntry.getValue().setName(profileEntry.getKey());
profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged);
profileEntry.getValue().addPropertyChangedListener(e -> save());
profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.saveConfig());
}
Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save)));
CONFIG.addListener(source -> save());
}
private void save() {
ConfigHolder.saveConfig(CONFIG);
}
public boolean isFirstLaunch() {
return firstLaunch;
}
private Locales.SupportedLocale locale = Locales.getLocaleByName(CONFIG.getLocalization());
public Locales.SupportedLocale getLocale() {
return locale;
}
public void setLocale(Locales.SupportedLocale locale) {
this.locale = locale;
CONFIG.setLocalization(Locales.getNameByLocale(locale));
config().addListener(source -> ConfigHolder.saveConfig());
}
public Font getFont() {
return Font.font(CONFIG.getFontFamily(), CONFIG.getFontSize());
return Font.font(config().getFontFamily(), config().getFontSize());
}
public void setFont(Font font) {
CONFIG.setFontFamily(font.getFamily());
CONFIG.setFontSize(font.getSize());
config().setFontFamily(font.getFamily());
config().setFontSize(font.getSize());
}
public int getLogLines() {
return Math.max(CONFIG.getLogLines(), 100);
return Math.max(config().getLogLines(), 100);
}
public void setLogLines(int logLines) {
CONFIG.setLogLines(logLines);
config().setLogLines(logLines);
}
/****************************************
* AUTHLIB INJECTORS *
****************************************/
public boolean isCommonDirectoryDisabled() {
return config().getCommonDirType() == EnumCommonDirectory.DISABLED;
}
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
* Don't call this before {@link #migrateAuthlibInjectorServers()} is called, otherwise old data would be lost.
*/
private void removeDanglingAuthlibInjectorAccounts() {
accounts.values().stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.filter(it -> !CONFIG.getAuthlibInjectorServers().contains(((AuthlibInjectorAccount) it).getServer()))
.collect(toList())
.forEach(this::deleteAccount);
public static String getDefaultCommonDirectory() {
return Launcher.MINECRAFT_DIRECTORY.getAbsolutePath();
}
public String getCommonDirectory() {
switch (config().getCommonDirType()) {
case DISABLED:
return null;
case DEFAULT:
return getDefaultCommonDirectory();
case CUSTOM:
return config().getCommonDirectory();
default:
return null;
}
}
/****************************************
@@ -167,94 +107,14 @@ public class Settings {
****************************************/
public DownloadProvider getDownloadProvider() {
return DownloadProviders.getDownloadProvider(CONFIG.getDownloadType());
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);
}
/****************************************
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", null) {
@Override
public Account get() {
Account a = super.get();
if (a == null || !accounts.containsKey(Accounts.getAccountId(a))) {
Account acc = accounts.values().stream().findAny().orElse(null);
set(acc);
return acc;
} else return a;
}
@Override
public void set(Account newValue) {
if (newValue == null || accounts.containsKey(Accounts.getAccountId(newValue))) {
super.set(newValue);
}
}
@Override
public void invalidated() {
super.invalidated();
CONFIG.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
}
};
public Account getSelectedAccount() {
return selectedAccount.get();
}
public ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
public void setSelectedAccount(Account selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public void addAccount(Account account) {
accounts.put(Accounts.getAccountId(account), account);
account.addListener(accountChangeListener);
accountChangeListener.invalidated(account);
onAccountLoading();
EventBus.EVENT_BUS.fireEvent(new AccountAddedEvent(this, account));
}
public Account getAccount(String name, String character) {
return accounts.get(Accounts.getAccountId(name, character));
}
public Collection<Account> getAccounts() {
return Collections.unmodifiableCollection(accounts.values());
}
public void deleteAccount(String name, String character) {
Account removed = accounts.remove(Accounts.getAccountId(name, character));
if (removed != null) {
removed.removeListener(accountChangeListener);
accountChangeListener.invalidated(removed);
onAccountLoading();
selectedAccount.get();
}
}
public void deleteAccount(Account account) {
accounts.remove(Accounts.getAccountId(account));
account.removeListener(accountChangeListener);
accountChangeListener.invalidated(account);
onAccountLoading();
selectedAccount.get();
config().setDownloadType(index);
}
/****************************************
@@ -264,18 +124,18 @@ public class Settings {
public Profile getSelectedProfile() {
checkProfileMap();
if (!hasProfile(CONFIG.getSelectedProfile())) {
if (!hasProfile(config().getSelectedProfile())) {
getProfileMap().keySet().stream().findFirst().ifPresent(selectedProfile -> {
CONFIG.setSelectedProfile(selectedProfile);
config().setSelectedProfile(selectedProfile);
});
Schedulers.computation().schedule(this::onProfileChanged);
}
return getProfile(CONFIG.getSelectedProfile());
return getProfile(config().getSelectedProfile());
}
public void setSelectedProfile(Profile selectedProfile) {
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), CONFIG.getSelectedProfile())) {
CONFIG.setSelectedProfile(selectedProfile.getName());
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), config().getSelectedProfile())) {
config().setSelectedProfile(selectedProfile.getName());
Schedulers.computation().schedule(this::onProfileChanged);
}
}
@@ -292,7 +152,7 @@ public class Settings {
}
public Map<String, Profile> getProfileMap() {
return CONFIG.getConfigurations();
return config().getConfigurations();
}
public Collection<Profile> getProfiles() {
@@ -347,8 +207,4 @@ public class Settings {
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles()));
onProfileChanged();
}
public void onAccountLoading() {
EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts()));
}
}
@@ -27,7 +27,7 @@ import org.jackhuang.hmcl.util.Logging;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.io.File;
import java.io.IOException;
@@ -120,7 +120,7 @@ public class Theme {
}
public static ObjectBinding<Color> foregroundFillBinding() {
return Bindings.createObjectBinding(() -> CONFIG.getTheme().getForegroundColor(), CONFIG.themeProperty());
return Bindings.createObjectBinding(() -> config().getTheme().getForegroundColor(), config().themeProperty());
}
public static ObjectBinding<Color> blackFillBinding() {
@@ -30,7 +30,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/**
*
@@ -81,7 +81,7 @@ public final class VersionSetting {
}
/**
* Java version or null if user customizes java directory.
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
*/
public String getJava() {
return javaProperty.get();
@@ -91,6 +91,15 @@ public final class VersionSetting {
javaProperty.set(java);
}
public boolean isUsesCustomJavaDir() {
return "Custom".equals(getJava());
}
public void setUsesCustomJavaDir() {
setJava("Custom");
setDefaultJavaPath(null);
}
private final ImmediateStringProperty defaultJavaPathProperty = new ImmediateStringProperty(this, "defaultJavaPath", "");
/**
@@ -278,23 +287,6 @@ public final class VersionSetting {
notCheckGameProperty.set(notCheckGame);
}
private final ImmediateBooleanProperty noCommonProperty = new ImmediateBooleanProperty(this, "noCommon", false);
public ImmediateBooleanProperty noCommonProperty() {
return noCommonProperty;
}
/**
* True if HMCL does not find/download libraries in/to common path.
*/
public boolean isNoCommon() {
return noCommonProperty.get();
}
public void setNoCommon(boolean noCommon) {
noCommonProperty.set(noCommon);
}
private final ImmediateBooleanProperty showLogsProperty = new ImmediateBooleanProperty(this, "showLogs", false);
public ImmediateBooleanProperty showLogsProperty() {
@@ -455,7 +447,7 @@ public final class VersionSetting {
if (StringUtils.isBlank(getJava()))
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
else if ("Custom".equals(getJava())) {
else if (isUsesCustomJavaDir()) {
try {
return JavaVersion.fromExecutable(new File(getJavaDir()));
} catch (IOException e) {
@@ -477,6 +469,11 @@ public final class VersionSetting {
} else throw new Error();
}
public void setJavaVersion(JavaVersion java) {
setJava(java.getVersion());
setDefaultJavaPath(java.getBinary().toString());
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
@@ -490,7 +487,6 @@ public final class VersionSetting {
minecraftArgsProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
noCommonProperty.addListener(listener);
showLogsProperty.addListener(listener);
serverIpProperty.addListener(listener);
fullscreenProperty.addListener(listener);
@@ -519,10 +515,10 @@ public final class VersionSetting {
.setFullscreen(isFullscreen())
.setServerIp(getServerIp())
.setWrapper(getWrapper())
.setProxyHost(CONFIG.getProxyHost())
.setProxyPort(CONFIG.getProxyPort())
.setProxyUser(CONFIG.getProxyUser())
.setProxyPass(CONFIG.getProxyPass())
.setProxyHost(config().getProxyHost())
.setProxyPort(config().getProxyPort())
.setProxyUser(config().getProxyUser())
.setProxyPass(config().getProxyPass())
.setPrecalledCommand(getPreLaunchCommand())
.setNoGeneratedJVMArgs(isNoJVMArgs())
.create();
@@ -555,7 +551,6 @@ public final class VersionSetting {
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("noCommon", src.isNoCommon());
obj.addProperty("showLogs", src.isShowLogs());
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
@@ -593,7 +588,6 @@ public final class VersionSetting {
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoCommon(Optional.ofNullable(obj.get("noCommon")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
@@ -35,7 +35,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.ComponentList;
@@ -97,7 +97,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), 15, 15));
lblCharacter.setText(account.getCharacter());
lblType.setText(AddAccountPane.accountType(account));
lblType.setText(Accounts.getAccountTypeName(account));
lblEmail.setText(account.getUsername());
btnRefresh.setVisible(account instanceof YggdrasilAccount);
@@ -105,7 +105,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
@FXML
private void onDelete() {
Settings.INSTANCE.deleteAccount(account);
Accounts.getAccounts().remove(account);
Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run);
}
@@ -21,7 +21,7 @@ import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
@@ -33,15 +33,12 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
@@ -51,24 +48,25 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Logging;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
public class AddAccountPane extends StackPane {
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<AccountFactory<?>> cboType;
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@@ -81,27 +79,35 @@ public class AddAccountPane extends StackPane {
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), CONFIG.getAuthlibInjectorServers());
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboType.getItems().setAll(i18n("account.methods.offline"), i18n("account.methods.yggdrasil"), i18n("account.methods.authlib_injector"));
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR);
cboType.setConverter(stringConverter(Accounts::getAccountTypeName));
cboType.getSelectionModel().select(0);
ReadOnlyIntegerProperty loginTypeIdProperty = cboType.getSelectionModel().selectedIndexProperty();
ReadOnlyObjectProperty<AccountFactory<?>> loginType = cboType.getSelectionModel().selectedItemProperty();
txtPassword.visibleProperty().bind(loginTypeIdProperty.isNotEqualTo(0));
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
cboServers.visibleProperty().bind(loginTypeIdProperty.isEqualTo(2));
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
linkManageInjectorServers.visibleProperty().bind(cboServers.visibleProperty());
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
() -> !txtUsername.validate() || (loginTypeIdProperty.get() != 0 && !txtPassword.validate()),
txtUsername.textProperty(), txtPassword.textProperty(), loginTypeIdProperty));
() -> !( // consider the opposite situation: input is valid
txtUsername.validate() &&
// invisible means the field is not needed, neither should it be validated
(!txtPassword.isVisible() || txtPassword.validate()) &&
(!cboServers.isVisible() || cboServers.getSelectionModel().getSelectedItem() != null)
),
txtUsername.textProperty(),
txtPassword.textProperty(), txtPassword.visibleProperty(),
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
}
/**
@@ -113,47 +119,48 @@ public class AddAccountPane extends StackPane {
}
}
/**
* Gets the additional data that needs to be passed into {@link AccountFactory#create(CharacterSelector, String, String, Object)}.
*/
private Object getAuthAdditionalData() {
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
if (factory == Accounts.FACTORY_AUTHLIB_INJECTOR) {
return requireNonNull(cboServers.getSelectionModel().getSelectedItem(), "selected server cannot be null");
}
return null;
}
@FXML
private void onCreationAccept() {
if (btnAccept.isDisabled())
return;
String username = txtUsername.getText();
String password = txtPassword.getText();
Object addtionalData;
int type = cboType.getSelectionModel().getSelectedIndex();
AccountFactory<?> factory;
switch (type) {
case 0:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY);
addtionalData = null;
break;
case 1:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY);
addtionalData = null;
break;
case 2:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY);
Optional<AuthlibInjectorServer> server = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem());
if (server.isPresent()) {
addtionalData = server.get();
} else {
lblCreationWarning.setText(i18n("account.failed.no_selected_server"));
return;
}
break;
default:
throw new Error();
}
acceptPane.showSpinner();
lblCreationWarning.setText("");
setDisable(true);
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, addtionalData))
String username = txtUsername.getText();
String password = txtPassword.getText();
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
Object additionalData = getAuthAdditionalData();
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, additionalData))
.finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account"));
Account account = variables.get("create_account");
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
} else {
// adding an already-added account
// instead of discarding the new account, we first remove the existing one then add the new one
Accounts.getAccounts().remove(oldIndex);
Accounts.getAccounts().add(oldIndex, account);
}
// select the new account
Accounts.setSelectedAccount(account);
acceptPane.hideSpinner();
fireEvent(new DialogCloseEvent());
}, exception -> {
@@ -256,6 +263,8 @@ public class AddAccountPane extends StackPane {
return i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
return i18n("account.failed.connect_authentication_server");
} else if (exception instanceof ServerResponseMalformedException) {
return i18n("account.failed.server_response_malformed");
} else if (exception instanceof RemoteAuthenticationException) {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
@@ -272,11 +281,4 @@ public class AddAccountPane extends StackPane {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
public static String accountType(Account account) {
if (account instanceof OfflineAccount) return i18n("account.methods.offline");
else if (account instanceof AuthlibInjectorAccount) return i18n("account.methods.authlib_injector");
else if (account instanceof YggdrasilAccount) return i18n("account.methods.yggdrasil");
else throw new Error(i18n("account.methods.no_method") + ": " + account);
}
}
@@ -39,7 +39,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class AddAuthlibInjectorServerPane extends StackPane {
@@ -133,8 +133,8 @@ public class AddAuthlibInjectorServerPane extends StackPane {
@FXML
private void onAddFinish() {
if (!CONFIG.getAuthlibInjectorServers().contains(serverBeingAdded)) {
CONFIG.getAuthlibInjectorServers().add(serverBeingAdded);
if (!config().getAuthlibInjectorServers().contains(serverBeingAdded)) {
config().getAuthlibInjectorServers().add(serverBeingAdded);
}
fireEvent(new DialogCloseEvent());
}
@@ -17,23 +17,24 @@
*/
package org.jackhuang.hmcl.ui;
import static java.util.stream.Collectors.toList;
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;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.MappedObservableList;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("account.injector.manage.title"));
@@ -42,26 +43,21 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
@FXML private VBox listPane;
@FXML private StackPane contentPane;
private InvalidationListener serversListener;
private ObservableList<AuthlibInjectorServerItem> serverItems;
public AuthlibInjectorServersPage() {
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
smoothScrolling(scrollPane);
serversListener = observable -> updateServersList();
CONFIG.getAuthlibInjectorServers().addListener(new WeakInvalidationListener(serversListener));
updateServersList();
serverItems = MappedObservableList.create(config().getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(listPane.getChildren(), serverItems);
}
private void updateServersList() {
listPane.getChildren().setAll(
CONFIG.getAuthlibInjectorServers().stream()
.map(server -> new AuthlibInjectorServerItem(server,
item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer())))
.collect(toList()));
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
return new AuthlibInjectorServerItem(server,
item -> config().getAuthlibInjectorServers().remove(item.getServer()));
}
@FXML
private void onAdd() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
@@ -34,7 +34,7 @@ import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.JavaVersion;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.util.function.Consumer;
@@ -102,13 +102,13 @@ public final class Controllers {
decorator.showPage(null);
leftPaneController = new LeftPaneController(decorator.getLeftPane());
Settings.INSTANCE.onProfileLoading();
Settings.instance().onProfileLoading();
Task.of(JavaVersion::initialize).start();
decorator.setCustomMaximize(false);
scene = new Scene(decorator, 804, 521);
scene.getStylesheets().setAll(CONFIG.getTheme().getStylesheets());
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
stage.setMinWidth(804);
stage.setMaxWidth(804);
stage.setMinHeight(521);
@@ -55,8 +55,8 @@ import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -80,7 +80,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
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.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
public final class Decorator extends StackPane implements TaskExecutorDialogWizardDisplayer {
@@ -199,7 +199,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
);
nowAnimation.play();
});
if (!Settings.INSTANCE.isFirstLaunch() || Settings.INSTANCE.getLocale().getLocale() != Locale.CHINA)
if (!ConfigHolder.isNewlyCreated() || config().getLocalization().getLocale() != Locale.CHINA)
drawerWrapper.getChildren().remove(welcomeView);
if (!min) buttonsContainer.getChildren().remove(btnMin);
@@ -227,8 +227,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
Bindings.createObjectBinding(
() -> {
Image image = null;
if (CONFIG.getBackgroundImageType() == EnumBackgroundImage.CUSTOM) {
image = tryLoadImage(Paths.get(CONFIG.getBackgroundImage()))
if (config().getBackgroundImageType() == EnumBackgroundImage.CUSTOM && config().getBackgroundImage() != null) {
image = tryLoadImage(Paths.get(config().getBackgroundImage()))
.orElse(null);
}
if (image == null) {
@@ -236,8 +236,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
},
CONFIG.backgroundImageTypeProperty(),
CONFIG.backgroundImageProperty()));
config().backgroundImageTypeProperty(),
config().backgroundImageProperty()));
}
private Image defaultBackground = new Image("/assets/img/background.jpg");
@@ -21,6 +21,12 @@ import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
@@ -49,21 +55,39 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lang;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.util.MappedObservableList;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static javafx.collections.FXCollections.singletonObservableList;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LeftPaneController {
private final AdvancedListBox leftPane;
private final VBox profilePane = new VBox();
private final VBox accountPane = new VBox();
private final IconedItem launcherSettingsItem;
private final VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
private final HashMap<Account, VersionListItem> items = new HashMap<>();
private ListProperty<RipplerContainer> accountItems = new SimpleListProperty<>();
private ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
{
accountItems.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
Account selected = get();
accountItems.forEach(item -> item.setSelected(
getAccountFromItem(item)
.map(it -> it == selected)
.orElse(false)));
}
};
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
@@ -90,30 +114,91 @@ public final class LeftPaneController {
})))
.add(profilePane);
EventBus.EVENT_BUS.channel(AccountAddedEvent.class).register(this::onAccountAdd);
EventBus.EVENT_BUS.channel(AccountLoadingEvent.class).register(this::onAccountsLoading);
// ==== Accounts ====
// Missing account item
VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
RipplerContainer missingAccountRippler = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
missingAccountRippler.setOnMouseClicked(e -> addNewAccount());
accountItems.bind(
new When(Accounts.accountsProperty().emptyProperty())
.then(singletonObservableList(missingAccountRippler))
.otherwise(MappedObservableList.create(Accounts.getAccounts(), this::createAccountItem)));
Bindings.bindContent(accountPane.getChildren(), accountItems);
selectedAccount.bindBidirectional(Accounts.selectedAccountProperty());
// ====
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions);
}
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), this::onSelectedAccountChanged);
onAccountsLoading();
// ==== Accounts ====
private Optional<Account> getAccountFromItem(RipplerContainer accountItem) {
return Optional.ofNullable(accountItem.getProperties().get("account"))
.map(Account.class::cast);
}
private static String accountSubtitle(Account account) {
if (account instanceof OfflineAccount)
return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount)
return account.getUsername();
else
return "";
}
private RipplerContainer createAccountItem(Account account) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountSubtitle(account));
RipplerContainer rippler = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
JFXPopup popup = new JFXPopup(accountPage);
accountPage.setOnDelete(popup::hide);
popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
});
rippler.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
selectedAccount.set(account);
}
});
rippler.getProperties().put("account", account);
rippler.maxWidthProperty().bind(leftPane.widthProperty());
if (account instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else {
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
}
if (account instanceof AuthlibInjectorAccount) {
FXUtils.installTooltip(rippler, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName()));
}
// update skin
if (account instanceof YggdrasilAccount) {
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
});
}
return rippler;
}
public void checkAccount() {
if (Accounts.getAccounts().isEmpty())
addNewAccount();
}
private void addNewAccount() {
Controllers.dialog(new AddAccountPane());
}
private void onSelectedAccountChanged(Account newAccount) {
Platform.runLater(() -> {
for (Node node : accountPane.getChildren()) {
if (node instanceof RipplerContainer && node.getProperties().get("account") instanceof Account) {
boolean current = Objects.equals(node.getProperties().get("account"), newAccount);
((RipplerContainer) node).setSelected(current);
}
}
});
}
// ====
private void onProfileChanged(ProfileChangedEvent event) {
Profile profile = event.getProfile();
@@ -131,11 +216,11 @@ public final class LeftPaneController {
private void onProfilesLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
for (Profile profile : Settings.INSTANCE.getProfiles()) {
for (Profile profile : Settings.instance().getProfiles()) {
VersionListItem item = new VersionListItem(Profiles.getProfileDisplayName(profile));
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
ripplerContainer.setOnMouseClicked(e -> Settings.INSTANCE.setSelectedProfile(profile));
ripplerContainer.setOnMouseClicked(e -> Settings.instance().setSelectedProfile(profile));
ripplerContainer.getProperties().put("profile", profile.getName());
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
list.add(ripplerContainer);
@@ -143,70 +228,6 @@ public final class LeftPaneController {
Platform.runLater(() -> profilePane.getChildren().setAll(list));
}
private static String accountType(Account account) {
if (account instanceof OfflineAccount) return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount) return account.getUsername();
else throw new Error(i18n("account.methods.no_method") + ": " + account);
}
private void onAccountAdd(AccountAddedEvent event) {
Account account = event.getAccount();
VersionListItem item = items.get(account);
if (account instanceof YggdrasilAccount)
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
});
}
private void onAccountsLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
items.clear();
Account selectedAccount = Settings.INSTANCE.getSelectedAccount();
for (Account account : Settings.INSTANCE.getAccounts()) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountType(account));
items.put(account, item);
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
JFXPopup popup = new JFXPopup(accountPage);
accountPage.setOnDelete(popup::hide);
popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
});
ripplerContainer.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY)
Settings.INSTANCE.setSelectedAccount(account);
});
ripplerContainer.getProperties().put("account", account);
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
if (account instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
if (account instanceof AuthlibInjectorAccount) {
FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName()));
}
if (selectedAccount == account)
ripplerContainer.setSelected(true);
list.add(ripplerContainer);
}
if (Settings.INSTANCE.getAccounts().isEmpty()) {
RipplerContainer container = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
container.setOnMouseClicked(e -> addNewAccount());
list.add(container);
}
Platform.runLater(() -> accountPane.getChildren().setAll(list));
}
public void showUpdate() {
launcherSettingsItem.setText(i18n("update.found"));
launcherSettingsItem.setTextFill(Color.RED);
@@ -247,9 +268,4 @@ public final class LeftPaneController {
}
});
}
public void checkAccount() {
if (Settings.INSTANCE.getAccounts().isEmpty())
addNewAccount();
}
}
@@ -43,7 +43,7 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.concurrent.CountDownLatch;
@@ -65,7 +65,7 @@ public final class LogWindow extends Stage {
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(CONFIG.getTheme().getStylesheets());
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
setTitle(i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
}
@@ -177,12 +177,12 @@ public final class LogWindow extends Stage {
engine = webView.getEngine();
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
.replace("${FONT}", Settings.INSTANCE.getFont().getSize() + "px \"" + Settings.INSTANCE.getFont().getFamily() + "\""));
.replace("${FONT}", Settings.instance().getFont().getSize() + "px \"" + Settings.instance().getFont().getFamily() + "\""));
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
document = engine.getDocument();
body = document.getElementsByTagName("body").item(0);
engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
engine.executeScript("limitedLogs=" + Settings.instance().getLogLines());
latch.countDown();
onDone.fireEvent(new Event(LogWindow.this));
}
@@ -190,14 +190,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(Settings.instance().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());
Settings.instance().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
engine.executeScript("limitedLogs=" + Settings.instance().getLogLines());
});
if (!flag)
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
@@ -48,6 +49,8 @@ import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.OperatingSystem;
import org.jackhuang.hmcl.util.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.util.List;
@@ -98,7 +101,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), i18n("install")));
FXUtils.installTooltip(btnAdd, i18n("install"));
btnRefresh.setOnMouseClicked(e -> Settings.INSTANCE.getSelectedProfile().getRepository().refreshVersionsAsync().start());
btnRefresh.setOnMouseClicked(e -> Settings.instance().getSelectedProfile().getRepository().refreshVersionsAsync().start());
FXUtils.installTooltip(btnRefresh, i18n("button.refresh"));
}
@@ -132,13 +135,13 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
item.setVersionName(id);
item.setOnLaunchButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
if (Accounts.getSelectedAccount() == null)
Controllers.getLeftPaneController().checkAccount();
else
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null);
});
item.setOnScriptButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
if (Accounts.getSelectedAccount() == null)
Controllers.dialog(i18n("login.empty_username"));
else {
FileChooser chooser = new FileChooser();
@@ -150,7 +153,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
File file = chooser.showSaveDialog(Controllers.getStage());
if (file != null)
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, file);
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, file);
}
});
item.setOnSettingsButtonClicked(e -> {
@@ -214,10 +217,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
if (Settings.INSTANCE.getSelectedAccount() == null)
if (Accounts.getSelectedAccount() == null)
Controllers.dialog(i18n("login.empty_username"));
else
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null);
}
});
File iconFile = repository.getVersionIcon(id);
@@ -233,6 +236,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId())))
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
@@ -80,7 +80,7 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
@FXML
private void onDelete() {
if (profile != null) {
Settings.INSTANCE.deleteProfile(profile);
Settings.instance().deleteProfile(profile);
Controllers.navigate(null);
}
}
@@ -99,10 +99,10 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
}
Profile newProfile = new Profile(txtProfileName.getText(), new File(getLocation()));
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
Settings.INSTANCE.putProfile(newProfile);
Settings.instance().putProfile(newProfile);
}
Settings.INSTANCE.onProfileLoading();
Settings.instance().onProfileLoading();
Controllers.navigate(null);
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -38,7 +39,6 @@ import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.construct.FontComboBox;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.Validator;
@@ -46,12 +46,14 @@ import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.i18n.Locales;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.net.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
public final class SettingsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("settings.launcher"));
@@ -73,7 +75,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private FontComboBox cboFont;
@FXML
private FileItem fileCommonLocation;
private MultiFileItem<EnumCommonDirectory> fileCommonLocation;
@FXML
private Label lblDisplay;
@FXML
@@ -85,7 +87,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private ScrollPane scroll;
@FXML
private MultiFileItem backgroundItem;
private MultiFileItem<EnumBackgroundImage> backgroundItem;
@FXML
private StackPane themeColorPickerContainer;
@FXML
@@ -101,54 +103,56 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private Pane proxyPane;
{
private ObjectProperty<Proxy.Type> selectedProxyType;
public SettingsPage() {
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
FXUtils.smoothScrolling(scroll);
cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.INSTANCE.getDownloadProvider()));
cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue())));
cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.instance().getDownloadProvider()));
cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.instance().setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue())));
cboFont.getSelectionModel().select(Settings.INSTANCE.getFont().getFamily());
cboFont.getSelectionModel().select(Settings.instance().getFont().getFamily());
cboFont.valueProperty().addListener((a, b, newValue) -> {
Font font = Font.font(newValue, Settings.INSTANCE.getFont().getSize());
Settings.INSTANCE.setFont(font);
Font font = Font.font(newValue, Settings.instance().getFont().getSize());
Settings.instance().setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
});
txtFontSize.setText(Double.toString(Settings.INSTANCE.getFont().getSize()));
txtFontSize.setText(Double.toString(Settings.instance().getFont().getSize()));
txtFontSize.getValidators().add(new Validator(it -> Lang.toDoubleOrNull(it) != null));
txtFontSize.textProperty().addListener((a, b, newValue) -> {
if (txtFontSize.validate()) {
Font font = Font.font(Settings.INSTANCE.getFont().getFamily(), Double.parseDouble(newValue));
Settings.INSTANCE.setFont(font);
Font font = Font.font(Settings.instance().getFont().getFamily(), Double.parseDouble(newValue));
Settings.instance().setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
}
});
lblDisplay.setStyle("-fx-font: " + Settings.INSTANCE.getFont().getSize() + " \"" + Settings.INSTANCE.getFont().getFamily() + "\";");
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(Settings.INSTANCE.getLocale().getResourceBundle())));
list.add(new Label(locale.getName(config().getLocalization().getResourceBundle())));
cboLanguage.setItems(list);
cboLanguage.getSelectionModel().select(Locales.LOCALES.indexOf(Settings.INSTANCE.getLocale()));
cboLanguage.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setLocale(Locales.getLocale(newValue.intValue())));
cboLanguage.getSelectionModel().select(Locales.LOCALES.indexOf(config().getLocalization()));
cboLanguage.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> config().setLocalization(Locales.getLocale(newValue.intValue())));
// ==== Proxy ====
txtProxyHost.textProperty().bindBidirectional(CONFIG.proxyHostProperty());
txtProxyPort.textProperty().bindBidirectional(CONFIG.proxyPortProperty());
txtProxyUsername.textProperty().bindBidirectional(CONFIG.proxyUserProperty());
txtProxyPassword.textProperty().bindBidirectional(CONFIG.proxyPassProperty());
txtProxyHost.textProperty().bindBidirectional(config().proxyHostProperty());
txtProxyPort.textProperty().bindBidirectional(config().proxyPortProperty());
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
txtProxyPassword.textProperty().bindBidirectional(config().proxyPassProperty());
proxyPane.disableProperty().bind(chkEnableProxy.selectedProperty().not());
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
chkEnableProxy.selectedProperty().bindBidirectional(CONFIG.hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(CONFIG.hasProxyAuthProperty());
chkEnableProxy.selectedProperty().bindBidirectional(config().hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
ObjectProperty<Proxy.Type> selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
{
invalidated();
}
@@ -164,7 +168,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
}
}
};
selectedProxyType.bindBidirectional(CONFIG.proxyTypeProperty());
selectedProxyType.bindBidirectional(config().proxyTypeProperty());
ToggleGroup proxyConfigurationGroup = new ToggleGroup();
chkProxyHttp.setUserData(Proxy.Type.HTTP);
@@ -179,51 +183,45 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
}));
// ====
fileCommonLocation.pathProperty().bindBidirectional(CONFIG.commonDirectoryProperty());
fileCommonLocation.loadChildren(Arrays.asList(
fileCommonLocation.createChildren(i18n("launcher.common_directory.disabled"), EnumCommonDirectory.DISABLED),
fileCommonLocation.createChildren(i18n("launcher.common_directory.default"), EnumCommonDirectory.DEFAULT)
), EnumCommonDirectory.CUSTOM);
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocation.customTextProperty().bindBidirectional(config().commonDirectoryProperty());
fileCommonLocation.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.common_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
checkUpdate();
// background
// ==== Background ====
backgroundItem.loadChildren(Collections.singletonList(
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
));
backgroundItem.setCustomUserData(EnumBackgroundImage.CUSTOM);
backgroundItem.getTxtCustom().textProperty().bindBidirectional(CONFIG.backgroundImageProperty());
ObjectProperty<EnumBackgroundImage> backgroundType = new SimpleObjectProperty<EnumBackgroundImage>(EnumBackgroundImage.DEFAULT) {
{
invalidated();
}
@Override
protected void invalidated() {
backgroundItem.getGroup().getToggles().stream()
.filter(it -> it.getUserData() == get())
.findFirst()
.ifPresent(it -> it.setSelected(true));
}
};
backgroundItem.getGroup().selectedToggleProperty().addListener((observable, oldValue, newValue) -> backgroundType.set((EnumBackgroundImage) newValue.getUserData()));
backgroundType.bindBidirectional(CONFIG.backgroundImageTypeProperty());
), EnumBackgroundImage.CUSTOM);
backgroundItem.customTextProperty().bindBidirectional(config().backgroundImageProperty());
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
backgroundItem.subtitleProperty().bind(
new When(backgroundType.isEqualTo(EnumBackgroundImage.DEFAULT))
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
.then(i18n("launcher.background.default"))
.otherwise(CONFIG.backgroundImageProperty()));
.otherwise(config().backgroundImageProperty()));
// ====
// theme
JFXColorPicker picker = new JFXColorPicker(Color.web(CONFIG.getTheme().getColor()), null);
// ==== Theme ====
JFXColorPicker picker = new JFXColorPicker(Color.web(config().getTheme().getColor()), null);
picker.setCustomColorText(i18n("color.custom"));
picker.setRecentColorsText(i18n("color.recent"));
picker.getCustomColors().setAll(Theme.SUGGESTED_COLORS);
picker.setOnAction(e -> {
Theme theme = Theme.custom(Theme.getColorDisplayName(picker.getValue()));
CONFIG.setTheme(theme);
config().setTheme(theme);
Controllers.getScene().getStylesheets().setAll(theme.getStylesheets());
});
themeColorPickerContainer.getChildren().setAll(picker);
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
// ====
}
public String getTitle() {
@@ -71,7 +71,7 @@ public final class VersionItem extends StackPane {
private void initializeComponents() {
setPickOnBounds(false);
FXUtils.setLimitWidth(this, 160);
FXUtils.setLimitHeight(this, 156);
FXUtils.setLimitHeight(this, 149);
content = new VBox();
{
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
@@ -43,6 +44,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
@@ -70,11 +72,10 @@ public final class VersionSettingsController {
@FXML private JFXCheckBox chkFullscreen;
@FXML private Label lblPhysicalMemory;
@FXML private JFXToggleButton chkNoJVMArgs;
@FXML private JFXToggleButton chkNoCommon;
@FXML private JFXToggleButton chkNoGameCheck;
@FXML private MultiFileItem globalItem;
@FXML private MultiFileItem javaItem;
@FXML private MultiFileItem gameDirItem;
@FXML private MultiFileItem<Boolean> globalItem;
@FXML private MultiFileItem<JavaVersion> javaItem;
@FXML private MultiFileItem<EnumGameDirectory> gameDirItem;
@FXML private JFXToggleButton chkShowLogs;
@FXML private ImagePickerItem iconPickerItem;
@@ -94,9 +95,12 @@ public final class VersionSettingsController {
initializeSelectedJava();
});
javaItem.setSelectedData(null);
javaItem.setFallbackData(JavaVersion.fromCurrentEnvironment());
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaItem.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
gameDirItem.setCustomUserData(EnumGameDirectory.CUSTOM);
gameDirItem.loadChildren(Arrays.asList(
gameDirItem.createChildren(i18n("settings.advanced.game_dir.default"), EnumGameDirectory.ROOT_FOLDER),
gameDirItem.createChildren(i18n("settings.advanced.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER)
@@ -132,15 +136,19 @@ public final class VersionSettingsController {
FXUtils.unbindString(txtServerIP, lastVersionSetting.serverIpProperty());
FXUtils.unbindBoolean(chkFullscreen, lastVersionSetting.fullscreenProperty());
FXUtils.unbindBoolean(chkNoGameCheck, lastVersionSetting.notCheckGameProperty());
FXUtils.unbindBoolean(chkNoCommon, lastVersionSetting.noCommonProperty());
FXUtils.unbindBoolean(chkNoJVMArgs, lastVersionSetting.noJVMArgsProperty());
FXUtils.unbindBoolean(chkShowLogs, lastVersionSetting.showLogsProperty());
FXUtils.unbindEnum(cboLauncherVisibility);
globalItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.usesGlobalProperty());
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().unbind();
}
// unbind data fields
globalItem.setToggleSelectedListener(null);
javaItem.setToggleSelectedListener(null);
gameDirItem.setToggleSelectedListener(null);
// bind new data fields
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
@@ -156,18 +164,15 @@ public final class VersionSettingsController {
FXUtils.bindString(txtServerIP, versionSetting.serverIpProperty());
FXUtils.bindBoolean(chkFullscreen, versionSetting.fullscreenProperty());
FXUtils.bindBoolean(chkNoGameCheck, versionSetting.notCheckGameProperty());
FXUtils.bindBoolean(chkNoCommon, versionSetting.noCommonProperty());
FXUtils.bindBoolean(chkNoJVMArgs, versionSetting.noJVMArgsProperty());
FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty());
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
javaItem.setToggleSelectedListener(newValue -> {
if (javaItem.isCustomToggle(newValue)) {
versionSetting.setJava("Custom");
versionSetting.setDefaultJavaPath(null);
versionSetting.setUsesCustomJavaDir();
} else {
JavaVersion java = (JavaVersion) newValue.getUserData();
versionSetting.setJava(java.getVersion());
versionSetting.setDefaultJavaPath(java.getBinary().toString());
versionSetting.setJavaVersion((JavaVersion) newValue.getUserData());
}
});
@@ -175,13 +180,13 @@ public final class VersionSettingsController {
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
initJavaSubtitle(versionSetting);
if (versionSetting.isUsesGlobal())
globalItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Boolean.TRUE).findFirst().ifPresent(it -> it.setSelected(true));
else
globalItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Boolean.FALSE).findFirst().ifPresent(it -> it.setSelected(true));
globalItem.selectedDataProperty().bindBidirectional(versionSetting.usesGlobalProperty());
globalItem.subtitleProperty().bind(Bindings.createStringBinding(() -> i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special"),
versionSetting.usesGlobalProperty()));
globalItem.setToggleSelectedListener(newValue -> {
// do not call versionSettings.setUsesGlobal(true/false)
// because versionSettings can be the global one.
// global versionSettings.usesGlobal is always true.
if ((Boolean) newValue.getUserData())
profile.globalizeVersionSetting(versionId);
else
@@ -190,19 +195,9 @@ public final class VersionSettingsController {
Platform.runLater(() -> loadVersionSetting(profile, versionId));
});
versionSetting.usesGlobalProperty().setChangedListenerAndOperate(it ->
globalItem.setSubtitle(i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special")));
gameDirItem.getGroup().getToggles().stream()
.filter(it -> it.getUserData() == versionSetting.getGameDirType())
.findFirst().ifPresent(toggle -> toggle.setSelected(true));
gameDirItem.setCustomUserData(EnumGameDirectory.CUSTOM);
gameDirItem.setToggleSelectedListener(newValue -> versionSetting.setGameDirType((EnumGameDirectory) newValue.getUserData()));
versionSetting.gameDirProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
versionSetting.gameDirTypeProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
initGameDirSubtitle(versionSetting);
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
lastVersionSetting = versionSetting;
@@ -217,30 +212,14 @@ public final class VersionSettingsController {
return;
}
List<Toggle> toggles = javaItem.getGroup().getToggles();
if ("Custom".equals(lastVersionSetting.getJava())) {
toggles.stream()
.filter(javaItem::isCustomToggle)
.findFirst()
.get().setSelected(true);
if (lastVersionSetting.isUsesCustomJavaDir()) {
javaItem.setSelectedData(null);
} else {
JavaVersion selectedJava;
try {
selectedJava = lastVersionSetting.getJavaVersion();
javaItem.setSelectedData(lastVersionSetting.getJavaVersion());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
toggles.stream()
.filter(it -> it.getUserData() == selectedJava)
.findFirst()
.orElseGet( // fallback to select current java
() -> toggles.stream()
.filter(it -> it.getUserData() == JavaVersion.fromCurrentEnvironment())
.findFirst()
.get())
.setSelected(true);
;
}
}
@@ -251,10 +230,6 @@ public final class VersionSettingsController {
.map(JavaVersion::getBinary).map(File::getAbsolutePath).orElse("Invalid Java Directory"))));
}
private void initGameDirSubtitle(VersionSetting versionSetting) {
gameDirItem.setSubtitle(profile.getRepository().getRunDirectory(versionId).getAbsolutePath());
}
@FXML
private void onExploreIcon() {
FileChooser chooser = new FileChooser();
@@ -22,14 +22,14 @@ 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.setting.ConfigHolder.config;
public class WebStage extends Stage {
private final WebView webView = new WebView();
public WebStage() {
setScene(new Scene(webView, 800, 480));
getScene().getStylesheets().addAll(CONFIG.getTheme().getStylesheets());
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
getIcons().add(new Image("/assets/img/icon.png"));
}
@@ -21,10 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.NamedArg;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
@@ -46,12 +43,15 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
public class MultiFileItem extends ComponentList {
public class MultiFileItem<T> extends ComponentList {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
private final BooleanProperty directory = new SimpleBooleanProperty(this, "directory", false);
private final ObjectProperty<T> selectedData = new SimpleObjectProperty<>(this, "selectedData");
private final ObjectProperty<T> fallbackData = new SimpleObjectProperty<>(this, "fallbackData");
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
private final ToggleGroup group = new ToggleGroup();
@@ -64,6 +64,7 @@ public class MultiFileItem extends ComponentList {
private Consumer<Toggle> toggleSelectedListener;
@SuppressWarnings("unchecked")
public MultiFileItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
this.hasCustom = hasCustom;
@@ -110,6 +111,20 @@ public class MultiFileItem extends ComponentList {
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
selectedData.set((T) newValue.getUserData());
});
selectedData.addListener((a, b, newValue) -> {
Optional<Toggle> selecting = group.getToggles().stream()
.filter(it -> it.getUserData() == newValue)
.findFirst();
if (!selecting.isPresent()) {
selecting = group.getToggles().stream()
.filter(it -> it.getUserData() == getFallbackData())
.findFirst();
}
selecting.ifPresent(toggle -> toggle.setSelected(true));
});
}
@@ -117,11 +132,11 @@ public class MultiFileItem extends ComponentList {
return createChildren(title, null);
}
public Node createChildren(String title, Object userData) {
public Node createChildren(String title, T userData) {
return createChildren(title, "", userData);
}
public Node createChildren(String title, String subtitle, Object userData) {
public Node createChildren(String title, String subtitle, T userData) {
BorderPane pane = new BorderPane();
pane.setStyle("-fx-padding: 3;");
FXUtils.setLimitHeight(pane, 20);
@@ -146,12 +161,9 @@ public class MultiFileItem extends ComponentList {
pane.getChildren().add(custom);
}
public void onExploreJavaDir() {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(i18n(getChooserTitle()));
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
txtCustom.setText(selectedDir.getAbsolutePath());
public void loadChildren(Collection<Node> list, T customUserData) {
loadChildren(list);
setCustomUserData(customUserData);
}
public ToggleGroup getGroup() {
@@ -182,7 +194,7 @@ public class MultiFileItem extends ComponentList {
this.chooserTitle.set(chooserTitle);
}
public void setCustomUserData(Object userData) {
public void setCustomUserData(T userData) {
radioCustom.setUserData(userData);
}
@@ -225,4 +237,28 @@ public class MultiFileItem extends ComponentList {
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters;
}
public T getSelectedData() {
return selectedData.get();
}
public ObjectProperty<T> selectedDataProperty() {
return selectedData;
}
public void setSelectedData(T selectedData) {
this.selectedData.set(selectedData);
}
public T getFallbackData() {
return fallbackData.get();
}
public ObjectProperty<T> fallbackDataProperty() {
return fallbackData;
}
public void setFallbackData(T fallbackData) {
this.fallbackData.set(fallbackData);
}
}
@@ -41,7 +41,7 @@ public final class DownloadWizardProvider implements WizardProvider {
@Override
public void start(Map<String, Object> settings) {
profile = Settings.INSTANCE.getSelectedProfile();
profile = Settings.instance().getSelectedProfile();
settings.put(PROFILE, profile);
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
@@ -100,9 +101,10 @@ public final class InstallerWizardProvider implements WizardProvider {
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
DownloadProvider provider = profile.getDependency().getDownloadProvider();
switch (step) {
case 0:
return new AdditionalInstallersPage(this, controller, profile.getRepository(), BMCLAPIDownloadProvider.INSTANCE);
return new AdditionalInstallersPage(this, controller, profile.getRepository(), provider);
default:
throw new IllegalStateException();
}
@@ -37,7 +37,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class ExportWizardProvider implements WizardProvider {
private final Profile profile;
@@ -83,11 +83,11 @@ public final class ExportWizardProvider implements WizardProvider {
try (ZipEngine zip = new ZipEngine(modpackFile)) {
Config exported = new Config();
exported.setBackgroundImageType(CONFIG.getBackgroundImageType());
exported.setBackgroundImage(CONFIG.getBackgroundImage());
exported.setTheme(CONFIG.getTheme());
exported.setDownloadType(CONFIG.getDownloadType());
exported.getAuthlibInjectorServers().setAll(CONFIG.getAuthlibInjectorServers());
exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage());
exported.setTheme(config().getTheme());
exported.setDownloadType(config().getDownloadType());
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
zip.putFile(tempModpack, "modpack.zip");
@@ -28,7 +28,7 @@ import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -67,7 +67,7 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
txtModpackName.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.textProperty().addListener(e -> checkValidation());
txtModpackVersion.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.setText(Optional.ofNullable(Settings.INSTANCE.getSelectedAccount()).map(Account::getUsername).orElse(""));
txtModpackAuthor.setText(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
lblVersionName.setText(version);
List<File> launcherJar = Launcher.getCurrentJarFiles();
@@ -23,24 +23,25 @@ import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.logging.Level;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
public final class I18n {
private I18n() {}
private static ResourceBundle RESOURCE_BUNDLE = null;
static {
private static SupportedLocale getCurrentLocale() {
try {
RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
} catch (Throwable e) {
LOG.log(Level.SEVERE, "Settings cannot be initialized", e);
return ConfigHolder.config().getLocalization();
} catch (IllegalStateException e) {
// e is thrown by ConfigHolder.config(), indicating the config hasn't been loaded
// fallback to use default locale
return Locales.DEFAULT;
}
}
public static ResourceBundle getResourceBundle() {
return RESOURCE_BUNDLE == null ? Locales.DEFAULT.getResourceBundle() : RESOURCE_BUNDLE;
return getCurrentLocale().getResourceBundle();
}
public static String i18n(String key, Object... formatArgs) {
@@ -19,6 +19,10 @@ package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.util.Lang;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@@ -107,5 +111,17 @@ public final class Locales {
if (name == null) return resourceBundle.getString("lang");
else return newResourceBundle.getString(name);
}
public static class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
@Override
public void write(JsonWriter out, SupportedLocale value) throws IOException {
out.value(getNameByLocale(value));
}
@Override
public SupportedLocale read(JsonReader in) throws IOException {
return getLocaleByName(in.nextString());
}
}
}
}
@@ -13,7 +13,7 @@
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
<StackPane fx:id="contentPane">
<ScrollPane fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
<JFXMasonryPane fx:id="masonryPane" HSpacing="3" VSpacing="3" cellWidth="182" cellHeight="160">
<JFXMasonryPane fx:id="masonryPane" HSpacing="3" VSpacing="3" cellWidth="182" cellHeight="153">
</JFXMasonryPane>
</ScrollPane>
<VBox style="-fx-padding: 15;" spacing="15" pickOnBounds="false" alignment="BOTTOM_RIGHT">
@@ -31,7 +31,7 @@
</right>
</BorderPane>
<FileItem fx:id="fileCommonLocation" name="%launcher.common_directory" title="%launcher.common_directory.choose" tooltip="%settings.launcher.common_path.tooltip" />
<MultiFileItem fx:id="fileCommonLocation" title="%launcher.common_directory" directory="true" chooserTitle="%launcher.common_directory.choose" hasSubtitle="true" customText="%settings.custom" />
<MultiFileItem fx:id="backgroundItem" title="%launcher.background" chooserTitle="%launcher.background.choose"
hasSubtitle="true" customText="%settings.custom" />
@@ -139,14 +139,6 @@
<JFXToggleButton fx:id="chkNoJVMArgs" size="7" FXUtils.limitHeight="10" />
</right>
</BorderPane>
<BorderPane>
<left>
<Label text="%settings.advanced.no_common"/>
</left>
<right>
<JFXToggleButton fx:id="chkNoCommon" size="7" FXUtils.limitHeight="10" />
</right>
</BorderPane>
<BorderPane>
<left>
<Label text="%settings.advanced.dont_check_game_completeness"/>
@@ -35,12 +35,12 @@ account.choose=Choose a character
account.create=Create a new account
account.email=Email
account.failed.connect_authentication_server=Cannot connect to the authentication server. Check your network.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.failed.invalid_credentials=Incorrect password, or you are forbidden to login temporarily.
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to login.
account.failed.no_character=No character in this account.
account.failed.no_selected_server=No authentication server is selected.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.failed.server_response_malformed=Invalid server response. Is this a valid authentication server?
account.injector.add=Add an authentication server
account.injector.manage=Manage authentication servers
account.injector.manage.title=Authentication servers
@@ -50,7 +50,6 @@ account.injector.server_url=Server URL
account.injector.server_name=Server Name
account.methods=Login Type
account.methods.authlib_injector=authlib-injector
account.methods.no_method=No login method
account.methods.offline=Offline
account.methods.yggdrasil=Mojang
account.missing=None
@@ -94,6 +93,7 @@ extension.sh=Bash shell
fatal.missing_javafx=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Java 8 or 10.\nIf you are using OpenJDK, please ensure OpenJFX is included.
fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use HMCL, but HMCL will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause HMCL not to function properly.\nPlease upgrade your Java to 8u101 or later to resolve the problem.
fatal.config_access_denied=The configuration at "%s" is not readable or writable.\nPlease grant HMCL read and write access to this file and its parent directory.
folder.config=Configs
folder.coremod=Core Mod
@@ -140,6 +140,7 @@ launch.failed=Unable to launch
launch.failed.cannot_create_jvm=Java virtual machine cannot be created. Java arguments may have problems. You can enable the no args mode in the settings.
launch.failed.creating_process=Failed to create process, maybe your java path is wrong, please modify your java path.
launch.failed.decompressing_natives=Unable to decompress native libraries.
launch.failed.download_library=Unable to download library %s.
launch.failed.executable_permission=Unable to add permission to the launch script
launch.failed.exited_abnormally=Game exited abnormally, please visit the log, or ask someone for help.
launch.state.dependencies=Dependencies
@@ -155,6 +156,8 @@ launcher.background.choose=Choose background path.
launcher.background.default=Default
launcher.common_directory=Common Directory
launcher.common_directory.choose=Choose common directory.
launcher.common_directory.default=Default
launcher.common_directory.disabled=Disabled
launcher.contact=Contact Us
launcher.crash=Hello Minecraft! Launcher has crashed!
launcher.crash_out_dated=Hello Minecraft! Launcher has crashed! Your launcher is outdated. Update it!
@@ -35,12 +35,12 @@ account.choose=選擇一個角色
account.create=新建賬戶
account.email=郵箱
account.failed.connect_authentication_server=無法連接認證服務器,可能是網絡問題
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄次數過多被暫時禁止登錄,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
account.failed.no_character=該帳號沒有角色
account.failed.no_selected_server=未選擇認證服務器
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.failed.server_response_malformed=這個服務器不是合法的登錄驗證服務器
account.injector.add=添加認證服務器
account.injector.manage=管理認證服務器
account.injector.manage.title=認證服務器
@@ -50,7 +50,6 @@ account.injector.server_url=服務器地址
account.injector.server_name=服務器名稱
account.methods=登錄方式
account.methods.authlib_injector=authlib-injector 登錄
account.methods.no_method=沒有登入方式
account.methods.offline=離線模式
account.methods.yggdrasil=正版登錄
account.missing=沒有賬戶
@@ -94,6 +93,7 @@ extension.sh=Bash 腳本
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,請降級到 Java 8 或 10。\n如果您使用的是 OpenJDK,請確保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=當前 Java 平台缺少 DST Root CA X3 證書。\n您依然可以使用 HMCL,但將無法連接到部分站點(如使用 Let's Encrypt 證書的站點),這可能會使 HMCL 無法正常工作。\n請將您的 Java 升級到 8u101 以上以解決此問題。
fatal.config_access_denied=HMCL 無法讀取或寫入位於 "%s" 的配置文件。\n請授予 HMCL 對該文件及其父目錄的讀寫權限。
folder.config=配置文件夾
folder.coremod=核心MOD文件夾
@@ -140,6 +140,7 @@ launch.failed=啓動失敗
launch.failed.cannot_create_jvm=截獲到無法創建 Java 虛擬機,可能是 Java 參數有問題,可以在設置中開啓無參數模式啓動。
launch.failed.creating_process=啓動失敗,在創建新進程時發生錯誤,可能是 Java 路徑錯誤。
launch.failed.decompressing_natives=未能解壓遊戲本地庫。
launch.failed.download_library=未能下载游戏依赖 %s.
launch.failed.executable_permission=未能爲啓動文件添加執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看日志文件,或聯繫他人尋求幫助。
launch.state.dependencies=正在處理遊戲依賴
@@ -155,6 +156,8 @@ launcher.background.choose=選擇背景路徑
launcher.background.default=默認(自動檢索啓動器同目錄下的 background.png/jpg 及 bg 文件夾內的圖片)
launcher.common_directory=公共文件夾
launcher.common_directory.choose=選擇公共文件夾
launcher.common_directory.default=默認
launcher.common_directory.disabled=禁用
launcher.contact=聯繫我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並通過 MCBBS、貼吧、GitHub 或 Minecraft Forum 反饋 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已檢測到您的啓動器不是最新版本,請更新後再試!
@@ -35,12 +35,12 @@ account.choose=选择一个角色
account.create=新建账户
account.email=邮箱
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.failed.invalid_credentials=您的用户名或密码错误,或者登录次数过多被暂时禁止登录,请稍后再试
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_character=该帐号没有角色
account.failed.no_selected_server=未选择认证服务器
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.failed.server_response_malformed=这个服务器不是合法的登录验证服务器
account.injector.add=添加认证服务器
account.injector.manage=管理认证服务器
account.injector.manage.title=认证服务器
@@ -50,7 +50,6 @@ account.injector.server_url=服务器地址
account.injector.server_name=服务器名称
account.methods=登录方式
account.methods.authlib_injector=authlib-injector 登录
account.methods.no_method=没有登入方式
account.methods.offline=离线模式
account.methods.yggdrasil=正版登录
account.missing=没有账户
@@ -94,6 +93,7 @@ extension.sh=Bash 脚本
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,请降级到 Java 8 或 10。\n如果您使用的是 OpenJDK,请确保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 HMCL,但将无法连接到部分站点(如使用 Let's Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
fatal.config_access_denied=HMCL 无法读取或写入位于 "%s" 的配置文件。\n请授予 HMCL 对该文件及其父目录的读写权限。
folder.config=配置文件夹
folder.coremod=核心MOD文件夹
@@ -140,6 +140,7 @@ launch.failed=启动失败
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
launch.failed.decompressing_natives=未能解压游戏本地库。
launch.failed.download_library=未能下载游戏依赖 %s.
launch.failed.executable_permission=未能为启动文件添加执行权限。
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
launch.state.dependencies=正在处理游戏依赖
@@ -153,8 +154,10 @@ launcher=启动器
launcher.background=背景地址
launcher.background.choose=选择背景路径
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg 及 bg 文件夹内的图片)
launcher.common_directory=公共文件夹
launcher.common_directory=公共文件夹(统一存放游戏资源文件,减少游戏体积)
launcher.common_directory.choose=选择公共文件夹
launcher.common_directory.default=默认(%AppData%/.minecraft 或者 ~/.minecraft)
launcher.common_directory.disabled=禁用(总是使用游戏路径)
launcher.contact=联系我们
launcher.crash=Hello Minecraft! Launcher 遇到了无法处理的错误,请复制下列内容并通过 MCBBS、贴吧、GitHub 或 Minecraft Forum 反馈 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了无法处理的错误,已检测到您的启动器不是最新版本,请更新后再试!
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
@@ -110,4 +111,16 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return server;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), server.hashCode());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AuthlibInjectorAccount))
return false;
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
return super.equals(another) && server.equals(another.server);
}
}
@@ -105,4 +105,17 @@ public class OfflineAccount extends Account {
.append("uuid", uuid)
.toString();
}
@Override
public int hashCode() {
return username.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof OfflineAccount))
return false;
OfflineAccount another = (OfflineAccount) obj;
return username.equals(another.username);
}
}
@@ -169,4 +169,16 @@ public class YggdrasilAccount extends Account {
return "YggdrasilAccount[username=" + getUsername() + "]";
}
@Override
public int hashCode() {
return characterUUID.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof YggdrasilAccount))
return false;
YggdrasilAccount another = (YggdrasilAccount) obj;
return characterUUID.equals(another.characterUUID);
}
}
@@ -0,0 +1,34 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 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.game;
import org.jackhuang.hmcl.game.Library;
public class LibraryDownloadException extends Exception {
private final Library library;
public LibraryDownloadException(Library library, Throwable cause) {
super("Unable to download library " + library, cause);
this.library = library;
}
public Library getLibrary() {
return library;
}
}
@@ -55,7 +55,7 @@ public final class LibraryDownloadTask extends Task {
@Override
public Collection<? extends Task> getDependents() {
return library.getChecksums() != null ? Collections.singleton(xzTask) : Collections.emptySet();
return library.getChecksums() != null ? Collections.singleton(xzTask) : Collections.singleton(task);
}
@Override
@@ -65,20 +65,23 @@ public final class LibraryDownloadTask extends Task {
@Override
public void execute() throws Exception {
if (isDependentsSucceeded() && library.getChecksums() != null) {
if (!isDependentsSucceeded()) {
// Since FileDownloadTask wraps the actual exception with another IOException.
// We should extract it letting the error message clearer.
Throwable t = library.getChecksums() != null ? xzTask.getLastException() : task.getLastException();
if (t.getCause() != null && t.getCause() != t)
throw new LibraryDownloadException(library, t.getCause());
else
throw new LibraryDownloadException(library, t);
}
if (library.getChecksums() != null) {
unpackLibrary(jar, FileUtils.readBytes(xzFile));
if (!checksumValid(jar, library.getChecksums()))
throw new IOException("Checksum failed for " + library);
downloaded = true;
}
}
@Override
public Collection<? extends Task> getDependencies() {
return downloaded ? Collections.emptySet() : Collections.singleton(task);
}
private static boolean checksumValid(File libPath, List<String> checksums) {
try {
if ((checksums == null) || (checksums.isEmpty())) {
@@ -20,112 +20,75 @@ package org.jackhuang.hmcl.game;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.util.IOUtils;
import org.jenkinsci.constant_pool_scanner.ConstantPool;
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
import org.jenkinsci.constant_pool_scanner.ConstantType;
import org.jenkinsci.constant_pool_scanner.StringConstant;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.US_ASCII;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* @author huangyuhui
*/
public final class GameVersion {
private static int lessThan32(byte[] b, int x) {
for (; x < b.length; x++)
if (b[x] < 32)
return x;
return -1;
private static Optional<String> getVersionOfClassMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException {
ConstantPool pool = ConstantPoolScanner.parse(file.getInputStream(entry), ConstantType.STRING);
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
.map(StringConstant::get)
.filter(s -> s.startsWith("Minecraft Minecraft "))
.map(s -> s.substring("Minecraft Minecraft ".length()))
.findFirst();
}
public static int matchArray(byte[] a, byte[] b) {
for (int i = 0; i < a.length - b.length; i++) {
int j = 1;
for (int k = 0; k < b.length; k++) {
if (b[k] == a[(i + k)])
continue;
j = 0;
private static Optional<String> getVersionFromClassMinecraftServer(ZipFile file, ZipArchiveEntry entry) throws IOException {
ConstantPool pool = ConstantPoolScanner.parse(file.getInputStream(entry), ConstantType.STRING);
List<String> list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
.map(StringConstant::get)
.collect(Collectors.toList());
int idx = -1;
for (int i = 0; i < list.size(); ++i)
if (list.get(i).startsWith("Can't keep up!")) {
idx = i;
break;
}
if (j != 0)
return i;
}
return -1;
}
private static Optional<String> getVersionOfOldMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException {
byte[] tmp = IOUtils.readFullyAsByteArray(file.getInputStream(entry));
for (int i = idx - 1; i >= 0; --i)
if (list.get(i).matches(".*[0-9].*"))
return Optional.of(list.get(i));
byte[] bytes = "Minecraft Minecraft ".getBytes(US_ASCII);
int j = matchArray(tmp, bytes);
if (j < 0)
return Optional.empty();
int i = j + bytes.length;
if ((j = lessThan32(tmp, i)) < 0)
return Optional.empty();
return Optional.of(new String(tmp, i, j - i, US_ASCII));
}
private static Optional<String> getVersionOfNewMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException {
byte[] tmp = IOUtils.readFullyAsByteArray(file.getInputStream(entry));
byte[] str = "-server.txt".getBytes(US_ASCII);
int j = matchArray(tmp, str);
if (j < 0) return Optional.empty();
int i = j + str.length;
i += 11;
j = lessThan32(tmp, i);
if (j < 0) return Optional.empty();
String result = new String(tmp, i, j - i, US_ASCII);
char ch = result.charAt(0);
// 1.8.1+
if (ch < '0' || ch > '9') {
str = "Can't keep up! Did the system time change, or is the server overloaded?".getBytes(US_ASCII);
j = matchArray(tmp, str);
if (j < 0) return Optional.empty();
i = -1;
while (j > 0) {
if (tmp[j] >= 48 && tmp[j] <= 57) {
i = j;
break;
}
j--;
}
if (i == -1) return Optional.empty();
int k = i;
if (tmp[i + 1] >= (int) 'a' && tmp[i + 1] <= (int) 'z')
i++;
while (tmp[k] >= 48 && tmp[k] <= 57 || tmp[k] == (int) '-' || tmp[k] == (int) '.' || tmp[k] >= 97 && tmp[k] <= (int) 'z')
k--;
k++;
return Optional.of(new String(tmp, k, i - k + 1, US_ASCII));
}
return Optional.of(result);
return Optional.empty();
}
public static Optional<String> minecraftVersion(File file) {
if (file == null || !file.exists() || !file.isFile() || !file.canRead())
return Optional.empty();
ZipFile f = null;
ZipFile gameJar = null;
try {
f = new ZipFile(file);
ZipArchiveEntry minecraft = f
.getEntry("net/minecraft/client/Minecraft.class");
if (minecraft != null)
return getVersionOfOldMinecraft(f, minecraft);
ZipArchiveEntry main = f.getEntry("net/minecraft/client/main/Main.class");
ZipArchiveEntry minecraftServer = f.getEntry("net/minecraft/server/MinecraftServer.class");
if ((main != null) && (minecraftServer != null))
return getVersionOfNewMinecraft(f, minecraftServer);
gameJar = new ZipFile(file);
ZipArchiveEntry minecraft = gameJar.getEntry("net/minecraft/client/Minecraft.class");
if (minecraft != null) {
Optional<String> result = getVersionOfClassMinecraft(gameJar, minecraft);
if (result.isPresent())
return result;
}
ZipArchiveEntry minecraftServer = gameJar.getEntry("net/minecraft/server/MinecraftServer.class");
if (minecraftServer != null)
return getVersionFromClassMinecraftServer(gameJar, minecraftServer);
return Optional.empty();
} catch (IOException e) {
return Optional.empty();
} finally {
IOUtils.closeQuietly(f);
IOUtils.closeQuietly(gameJar);
}
}
}
@@ -41,7 +41,7 @@ public final class ModManager {
File modsDirectory = new File(repository.getRunDirectory(id), "mods");
Consumer<File> puter = modFile -> Lang.ignoringException(() -> modCache.put(id, ModInfo.fromFile(modFile)));
Optional.ofNullable(modsDirectory.listFiles()).map(Arrays::stream).ifPresent(files -> files.forEach(modFile -> {
if (modFile.isDirectory() && VersionNumber.parseVersion(modFile.getName()) != null)
if (modFile.isDirectory() && VersionNumber.parseVersion(modFile.getName()).isPresent())
Optional.ofNullable(modFile.listFiles()).map(Arrays::stream).ifPresent(x -> x.forEach(puter));
else
puter.accept(modFile);
@@ -63,6 +63,16 @@ public abstract class Task {
this.state = state;
}
private Throwable lastException = null;
public Throwable getLastException() {
return lastException;
}
void setLastException(Throwable e) {
lastException = e;
}
/**
* The scheduler that decides how this task runs.
*/
@@ -190,6 +190,7 @@ public final class TaskExecutor {
} catch (SilentException | RejectedExecutionException e) {
// do nothing
} catch (Exception e) {
task.setLastException(e);
lastException = e;
variables.set(LAST_EXCEPTION_ID, e);
if (task.getSignificance().shouldLog()) {
@@ -0,0 +1,55 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 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 java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* If a version string contains '-', a {@link ComposedVersionNumber}
* will be generated.
*
* Formats like 1.7.10-OptiFine, 1.12.2-Forge
*
* @author huangyuhui
*/
public final class ComposedVersionNumber extends VersionNumber {
List<VersionNumber> composed;
public static boolean isComposedVersionNumber(String version) {
return version.contains("-");
}
ComposedVersionNumber(String version) {
composed = Arrays.stream(version.split("-"))
.map(VersionNumber::asVersion)
.collect(Collectors.toList());
}
@Override
public int hashCode() {
return Objects.hash(composed);
}
@Override
public String toString() {
return composed.stream().map(VersionNumber::toString).collect(Collectors.joining("-"));
}
}
@@ -17,7 +17,10 @@
*/
package org.jackhuang.hmcl.util;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -28,57 +31,37 @@ import java.util.stream.Collectors;
*/
public final class IntVersionNumber extends VersionNumber {
private final List<Integer> version;
final List<Integer> version;
IntVersionNumber(List<Integer> version) {
this.version = version;
public static boolean isIntVersionNumber(String version) {
return version.chars().noneMatch(ch -> ch != '.' && (ch < '0' || ch > '9'))
&& !version.contains("..") && StringUtils.isNotBlank(version);
}
IntVersionNumber(String version) {
if (!isIntVersionNumber(version))
throw new IllegalArgumentException("The version " + version + " is malformed, only dots and digits are allowed.");
List<Integer> versions = Arrays.stream(version.split("\\."))
.map(Integer::parseInt)
.collect(Collectors.toList());
while (!versions.isEmpty() && versions.get(versions.size() - 1) == 0)
versions.remove(versions.size() - 1);
this.version = versions;
}
public int get(int index) {
return version.get(index);
}
@Override
public int compareTo(VersionNumber o) {
if (!(o instanceof IntVersionNumber))
return 0;
IntVersionNumber other = (IntVersionNumber) o;
int len = Math.max(this.version.size(), other.version.size());
for (int i = 0; i < len; ++i) {
int thisInt = Lang.get(this.version, i).orElse(0);
int otherInt = Lang.get(other.version, i).orElse(0);
if (thisInt != otherInt)
return Integer.compare(thisInt, otherInt);
}
return 0;
}
@Override
public int hashCode() {
int hash = 3;
for (int i : version)
hash = 83 * hash + i;
return hash;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof VersionNumber))
return false;
if (!(obj instanceof IntVersionNumber))
return true;
IntVersionNumber other = (IntVersionNumber) obj;
if (version.size() != other.version.size())
return false;
for (int i = 0; i < version.size(); ++i)
if (!version.get(i).equals(other.version.get(i)))
return false;
return true;
return Objects.hash(version);
}
@Override
public String toString() {
return String.join(".", version.stream().map(Object::toString).collect(Collectors.toList()));
return version.stream().map(Object::toString).collect(Collectors.joining("."));
}
}
@@ -41,7 +41,7 @@ public final class Lang {
*/
@SafeVarargs
public static <K, V> Map<K, V> mapOf(Pair<K, V>... pairs) {
HashMap<K, V> map = new HashMap<>();
Map<K, V> map = new LinkedHashMap<>();
for (Pair<K, V> pair : pairs)
map.put(pair.getKey(), pair.getValue());
return map;
@@ -0,0 +1,160 @@
/*
* 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 static java.util.stream.Collectors.toCollection;
import static javafx.collections.FXCollections.unmodifiableObservableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
/**
* @author yushijinhun
*/
public final class MappedObservableList {
private MappedObservableList() {
}
private static class ReferenceHolder implements InvalidationListener {
@SuppressWarnings("unused")
private Object ref;
ReferenceHolder(Object ref) {
this.ref = ref;
}
@Override
public void invalidated(Observable observable) {
// no-op
}
}
private static class MappedObservableListUpdater<T, U> implements ListChangeListener<T> {
private ObservableList<T> origin;
private ObservableList<U> target;
private Function<T, U> mapper;
// If we directly synchronize changes to target, each operation on target will cause a event to be fired.
// So we first write changes to buffer. After all the changes are processed, we use target.setAll to synchronize the changes.
private List<U> buffer;
MappedObservableListUpdater(ObservableList<T> origin, ObservableList<U> target, Function<T, U> mapper) {
this.origin = origin;
this.target = target;
this.mapper = mapper;
this.buffer = new ArrayList<>(target);
}
@Override
public void onChanged(Change<? extends T> change) {
// cache removed elements to reduce calls to mapper
Map<T, LinkedList<U>> cache = new IdentityHashMap<>();
while (change.next()) {
int from = change.getFrom();
int to = change.getTo();
if (change.wasPermutated()) {
@SuppressWarnings("unchecked")
U[] temp = (U[]) new Object[to - from];
for (int i = 0; i < temp.length; i++) {
temp[i] = buffer.get(from + i);
}
for (int idx = from; idx < to; idx++) {
buffer.set(change.getPermutation(idx), temp[idx - from]);
}
} else {
if (change.wasRemoved()) {
List<? extends T> originRemoved = change.getRemoved();
List<U> targetRemoved = buffer.subList(from, from + originRemoved.size());
for (int i = 0; i < targetRemoved.size(); i++) {
pushCache(cache, originRemoved.get(i), targetRemoved.get(i));
}
targetRemoved.clear();
}
if (change.wasAdded()) {
@SuppressWarnings("unchecked")
U[] toAdd = (U[]) new Object[to - from];
for (int i = 0; i < toAdd.length; i++) {
toAdd[i] = map(cache, origin.get(from + i));
}
buffer.addAll(from, Arrays.asList(toAdd));
}
}
}
target.setAll(buffer);
}
private void pushCache(Map<T, LinkedList<U>> cache, T key, U value) {
cache.computeIfAbsent(key, any -> new LinkedList<>())
.push(value);
}
private U map(Map<T, LinkedList<U>> cache, T key) {
LinkedList<U> stack = cache.get(key);
if (stack != null && !stack.isEmpty()) {
return stack.pop();
}
return mapper.apply(key);
}
}
/**
* This methods creates a mapping of {@code origin}, using {@code mapper} as the converter.
*
* If an item is added to {@code origin}, {@code mapper} will be invoked to create a corresponding item, which will also be added to the returned {@code ObservableList}.
* If an item is removed from {@code origin}, the corresponding item in the returned {@code ObservableList} will also be removed.
* If {@code origin} is permutated, the returned {@code ObservableList} will also be permutated in the same way.
*
* The returned {@code ObservableList} is unmodifiable.
*/
public static <T, U> ObservableList<U> create(ObservableList<T> origin, Function<T, U> mapper) {
// create a already-synchronized target ObservableList<U>
ObservableList<U> target = origin.stream()
.map(mapper)
.collect(toCollection(FXCollections::observableArrayList));
// then synchronize further changes to target
ListChangeListener<T> listener = new MappedObservableListUpdater<>(origin, target, mapper);
// let target hold a reference to listener to prevent listener being garbage-collected before target is garbage-collected
target.addListener(new ReferenceHolder(listener));
// let origin hold a weak reference to listener, so that target can be garbage-collected when it's no longer used
origin.addListener(new WeakListChangeListener<>(listener));
// ref graph:
// target ------> listener <-weak- origin
// <------ ------>
return unmodifiableObservableList(target);
}
}
@@ -34,14 +34,6 @@ public final class StringVersionNumber extends VersionNumber {
this.version = version;
}
@Override
public int compareTo(VersionNumber o) {
if (!(o instanceof StringVersionNumber))
return 0;
return version.compareTo(((StringVersionNumber) o).version);
}
public String getVersion() {
return version;
}
@@ -56,15 +48,4 @@ public final class StringVersionNumber extends VersionNumber {
return version;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof VersionNumber))
return false;
if (!(obj instanceof StringVersionNumber))
return true;
return version.equals(((StringVersionNumber) obj).version);
}
}
@@ -17,10 +17,7 @@
*/
package org.jackhuang.hmcl.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
/**
* The formatted version number represents a version string.
@@ -29,52 +26,76 @@ import java.util.Optional;
*/
public abstract class VersionNumber implements Comparable<VersionNumber> {
/**
* @throws IllegalArgumentException if there are some characters excluding digits and dots.
* @param version version string in form x.x.x
* @return the int version number
*/
public static IntVersionNumber asIntVersionNumber(String version) {
if (version.chars().filter(ch -> ch != '.' && (ch < '0' || ch > '9')).count() > 0
|| version.contains("..") || StringUtils.isBlank(version))
throw new IllegalArgumentException("The version " + version + " is malformed, only dots and digits are allowed.");
String[] s = version.split("\\.");
ArrayList<Integer> versions = new ArrayList<>(s.length);
for (String value : s) versions.add(Integer.parseInt(value));
while (!versions.isEmpty() && versions.get(versions.size() - 1) == 0)
versions.remove(versions.size() - 1);
return new IntVersionNumber(Collections.unmodifiableList(versions));
}
public static StringVersionNumber asStringVersionNumber(String version) {
return new StringVersionNumber(version);
}
public static VersionNumber asVersion(String version) {
Objects.requireNonNull(version);
try {
return asIntVersionNumber(version);
} catch (IllegalArgumentException e) {
return asStringVersionNumber(version);
}
if (ComposedVersionNumber.isComposedVersionNumber(version))
return new ComposedVersionNumber(version);
else if (IntVersionNumber.isIntVersionNumber(version))
return new IntVersionNumber(version);
else
return new StringVersionNumber(version);
}
public static Optional<String> parseVersion(String str) {
if (str.chars().anyMatch(it -> it != '.' && (it < '0' || it > '9')) || StringUtils.isBlank(str))
if (IntVersionNumber.isIntVersionNumber(str))
return Optional.of(new IntVersionNumber(str).toString());
else
return Optional.empty();
String[] s = str.split("\\.");
for (String i : s)
if (StringUtils.isBlank(i))
return Optional.empty();
StringBuilder builder = new StringBuilder();
int last = s.length - 1;
for (int i = s.length - 1; i >= 0; --i)
if (Integer.parseInt(s[i]) == 0)
last = i;
for (int i = 0; i < last; ++i)
builder.append(s[i]).append(".");
return Optional.of(builder.append(s[last]).toString());
}
@Override
public int compareTo(VersionNumber o) {
return COMPARATOR.compare(this, o);
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
else return toString().equals(obj.toString());
}
private static <T extends Comparable<T>> int compareTo(List<T> a, List<T> b) {
int i;
for (i = 0; i < a.size() && i < b.size(); ++i) {
int res = a.get(i).compareTo(b.get(i));
if (res != 0)
return res;
}
if (i < a.size()) return 1;
else if (i < b.size()) return -1;
else return 0;
}
public static final Comparator<VersionNumber> COMPARATOR = new Comparator<VersionNumber>() {
@Override
public int compare(VersionNumber a, VersionNumber b) {
if (a == null || b == null)
return 0;
else {
if (a instanceof ComposedVersionNumber) {
if (b instanceof ComposedVersionNumber)
return compareTo(((ComposedVersionNumber) a).composed, ((ComposedVersionNumber) b).composed);
else
return compare(((ComposedVersionNumber) a).composed.get(0), b);
} else if (a instanceof IntVersionNumber) {
if (b instanceof ComposedVersionNumber)
return -compare(b, a);
else if (b instanceof IntVersionNumber)
return compareTo(((IntVersionNumber) a).version, ((IntVersionNumber) b).version);
else if (b instanceof StringVersionNumber)
return a.toString().compareTo(b.toString());
} else if (a instanceof StringVersionNumber) {
if (b instanceof ComposedVersionNumber)
return -compare(b, a);
else if (b instanceof StringVersionNumber)
return a.toString().compareTo(b.toString());
else if (b instanceof IntVersionNumber)
return a.toString().compareTo(b.toString());
}
throw new IllegalArgumentException("Unrecognized VersionNumber " + a + " and " + b);
}
}
};
}
+3
View File
@@ -0,0 +1,3 @@
Debug
Release
.vs
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.
+174
View File
@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{672B1019-E741-4C0D-A986-627E2ACE157B}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>HMCL</RootNamespace>
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v141_xp</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v141_xp</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MinSpace</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="HMCL.h" />
<ClInclude Include="Resource.h" />
<ClInclude Include="stdafx.h" />
<ClInclude Include="targetver.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="HMCL.cpp" />
<ClCompile Include="stdafx.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="HMCL.rc" />
</ItemGroup>
<ItemGroup>
<Image Include="HMCL.ico" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="stdafx.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="targetver.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Resource.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="HMCL.h">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="stdafx.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="HMCL.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="HMCL.rc">
<Filter>资源文件</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<Image Include="HMCL.ico">
<Filter>资源文件</Filter>
</Image>
</ItemGroup>
</Project>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+31
View File
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27428.2005
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HMCL", "HMCL\HMCL.vcxproj", "{672B1019-E741-4C0D-A986-627E2ACE157B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{672B1019-E741-4C0D-A986-627E2ACE157B}.Debug|x64.ActiveCfg = Debug|x64
{672B1019-E741-4C0D-A986-627E2ACE157B}.Debug|x64.Build.0 = Debug|x64
{672B1019-E741-4C0D-A986-627E2ACE157B}.Debug|x86.ActiveCfg = Debug|Win32
{672B1019-E741-4C0D-A986-627E2ACE157B}.Debug|x86.Build.0 = Debug|Win32
{672B1019-E741-4C0D-A986-627E2ACE157B}.Release|x64.ActiveCfg = Release|x64
{672B1019-E741-4C0D-A986-627E2ACE157B}.Release|x64.Build.0 = Release|x64
{672B1019-E741-4C0D-A986-627E2ACE157B}.Release|x86.ActiveCfg = Release|Win32
{672B1019-E741-4C0D-A986-627E2ACE157B}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FE40055B-673D-42F5-8AE4-6DF2C87EB659}
EndGlobalSection
EndGlobal
+5 -4
View File
@@ -38,10 +38,11 @@ allprojects {
sourceCompatibility = 1.8
dependencies {
compile "com.google.code.gson:gson:2.8.2"
compile "org.apache.commons:commons-compress:1.15"
compile "org.tukaani:xz:1.2"
compile "org.hildan.fxgson:fx-gson:3.1.0"
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
compile group: 'org.apache.commons', name: 'commons-compress', version: '1.17'
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'
testCompile group: 'junit', name: 'junit', version: '4.12'
}