Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb9418dead | ||
|
|
4a1b06b80c | ||
|
|
8905f2e855 | ||
|
|
6c74364fa5 | ||
|
|
f0720c1755 | ||
|
|
f1acc1d2c1 | ||
|
|
82bddbfa6e | ||
|
|
841755fe56 | ||
|
|
340ee751e8 | ||
|
|
cf70f72f9c | ||
|
|
16ea88a659 | ||
|
|
eaa6790013 | ||
|
|
af571d533d | ||
|
|
004c3c588a | ||
|
|
2a44167c22 | ||
|
|
cf6b735026 | ||
|
|
5918b04b48 | ||
|
|
8004cb3a64 | ||
|
|
9981423927 | ||
|
|
0e5250dfb3 | ||
|
|
2dc956b357 | ||
|
|
b8bd0a08e0 | ||
|
|
fb31eebcf2 | ||
|
|
3c8689a3f1 | ||
|
|
2957d66dd6 | ||
|
|
6f6efe0874 | ||
|
|
355fc04069 | ||
|
|
b4738f383b | ||
|
|
b12711969c | ||
|
|
94727353c4 | ||
|
|
5eb7449b74 |
No files matched your search
@@ -0,0 +1,28 @@
|
||||
# 公告
|
||||
|
||||
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
|
||||
|
||||
**如果你在阅读完本公告后仍然希望在此处发布 issue,请将公告内容删去,并按照下面的模板填入相关信息。**
|
||||
|
||||
|
||||
|
||||
我们发现,对于不经常检查邮箱的人来说,GitHub issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
|
||||
|
||||
如果你希望在 QQ 群中反馈问题,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
|
||||
|
||||
|
||||
|
||||
# 问题提交
|
||||
|
||||
*完整地填下面的问题提交表对我们很重要,这可以加快我们分析问题原因的速度。*
|
||||
|
||||
* 启动器版本:
|
||||
* 操作系统:
|
||||
* Java 版本:
|
||||
* 错误截图(最好请将整个电脑屏幕的截图发上来):
|
||||
* 游戏版本(如果是启动通过启动器自带的自动安装功能安装的游戏):
|
||||
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
|
||||
* 游戏崩溃报告(如果有):
|
||||
* 启动器崩溃报告(如果有):
|
||||
* 启动器日志文件(在启动器设置中打开日志文件夹并将文件夹内所有文件打包发在这里):
|
||||
* 问题描述(如何触发问题):
|
||||
@@ -55,7 +55,8 @@ public final class Launcher extends Application {
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
|
||||
}
|
||||
|
||||
try {
|
||||
// runLater to ensure ConfigHolder.init() finished initialization
|
||||
Platform.runLater(() -> {
|
||||
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
|
||||
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
|
||||
Platform.setImplicitExit(false);
|
||||
@@ -66,9 +67,8 @@ public final class Launcher extends Application {
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
} catch (Throwable e) {
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -140,12 +140,12 @@ public final class AccountHelper {
|
||||
private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh) throws Exception {
|
||||
account.clearCache();
|
||||
|
||||
Optional<Texture> texture = account.getSkin(profile);
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
File file = getSkinFile(profile.getId());
|
||||
if (!refresh && file.exists())
|
||||
return;
|
||||
Optional<Texture> texture = account.getSkin(profile);
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
|
||||
}
|
||||
|
||||
@@ -153,12 +153,12 @@ public final class AccountHelper {
|
||||
account.clearCache();
|
||||
|
||||
if (account.getCharacter() == null) return;
|
||||
Optional<Texture> texture = account.getSkin();
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
File file = getSkinFile(account.getUUID());
|
||||
if (!refresh && file.exists())
|
||||
return;
|
||||
Optional<Texture> texture = account.getSkin();
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
@@ -216,7 +217,10 @@ public final class LauncherHelper {
|
||||
if (ex != null) {
|
||||
String message;
|
||||
if (ex instanceof CurseCompletionException) {
|
||||
message = i18n("modpack.type.curse.error");
|
||||
if (ex.getCause() instanceof FileNotFoundException)
|
||||
message = i18n("modpack.type.curse.not_found");
|
||||
else
|
||||
message = i18n("modpack.type.curse.error");
|
||||
} else if (ex instanceof PermissionException) {
|
||||
message = i18n("launch.failed.executable_permission");
|
||||
} else if (ex instanceof ProcessCreationException) {
|
||||
@@ -343,6 +347,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// 32-bit JVM cannot make use of too much memory.
|
||||
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
|
||||
setting.getMaxMemory() > 1.5 * 1024) {
|
||||
// 1.5 * 1024 is an inaccurate number.
|
||||
@@ -350,12 +355,24 @@ public final class LauncherHelper {
|
||||
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
|
||||
// Cannot allocate too much memory exceeding free space.
|
||||
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
|
||||
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
// Forge 2760 and above will crash game with LiteLoader.
|
||||
if (!flag) {
|
||||
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
|
||||
.anyMatch(it -> VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2760") >= 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
|
||||
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!flag)
|
||||
onAccept.run();
|
||||
}
|
||||
@@ -454,6 +471,39 @@ public final class LauncherHelper {
|
||||
});
|
||||
}
|
||||
|
||||
private void finishLaunch() {
|
||||
switch (launcherVisibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
Platform.runLater(() -> {
|
||||
// If application was stopped and execution services did not finish termination,
|
||||
// these codes will be executed.
|
||||
if (Controllers.getStage() != null) {
|
||||
Controllers.getStage().hide();
|
||||
emitStatus(LoadingState.DONE);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case CLOSE:
|
||||
// Never come to here.
|
||||
break;
|
||||
case KEEP:
|
||||
Platform.runLater(() -> {
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
case HIDE:
|
||||
Platform.runLater(() -> {
|
||||
// If application was stopped and execution services did not finish termination,
|
||||
// these codes will be executed.
|
||||
if (Controllers.getStage() != null) {
|
||||
Controllers.getStage().close();
|
||||
emitStatus(LoadingState.DONE);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onLog(String log, Log4jLevel level) {
|
||||
String newLog = log;
|
||||
@@ -483,28 +533,7 @@ public final class LauncherHelper {
|
||||
|
||||
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
|
||||
lwjgl = true;
|
||||
switch (launcherVisibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
Platform.runLater(() -> {
|
||||
Controllers.getStage().hide();
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
case CLOSE:
|
||||
// Never come to here.
|
||||
break;
|
||||
case KEEP:
|
||||
Platform.runLater(() -> {
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
case HIDE:
|
||||
Platform.runLater(() -> {
|
||||
Controllers.getStage().close();
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
}
|
||||
finishLaunch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,6 +542,9 @@ public final class LauncherHelper {
|
||||
if (exitType == ExitType.INTERRUPTED)
|
||||
return;
|
||||
|
||||
// Game crashed before opening the game window.
|
||||
if (!lwjgl) finishLaunch();
|
||||
|
||||
if (exitType != ExitType.NORMAL && logWindow == null)
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
|
||||
@@ -145,6 +145,9 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("_version")
|
||||
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
@SerializedName("uiVersion")
|
||||
private IntegerProperty uiVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public Config() {
|
||||
@@ -440,4 +443,27 @@ public final class Config implements Cloneable, Observable {
|
||||
this.updateChannel.set(updateChannel);
|
||||
}
|
||||
|
||||
public int getConfigVersion() {
|
||||
return configVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty configVersionProperty() {
|
||||
return configVersion;
|
||||
}
|
||||
|
||||
public void setConfigVersion(int configVersion) {
|
||||
this.configVersion.set(configVersion);
|
||||
}
|
||||
|
||||
public int getUiVersion() {
|
||||
return uiVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty uiVersionProperty() {
|
||||
return uiVersion;
|
||||
}
|
||||
|
||||
public void setUiVersion(int uiVersion) {
|
||||
this.uiVersion.set(uiVersion);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ import java.util.Map;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
final class ConfigUpgrader {
|
||||
private static final int VERSION = 0;
|
||||
|
||||
private ConfigUpgrader() {
|
||||
}
|
||||
|
||||
@@ -37,10 +39,22 @@ final class ConfigUpgrader {
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
* @return true if config version is upgraded
|
||||
*/
|
||||
static void upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
|
||||
static boolean upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
|
||||
boolean upgraded;
|
||||
if (deserialized.getConfigVersion() < VERSION) {
|
||||
deserialized.setConfigVersion(VERSION);
|
||||
// TODO: Add upgrade code here.
|
||||
upgraded = true;
|
||||
} else {
|
||||
upgraded = false;
|
||||
}
|
||||
|
||||
upgradeV2(deserialized, rawJson);
|
||||
upgradeV3(deserialized, rawJson);
|
||||
|
||||
return upgraded;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
@@ -146,15 +147,17 @@ public final class Profile implements Observable {
|
||||
}
|
||||
|
||||
private void checkSelectedVersion() {
|
||||
if (!repository.isLoaded()) return;
|
||||
String newValue = selectedVersion.get();
|
||||
if (!repository.hasVersion(newValue)) {
|
||||
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
|
||||
if (version.isPresent())
|
||||
selectedVersion.setValue(version.get());
|
||||
else if (newValue != null)
|
||||
selectedVersion.setValue(null);
|
||||
}
|
||||
JFXUtilities.runInFX(() -> {
|
||||
if (!repository.isLoaded()) return;
|
||||
String newValue = selectedVersion.get();
|
||||
if (!repository.hasVersion(newValue)) {
|
||||
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
|
||||
if (version.isPresent())
|
||||
selectedVersion.setValue(version.get());
|
||||
else if (newValue != null)
|
||||
selectedVersion.setValue(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public HMCLGameRepository getRepository() {
|
||||
|
||||
@@ -43,7 +43,7 @@ public class Theme {
|
||||
Color.web("#43A047"), // green
|
||||
Color.web("#E67E22"), // orange
|
||||
Color.web("#9C27B0"), // purple
|
||||
Color.web("#F44336") // red
|
||||
Color.web("#B71C1C") // red
|
||||
};
|
||||
|
||||
private final String color;
|
||||
|
||||
@@ -17,35 +17,50 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AccountList;
|
||||
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.profile.ProfileList;
|
||||
import org.jackhuang.hmcl.ui.versions.GameItem;
|
||||
import org.jackhuang.hmcl.ui.versions.GameList;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Controllers {
|
||||
public static final int UI_VERSION = 1;
|
||||
|
||||
private static Scene scene;
|
||||
private static Stage stage;
|
||||
@@ -121,8 +136,61 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static MainPage getMainPage() {
|
||||
if (mainPage == null)
|
||||
if (mainPage == null) {
|
||||
mainPage = new MainPage();
|
||||
mainPage.setOnDragOver(event -> {
|
||||
if (event.getGestureSource() != mainPage && event.getDragboard().hasFiles()) {
|
||||
if (event.getDragboard().getFiles().stream().anyMatch(it -> "zip".equals(FileUtils.getExtension(it))))
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
mainPage.setOnDragDropped(event -> {
|
||||
List<File> files = event.getDragboard().getFiles();
|
||||
if (files != null) {
|
||||
List<File> modpacks = files.stream()
|
||||
.filter(it -> "zip".equals(FileUtils.getExtension(it)))
|
||||
.collect(Collectors.toList());
|
||||
if (!modpacks.isEmpty()) {
|
||||
File modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(modpack), i18n("install.modpack"));
|
||||
event.setDropCompleted(true);
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
|
||||
if (version != null) {
|
||||
mainPage.setCurrentGame(version);
|
||||
} else {
|
||||
mainPage.setCurrentGame(i18n("version.empty"));
|
||||
}
|
||||
});
|
||||
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
|
||||
mainPage.latestVersionProperty().bind(
|
||||
MultiStepBinding.of(UpdateChecker.latestVersionProperty())
|
||||
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
|
||||
|
||||
Profiles.registerVersionsListener(profile -> {
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
List<Node> children = repository.getVersions().parallelStream()
|
||||
.filter(version -> !version.isHidden())
|
||||
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date() : version.getReleaseTime())
|
||||
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
|
||||
.map(version -> {
|
||||
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
|
||||
node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId()));
|
||||
return node;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
JFXUtilities.runInFX(() -> {
|
||||
if (profile == Profiles.getSelectedProfile())
|
||||
mainPage.getVersions().setAll(children);
|
||||
});
|
||||
});
|
||||
}
|
||||
return mainPage;
|
||||
}
|
||||
|
||||
@@ -131,6 +199,8 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void initialize(Stage stage) {
|
||||
Logging.LOG.info("Start initializing application");
|
||||
|
||||
Controllers.stage = stage;
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
@@ -46,14 +46,14 @@ public class ListPageSkin extends SkinBase<ListPage> {
|
||||
{
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
VBox accountList = new VBox();
|
||||
accountList.maxWidthProperty().bind(scrollPane.widthProperty());
|
||||
accountList.setSpacing(10);
|
||||
accountList.setStyle("-fx-padding: 10 10 10 10;");
|
||||
VBox list = new VBox();
|
||||
list.maxWidthProperty().bind(scrollPane.widthProperty());
|
||||
list.setSpacing(10);
|
||||
list.setPadding(new Insets(10));
|
||||
|
||||
Bindings.bindContent(accountList.getChildren(), skinnable.itemsProperty());
|
||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(accountList);
|
||||
scrollPane.setContent(list);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,43 +17,37 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameItem;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MainPage extends StackPane implements DecoratorPage {
|
||||
@@ -62,109 +56,142 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
private final PopupMenu menu = new PopupMenu();
|
||||
private final JFXPopup popup = new JFXPopup(menu);
|
||||
|
||||
@FXML
|
||||
private StackPane main;
|
||||
@FXML
|
||||
private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame");
|
||||
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
|
||||
private final StringProperty latestVersion = new SimpleStringProperty(this, "latestVersion");
|
||||
private final ObservableList<Node> versions = FXCollections.observableArrayList();
|
||||
|
||||
private StackPane updatePane;
|
||||
@FXML
|
||||
private JFXButton btnLaunch;
|
||||
@FXML
|
||||
private JFXButton btnMenu;
|
||||
@FXML
|
||||
private JFXButton closeUpdateButton;
|
||||
@FXML
|
||||
private Label lblCurrentGame;
|
||||
@FXML
|
||||
private Label lblIcon;
|
||||
@FXML
|
||||
private TwoLineListItem lblLatestVersion;
|
||||
@FXML
|
||||
private Rectangle separator;
|
||||
private JFXButton menuButton;
|
||||
|
||||
{
|
||||
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
|
||||
setPadding(new Insets(25));
|
||||
|
||||
btnLaunch.setClip(new Rectangle(-100, -100, 310, 200));
|
||||
btnMenu.setClip(new Rectangle(211, -100, 100, 200));
|
||||
menu.setMaxHeight(365);
|
||||
menu.setMinWidth(545);
|
||||
updatePane = new StackPane();
|
||||
updatePane.setVisible(false);
|
||||
updatePane.getStyleClass().add("bubble");
|
||||
FXUtils.setLimitWidth(updatePane, 230);
|
||||
FXUtils.setLimitHeight(updatePane, 55);
|
||||
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
|
||||
updatePane.setOnMouseClicked(e -> onUpgrade());
|
||||
FXUtils.onChange(showUpdateProperty(), this::doAnimation);
|
||||
|
||||
updatePane.visibleProperty().bind(UpdateChecker.outdatedProperty());
|
||||
closeUpdateButton.setGraphic(SVG.close(Theme.whiteFillBinding(), 10, 10));
|
||||
closeUpdateButton.setOnMouseClicked(event -> {
|
||||
Duration duration = Duration.millis(320);
|
||||
Timeline nowAnimation = new Timeline();
|
||||
nowAnimation.getKeyFrames().addAll(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(updatePane.translateXProperty(), 0, Interpolator.EASE_IN)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(updatePane.translateXProperty(), 260, Interpolator.EASE_IN)),
|
||||
new KeyFrame(duration, e -> {
|
||||
updatePane.visibleProperty().unbind();
|
||||
updatePane.setVisible(false);
|
||||
}));
|
||||
nowAnimation.play();
|
||||
});
|
||||
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
|
||||
lblLatestVersion.titleProperty().bind(
|
||||
MultiStepBinding.of(UpdateChecker.latestVersionProperty())
|
||||
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
|
||||
{
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(12);
|
||||
hBox.setAlignment(Pos.CENTER_LEFT);
|
||||
StackPane.setAlignment(hBox, Pos.CENTER_LEFT);
|
||||
StackPane.setMargin(hBox, new Insets(9, 12, 9, 16));
|
||||
{
|
||||
Label lblIcon = new Label();
|
||||
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
|
||||
|
||||
StackPane graphic = new StackPane();
|
||||
Node svg = SVG.triangle(Theme.whiteFillBinding(), 10, 10);
|
||||
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
|
||||
graphic.getChildren().setAll(svg);
|
||||
graphic.setTranslateX(12);
|
||||
btnMenu.setGraphic(graphic);
|
||||
TwoLineListItem prompt = new TwoLineListItem();
|
||||
prompt.setTitleFill(Color.WHITE);
|
||||
prompt.setSubtitleFill(Color.WHITE);
|
||||
prompt.setSubtitle(i18n("update.bubble.subtitle"));
|
||||
prompt.setPickOnBounds(false);
|
||||
prompt.setStyle("-jfx-title-font-weight: BOLD;");
|
||||
prompt.titleProperty().bind(latestVersionProperty());
|
||||
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
|
||||
if (version != null) {
|
||||
lblCurrentGame.setText(version);
|
||||
} else {
|
||||
lblCurrentGame.setText(i18n("version.empty"));
|
||||
hBox.getChildren().setAll(lblIcon, prompt);
|
||||
}
|
||||
});
|
||||
|
||||
Profiles.registerVersionsListener(this::loadVersions);
|
||||
JFXButton closeUpdateButton = new JFXButton();
|
||||
closeUpdateButton.setGraphic(SVG.close(Theme.whiteFillBinding(), 10, 10));
|
||||
StackPane.setAlignment(closeUpdateButton, Pos.TOP_RIGHT);
|
||||
closeUpdateButton.getStyleClass().add("toggle-icon-tiny");
|
||||
StackPane.setMargin(closeUpdateButton, new Insets(5));
|
||||
closeUpdateButton.setOnMouseClicked(e -> closeUpdateBubble());
|
||||
|
||||
updatePane.getChildren().setAll(hBox, closeUpdateButton);
|
||||
}
|
||||
|
||||
StackPane launchPane = new StackPane();
|
||||
launchPane.setMaxWidth(230);
|
||||
launchPane.setMaxHeight(55);
|
||||
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
|
||||
{
|
||||
JFXButton launchButton = new JFXButton();
|
||||
launchButton.setPrefWidth(230);
|
||||
launchButton.setPrefHeight(55);
|
||||
launchButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
launchButton.getStyleClass().add("jfx-button-raised");
|
||||
launchButton.setOnMouseClicked(e -> launch());
|
||||
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
|
||||
{
|
||||
VBox graphic = new VBox();
|
||||
graphic.setAlignment(Pos.CENTER);
|
||||
graphic.setTranslateX(-7);
|
||||
graphic.setMaxWidth(200);
|
||||
Label launchLabel = new Label(i18n("version.launch"));
|
||||
launchLabel.setStyle("-fx-font-size: 16px;");
|
||||
Label currentLabel = new Label();
|
||||
currentLabel.setStyle("-fx-font-size: 12px;");
|
||||
currentLabel.textProperty().bind(currentGameProperty());
|
||||
graphic.getChildren().setAll(launchLabel, currentLabel);
|
||||
|
||||
launchButton.setGraphic(graphic);
|
||||
}
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
separator.getStyleClass().add("darker-fill");
|
||||
separator.setWidth(1);
|
||||
separator.setHeight(57);
|
||||
separator.setTranslateX(95);
|
||||
separator.setMouseTransparent(true);
|
||||
|
||||
menuButton = new JFXButton();
|
||||
menuButton.setPrefHeight(55);
|
||||
menuButton.setPrefWidth(230);
|
||||
menuButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
menuButton.getStyleClass().add("jfx-button-raised");
|
||||
menuButton.setStyle("-fx-font-size: 15px;");
|
||||
menuButton.setOnMouseClicked(e -> onMenu());
|
||||
menuButton.setClip(new Rectangle(211, -100, 100, 200));
|
||||
StackPane graphic = new StackPane();
|
||||
Node svg = SVG.triangle(Theme.whiteFillBinding(), 10, 10);
|
||||
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
|
||||
graphic.getChildren().setAll(svg);
|
||||
graphic.setTranslateX(12);
|
||||
menuButton.setGraphic(graphic);
|
||||
|
||||
launchPane.getChildren().setAll(launchButton, separator, menuButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(updatePane, launchPane);
|
||||
|
||||
menu.setMaxHeight(365);
|
||||
menu.setMaxWidth(545);
|
||||
menu.setAlwaysShowingVBar(true);
|
||||
menu.setOnMouseClicked(e -> popup.hide());
|
||||
Bindings.bindContent(menu.getContent(), versions);
|
||||
}
|
||||
|
||||
private void loadVersions(Profile profile) {
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
List<Node> children = repository.getVersions().parallelStream()
|
||||
.filter(version -> !version.isHidden())
|
||||
.sorted(Comparator.comparing(Version::getReleaseTime).thenComparing(a -> VersionNumber.asVersion(a.getId())))
|
||||
.map(version -> {
|
||||
StackPane pane = new StackPane();
|
||||
GameItem item = new GameItem(profile, version.getId());
|
||||
pane.getChildren().setAll(item);
|
||||
pane.getStyleClass().setAll("menu-container");
|
||||
item.setMouseTransparent(true);
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
container.setOnMouseClicked(e -> {
|
||||
profile.setSelectedVersion(version.getId());
|
||||
popup.hide();
|
||||
});
|
||||
return container;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
JFXUtilities.runInFX(() -> {
|
||||
if (profile == Profiles.getSelectedProfile())
|
||||
menu.getContent().setAll(children);
|
||||
});
|
||||
private void doAnimation(boolean show) {
|
||||
Duration duration = Duration.millis(320);
|
||||
Timeline nowAnimation = new Timeline();
|
||||
nowAnimation.getKeyFrames().addAll(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(updatePane.translateXProperty(), show ? 260 : 0, SINE)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(updatePane.translateXProperty(), show ? 0 : 260, SINE)));
|
||||
if (show) nowAnimation.getKeyFrames().add(
|
||||
new KeyFrame(Duration.ZERO, e -> updatePane.setVisible(true)));
|
||||
else nowAnimation.getKeyFrames().add(
|
||||
new KeyFrame(duration, e -> updatePane.setVisible(false)));
|
||||
nowAnimation.play();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void launch() {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onMenu() {
|
||||
popup.show(btnMenu, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.RIGHT, 0, -btnMenu.getHeight());
|
||||
popup.show(menuButton, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.RIGHT, 0, -menuButton.getHeight());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onUpgrade() {
|
||||
RemoteVersion target = UpdateChecker.getLatestVersion();
|
||||
if (target == null) {
|
||||
@@ -173,6 +200,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
UpdateHandler.updateFrom(target);
|
||||
}
|
||||
|
||||
private void closeUpdateBubble() {
|
||||
showUpdate.unbind();
|
||||
showUpdate.set(false);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
@@ -185,4 +217,44 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getCurrentGame() {
|
||||
return currentGame.get();
|
||||
}
|
||||
|
||||
public StringProperty currentGameProperty() {
|
||||
return currentGame;
|
||||
}
|
||||
|
||||
public void setCurrentGame(String currentGame) {
|
||||
this.currentGame.set(currentGame);
|
||||
}
|
||||
|
||||
public boolean isShowUpdate() {
|
||||
return showUpdate.get();
|
||||
}
|
||||
|
||||
public BooleanProperty showUpdateProperty() {
|
||||
return showUpdate;
|
||||
}
|
||||
|
||||
public void setShowUpdate(boolean showUpdate) {
|
||||
this.showUpdate.set(showUpdate);
|
||||
}
|
||||
|
||||
public String getLatestVersion() {
|
||||
return latestVersion.get();
|
||||
}
|
||||
|
||||
public StringProperty latestVersionProperty() {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
public void setLatestVersion(String latestVersion) {
|
||||
this.latestVersion.set(latestVersion);
|
||||
}
|
||||
|
||||
public ObservableList<Node> getVersions() {
|
||||
return versions;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import javafx.beans.property.*;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
@@ -200,6 +201,11 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
|
||||
UpdateHandler.updateFrom(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onOpenLogFolder() {
|
||||
FXUtils.openFolder(Launcher.LOG_DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHelp() {
|
||||
FXUtils.openLink(Metadata.HELP_URL);
|
||||
|
||||
@@ -333,34 +333,49 @@ public abstract class SettingsView extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
VBox fontPane = new VBox();
|
||||
fontPane.setSpacing(5);
|
||||
settingsPane.getContent().add(fontPane);
|
||||
ComponentSublist logPane = new ComponentSublist();
|
||||
logPane.setTitle(i18n("settings.launcher.log"));
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
fontPane.getChildren().add(borderPane);
|
||||
{
|
||||
Label left = new Label(i18n("settings.launcher.log_font"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
borderPane.setLeft(left);
|
||||
}
|
||||
JFXButton logButton = new JFXButton(i18n("settings.launcher.log.dir"));
|
||||
logButton.setOnMouseClicked(e -> onOpenLogFolder());
|
||||
logButton.getStyleClass().setAll("jfx-button-border");
|
||||
|
||||
{
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(3);
|
||||
|
||||
cboFont = new FontComboBox(12, false);
|
||||
txtFontSize = new JFXTextField();
|
||||
FXUtils.setLimitWidth(txtFontSize, 50);
|
||||
hBox.getChildren().setAll(cboFont, txtFontSize);
|
||||
|
||||
borderPane.setRight(hBox);
|
||||
}
|
||||
logPane.setHeaderRight(logButton);
|
||||
}
|
||||
|
||||
lblDisplay = new Label("[23:33:33] [Client Thread/INFO] [WaterPower]: Loaded mod WaterPower.");
|
||||
fontPane.getChildren().add(lblDisplay);
|
||||
{
|
||||
VBox fontPane = new VBox();
|
||||
fontPane.setSpacing(5);
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
fontPane.getChildren().add(borderPane);
|
||||
{
|
||||
Label left = new Label(i18n("settings.launcher.log.font"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
borderPane.setLeft(left);
|
||||
}
|
||||
|
||||
{
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(3);
|
||||
|
||||
cboFont = new FontComboBox(12, false);
|
||||
txtFontSize = new JFXTextField();
|
||||
FXUtils.setLimitWidth(txtFontSize, 50);
|
||||
hBox.getChildren().setAll(cboFont, txtFontSize);
|
||||
|
||||
borderPane.setRight(hBox);
|
||||
}
|
||||
}
|
||||
|
||||
lblDisplay = new Label("[23:33:33] [Client Thread/INFO] [WaterPower]: Loaded mod WaterPower.");
|
||||
fontPane.getChildren().add(lblDisplay);
|
||||
|
||||
logPane.getContent().add(fontPane);
|
||||
}
|
||||
settingsPane.getContent().add(logPane);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -477,4 +492,5 @@ public abstract class SettingsView extends StackPane {
|
||||
|
||||
protected abstract void onUpdate();
|
||||
protected abstract void onHelp();
|
||||
protected abstract void onOpenLogFolder();
|
||||
}
|
||||
@@ -88,11 +88,16 @@ class ComponentListCell extends StackPane {
|
||||
VBox labelVBox = new VBox();
|
||||
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
boolean overrideHeaderLeft = false;
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
|
||||
if (leftNode != null)
|
||||
if (leftNode != null) {
|
||||
labelVBox.getChildren().setAll(leftNode);
|
||||
} else {
|
||||
overrideHeaderLeft = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!overrideHeaderLeft) {
|
||||
Label label = new Label();
|
||||
label.textProperty().bind(list.titleProperty());
|
||||
labelVBox.getChildren().add(label);
|
||||
@@ -108,6 +113,7 @@ class ComponentListCell extends StackPane {
|
||||
groupNode.setLeft(labelVBox);
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setSpacing(16);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node rightNode = ((ComponentSublist) list).getHeaderRight();
|
||||
|
||||
@@ -30,6 +30,7 @@ import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Stack;
|
||||
@@ -56,10 +57,19 @@ public class Navigator extends StackPane {
|
||||
if (from == node)
|
||||
return;
|
||||
|
||||
Logging.LOG.info("Navigate to " + node);
|
||||
|
||||
stack.push(node);
|
||||
fireEvent(new NavigationEvent(this, from, NavigationEvent.NAVIGATING));
|
||||
|
||||
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
|
||||
fireEvent(navigating);
|
||||
node.fireEvent(navigating);
|
||||
|
||||
setContent(node);
|
||||
fireEvent(new NavigationEvent(this, node, NavigationEvent.NAVIGATED));
|
||||
|
||||
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
|
||||
fireEvent(navigated);
|
||||
node.fireEvent(navigated);
|
||||
|
||||
EventHandler<PageCloseEvent> handler = event -> close(node);
|
||||
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
|
||||
@@ -79,14 +89,19 @@ public class Navigator extends StackPane {
|
||||
public void close(Node from) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
Logging.LOG.info("Closed page " + from);
|
||||
|
||||
if (stack.peek() != from)
|
||||
throw new IllegalStateException();
|
||||
stack.pop();
|
||||
Node node = stack.peek();
|
||||
|
||||
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
|
||||
fireEvent(navigating);
|
||||
node.fireEvent(navigating);
|
||||
|
||||
setContent(node);
|
||||
|
||||
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
|
||||
fireEvent(navigated);
|
||||
node.fireEvent(navigated);
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.When;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
@@ -25,12 +28,14 @@ import javafx.scene.control.Control;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class PopupMenu extends Control {
|
||||
|
||||
private final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||
private final BooleanProperty alwaysShowingVBar = new SimpleBooleanProperty();
|
||||
|
||||
public PopupMenu() {
|
||||
}
|
||||
@@ -39,11 +44,31 @@ public class PopupMenu extends Control {
|
||||
return content;
|
||||
}
|
||||
|
||||
public boolean isAlwaysShowingVBar() {
|
||||
return alwaysShowingVBar.get();
|
||||
}
|
||||
|
||||
public BooleanProperty alwaysShowingVBarProperty() {
|
||||
return alwaysShowingVBar;
|
||||
}
|
||||
|
||||
public void setAlwaysShowingVBar(boolean alwaysShowingVBar) {
|
||||
this.alwaysShowingVBar.set(alwaysShowingVBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new PopupMenuSkin();
|
||||
}
|
||||
|
||||
public static Node wrapPopupMenuItem(Node node) {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getChildren().setAll(node);
|
||||
pane.getStyleClass().setAll("menu-container");
|
||||
node.setMouseTransparent(true);
|
||||
return new RipplerContainer(pane);
|
||||
}
|
||||
|
||||
private class PopupMenuSkin extends SkinBase<PopupMenu> {
|
||||
|
||||
protected PopupMenuSkin() {
|
||||
@@ -52,6 +77,9 @@ public class PopupMenu extends Control {
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.vbarPolicyProperty().bind(new When(alwaysShowingVBar)
|
||||
.then(ScrollPane.ScrollBarPolicy.ALWAYS)
|
||||
.otherwise(ScrollPane.ScrollBarPolicy.AS_NEEDED));
|
||||
|
||||
VBox content = new VBox();
|
||||
content.getStyleClass().add("menu");
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
|
||||
public class DecoratorControl extends Control {
|
||||
public class Decorator extends Control {
|
||||
private final ListProperty<Node> drawer = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
private final ListProperty<Node> content = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
private final ListProperty<Node> container = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
@@ -49,7 +49,7 @@ public class DecoratorControl extends Control {
|
||||
private final Stage primaryStage;
|
||||
private StackPane drawerWrapper;
|
||||
|
||||
public DecoratorControl(Stage primaryStage) {
|
||||
public Decorator(Stage primaryStage) {
|
||||
this.primaryStage = primaryStage;
|
||||
|
||||
primaryStage.initStyle(StageStyle.UNDECORATED);
|
||||
@@ -69,7 +69,7 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
public class DecoratorController {
|
||||
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
|
||||
|
||||
private final DecoratorControl decorator;
|
||||
private final Decorator decorator;
|
||||
private final ImageView welcomeView;
|
||||
private final Navigator navigator;
|
||||
private final Node mainPage;
|
||||
@@ -80,7 +80,7 @@ public class DecoratorController {
|
||||
public DecoratorController(Stage stage, Node mainPage) {
|
||||
this.mainPage = mainPage;
|
||||
|
||||
decorator = new DecoratorControl(stage);
|
||||
decorator = new Decorator(stage);
|
||||
decorator.titleProperty().set(Metadata.TITLE);
|
||||
decorator.setOnCloseButtonAction(Launcher::stopApplication);
|
||||
|
||||
@@ -106,15 +106,18 @@ public class DecoratorController {
|
||||
);
|
||||
nowAnimation.play();
|
||||
});
|
||||
if (ConfigHolder.isNewlyCreated() && config().getLocalization().getLocale() == Locale.CHINA)
|
||||
|
||||
if (ConfigHolder.config().getUiVersion() < Controllers.UI_VERSION && config().getLocalization().getLocale() == Locale.CHINA) {
|
||||
ConfigHolder.config().setUiVersion(Controllers.UI_VERSION);
|
||||
decorator.getContainer().setAll(welcomeView);
|
||||
}
|
||||
|
||||
setupBackground();
|
||||
|
||||
setupAuthlibInjectorDnD();
|
||||
}
|
||||
|
||||
public DecoratorControl getDecorator() {
|
||||
public Decorator getDecorator() {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
public class DecoratorSkin extends SkinBase<DecoratorControl> {
|
||||
public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE),
|
||||
glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); });
|
||||
|
||||
@@ -62,14 +62,14 @@ public class DecoratorSkin extends SkinBase<DecoratorControl> {
|
||||
*
|
||||
* @param control The control for which this Skin should attach to.
|
||||
*/
|
||||
public DecoratorSkin(DecoratorControl control) {
|
||||
public DecoratorSkin(Decorator control) {
|
||||
super(control);
|
||||
|
||||
primaryStage = control.getPrimaryStage();
|
||||
|
||||
minus.fillProperty().bind(Theme.foregroundFillBinding());
|
||||
|
||||
DecoratorControl skinnable = getSkinnable();
|
||||
Decorator skinnable = getSkinnable();
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.getStyleClass().setAll("jfx-decorator", "resize-border");
|
||||
@@ -146,7 +146,7 @@ public class DecoratorSkin extends SkinBase<DecoratorControl> {
|
||||
titleContainer.setOnMouseMoved(this::onMouseMoved);
|
||||
titleContainer.setPickOnBounds(false);
|
||||
titleContainer.setMinHeight(40);
|
||||
titleContainer.getStyleClass().setAll("jfx-tool-bar");
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar", "window-title-bar");
|
||||
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true);
|
||||
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
|
||||
if (!isDragging) allowMove = false;
|
||||
@@ -169,9 +169,9 @@ public class DecoratorSkin extends SkinBase<DecoratorControl> {
|
||||
titleWrapper.setCenter(lblTitle);
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
separator.getStyleClass().add("separator");
|
||||
separator.heightProperty().bind(titleWrapper.heightProperty());
|
||||
separator.setWidth(1);
|
||||
separator.setFill(Color.GRAY);
|
||||
titleWrapper.setRight(separator);
|
||||
}
|
||||
titleContainer.setLeft(titleWrapper);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
private Profile profile;
|
||||
private final File file;
|
||||
|
||||
public ModpackInstallWizardProvider() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(File modpackFile) {
|
||||
this.file = modpackFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Map<String, Object> settings) {
|
||||
profile = Profiles.getSelectedProfile();
|
||||
|
||||
if (file != null)
|
||||
settings.put(ModpackPage.MODPACK_FILE, file);
|
||||
settings.put(PROFILE, profile);
|
||||
}
|
||||
|
||||
private Task finishModpackInstallingAsync(Map<String, Object> settings) {
|
||||
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
|
||||
return null;
|
||||
|
||||
File selected = tryCast(settings.get(ModpackPage.MODPACK_FILE), File.class).orElse(null);
|
||||
Modpack modpack = tryCast(settings.get(ModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
|
||||
String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null);
|
||||
if (selected == null || modpack == null || name == null) return null;
|
||||
|
||||
return ModpackHelper.getInstallTask(profile, selected, name, modpack)
|
||||
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_message", i18n("install.failed"));
|
||||
|
||||
return finishModpackInstallingAsync(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new ModpackPage(controller);
|
||||
default:
|
||||
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static final String PROFILE = "PROFILE";
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WebStage;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
@@ -39,7 +40,9 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackPage extends StackPane implements WizardPage {
|
||||
@@ -75,31 +78,41 @@ public final class ModpackPage extends StackPane implements WizardPage {
|
||||
|
||||
Profile profile = (Profile) controller.getSettings().get("PROFILE");
|
||||
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("modpack.choose"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile == null) Platform.runLater(controller::onEnd);
|
||||
else {
|
||||
File selectedFile;
|
||||
|
||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
||||
if (filePath.isPresent()) {
|
||||
selectedFile = filePath.get();
|
||||
} else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("modpack.choose"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile == null) {
|
||||
Platform.runLater(controller::onEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.getSettings().put(MODPACK_FILE, selectedFile);
|
||||
}
|
||||
|
||||
try {
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile);
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
|
||||
|
||||
lblModpackLocation.setText(selectedFile.getAbsolutePath());
|
||||
txtModpackName.getValidators().addAll(
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
|
||||
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
|
||||
);
|
||||
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
|
||||
|
||||
try {
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile);
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
|
||||
} catch (UnsupportedModpackException e) {
|
||||
txtModpackName.setText(i18n("modpack.task.install.error"));
|
||||
btnInstall.setDisable(true);
|
||||
}
|
||||
} catch (UnsupportedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
|
||||
Platform.runLater(controller::onEnd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-34
@@ -21,8 +21,6 @@ import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -30,18 +28,14 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class DownloadWizardProvider implements WizardProvider {
|
||||
public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
private Profile profile;
|
||||
private final int type;
|
||||
|
||||
public DownloadWizardProvider(int type) {
|
||||
this.type = type;
|
||||
public VanillaInstallWizardProvider() {
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,29 +64,12 @@ public final class DownloadWizardProvider implements WizardProvider {
|
||||
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
|
||||
}
|
||||
|
||||
private Task finishModpackInstallingAsync(Map<String, Object> settings) {
|
||||
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
|
||||
return null;
|
||||
|
||||
File selected = tryCast(settings.get(ModpackPage.MODPACK_FILE), File.class).orElse(null);
|
||||
Modpack modpack = tryCast(settings.get(ModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
|
||||
String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null);
|
||||
if (selected == null || modpack == null || name == null) return null;
|
||||
|
||||
return ModpackHelper.getInstallTask(profile, selected, name, modpack)
|
||||
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_message", i18n("install.failed"));
|
||||
|
||||
switch (type) {
|
||||
case 0: return finishVersionDownloadingAsync(settings);
|
||||
case 1: return finishModpackInstallingAsync(settings);
|
||||
default: return null;
|
||||
}
|
||||
return finishVersionDownloadingAsync(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,14 +77,7 @@ public final class DownloadWizardProvider implements WizardProvider {
|
||||
DownloadProvider provider = profile.getDependency().getDownloadProvider();
|
||||
switch (step) {
|
||||
case 0:
|
||||
switch (type) {
|
||||
case 0:
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", provider, "game", () -> controller.onNext(new InstallersPage(controller, profile.getRepository(), provider)));
|
||||
case 1:
|
||||
return new ModpackPage(controller);
|
||||
default:
|
||||
throw new IllegalStateException("Error step " + step + ", subStep " + type + ", settings: " + settings + ", pages: " + controller.getPages());
|
||||
}
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", provider, "game", () -> controller.onNext(new InstallersPage(controller, profile.getRepository(), provider)));
|
||||
default:
|
||||
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
|
||||
}
|
||||
@@ -30,7 +30,8 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
|
||||
public GameAdvancedListItem() {
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
|
||||
if (version != null) {
|
||||
if (version != null && Profiles.getSelectedProfile() != null &&
|
||||
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
|
||||
setTitle(version);
|
||||
setSubtitle(null);
|
||||
setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
|
||||
|
||||
@@ -33,7 +33,8 @@ import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
@@ -99,11 +100,11 @@ public class GameList extends Control implements DecoratorPage {
|
||||
}
|
||||
|
||||
public void addNewGame() {
|
||||
Controllers.getDecorator().startWizard(new DownloadWizardProvider(0), i18n("install.new_game"));
|
||||
Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(), i18n("install.new_game"));
|
||||
}
|
||||
|
||||
public void importModpack() {
|
||||
Controllers.getDecorator().startWizard(new DownloadWizardProvider(1), i18n("install.modpack"));
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(), i18n("install.modpack"));
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
|
||||
@@ -28,7 +28,9 @@ import javafx.scene.control.Tab;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
@@ -62,6 +64,8 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
@FXML
|
||||
private JFXButton btnExport;
|
||||
@FXML
|
||||
private JFXButton btnTestGame;
|
||||
@FXML
|
||||
private StackPane rootPane;
|
||||
@FXML
|
||||
private StackPane contentPane;
|
||||
@@ -85,7 +89,7 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
new IconedMenuItem(null, i18n("folder.config"), FXUtils.withJFXPopupClosing(() -> onBrowse("config"), browsePopup)),
|
||||
new IconedMenuItem(null, i18n("folder.resourcepacks"), FXUtils.withJFXPopupClosing(() -> onBrowse("resourcepacks"), browsePopup)),
|
||||
new IconedMenuItem(null, i18n("folder.screenshots"), FXUtils.withJFXPopupClosing(() -> onBrowse("screenshots"), browsePopup)),
|
||||
new IconedMenuItem(null, i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> onBrowse("resourcepacks"), browsePopup))
|
||||
new IconedMenuItem(null, i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> onBrowse("saves"), browsePopup))
|
||||
);
|
||||
|
||||
PopupMenu managementList = new PopupMenu();
|
||||
@@ -102,6 +106,9 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
FXUtils.installTooltip(btnManagementMenu, i18n("settings.game.management"));
|
||||
FXUtils.installTooltip(btnExport, i18n("modpack.export"));
|
||||
|
||||
btnTestGame.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20));
|
||||
FXUtils.installTooltip(btnTestGame, i18n("version.launch.test"));
|
||||
|
||||
setEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
}
|
||||
|
||||
@@ -134,6 +141,11 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
load(this.version, this.profile);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onTestGame() {
|
||||
Versions.testGame(profile, version);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onBrowseMenu() {
|
||||
browsePopup.show(btnBrowseMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, btnBrowseMenu.getHeight());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.root {
|
||||
-fx-base-color: #5c6bc0;
|
||||
-fx-base-darker-color: derive(-fx-base-color, -10%);
|
||||
-fx-base-check-color: derive(-fx-base-color, 30%);
|
||||
-fx-base-text-fill: white;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
.root {
|
||||
-fx-base-color: %base-color%;
|
||||
-fx-base-darker-color: derive(-fx-base-color, -10%);
|
||||
-fx-base-check-color: derive(-fx-base-color, 30%);
|
||||
-fx-base-text-fill: %font-color%;
|
||||
}
|
||||
@@ -104,6 +104,14 @@
|
||||
-jfx-subtitle-fill: gray;
|
||||
}
|
||||
|
||||
.window-title-bar .separator {
|
||||
-fx-fill: -fx-base-darker-color;
|
||||
}
|
||||
|
||||
.darker-fill {
|
||||
-fx-fill: -fx-base-darker-color;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Tab Pane *
|
||||
@@ -538,7 +546,7 @@
|
||||
.jfx-button-raised {
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: -fx-base-color;
|
||||
-fx-font-size:14px;
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.jfx-button-raised .jfx-rippler {
|
||||
@@ -550,6 +558,22 @@
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.jfx-button-border {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-border-color: gray;
|
||||
-fx-border-radius: 5px;
|
||||
-fx-border-width: 0.2px;
|
||||
-fx-padding: 8px;
|
||||
}
|
||||
|
||||
.jfx-button-border .jfx-rippler {
|
||||
-jfx-rippler-fill: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.jfx-button-border .label {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
}
|
||||
|
||||
.jfx-button-raised-round {
|
||||
-fx-background-color: -fx-base-color;
|
||||
-fx-background-radius: 50px;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<Label fx:id="lblInjectorServer" text="%account.injector.server" GridPane.halignment="RIGHT"
|
||||
GridPane.columnIndex="0" GridPane.rowIndex="1"/>
|
||||
|
||||
<JFXComboBox fx:id="cboServers" promptText="%general.prompt.empty" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
||||
<JFXComboBox fx:id="cboServers" promptText="%account.injector.empty" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
||||
|
||||
<HBox GridPane.columnIndex="2" GridPane.rowIndex="1" spacing="8">
|
||||
<JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer">
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import com.jfoenix.controls.JFXButton?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<fx:root xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
styleClass="transparent"
|
||||
style="-fx-padding: 10 16 10 16;"
|
||||
type="StackPane" pickOnBounds="false">
|
||||
<BorderPane pickOnBounds="false">
|
||||
<left>
|
||||
<HBox alignment="CENTER" mouseTransparent="true">
|
||||
<StackPane fx:id="imageViewContainer" FXUtils.limitWidth="32" FXUtils.limitHeight="32">
|
||||
<ImageView preserveRatio="true" fx:id="imageView" smooth="false">
|
||||
<Image url="/assets/img/icon.png" />
|
||||
</ImageView>
|
||||
</StackPane>
|
||||
<BorderPane style="-fx-padding: 0 0 0 10;">
|
||||
<top>
|
||||
<Label fx:id="lblTitle" maxWidth="90" style="-fx-font-size: 15;" textAlignment="JUSTIFY" />
|
||||
</top>
|
||||
<bottom>
|
||||
<Label fx:id="lblSubtitle" maxWidth="90" style="-fx-font-size: 10;" textAlignment="JUSTIFY" />
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
</HBox>
|
||||
</left>
|
||||
<right>
|
||||
<HBox alignment="CENTER" pickOnBounds="false">
|
||||
<JFXButton fx:id="btnSettings" maxWidth="40" minWidth="40" prefWidth="40" styleClass="toggle-icon4">
|
||||
<graphic>
|
||||
<fx:include source="/assets/svg/gear.fxml" />
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
</HBox>
|
||||
</right>
|
||||
</BorderPane>
|
||||
</fx:root>
|
||||
@@ -22,7 +22,7 @@
|
||||
<BorderPane><left><Label text="%archive.version"/></left><right><Label fx:id="lblVersion" /></right></BorderPane>
|
||||
<BorderPane><left><Label text="%archive.author"/></left><right><Label fx:id="lblAuthor" /></right></BorderPane>
|
||||
<BorderPane>
|
||||
<left><JFXButton fx:id="btnDescription" onMouseClicked="#onDescribe" text="%modpack.wizard.step.3" styleClass="jfx-button" /></left>
|
||||
<left><JFXButton fx:id="btnDescription" onMouseClicked="#onDescribe" text="%modpack.wizard.step.3" styleClass="jfx-button-border" /></left>
|
||||
<right><JFXButton buttonType="RAISED" fx:id="btnInstall" onMouseClicked="#onInstall" text="%button.install" styleClass="jfx-button-raised" /></right>
|
||||
</BorderPane>
|
||||
</ComponentList>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import com.jfoenix.controls.JFXButton?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.shape.Rectangle?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import org.jackhuang.hmcl.ui.construct.TwoLineListItem?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<fx:root type="StackPane" pickOnBounds="false"
|
||||
xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<StackPane fx:id="main" style="-fx-padding: 25;">
|
||||
<StackPane fx:id="updatePane" styleClass="bubble" FXUtils.limitWidth="230" FXUtils.limitHeight="55"
|
||||
onMouseClicked="#onUpgrade" StackPane.alignment="TOP_RIGHT">
|
||||
<HBox StackPane.alignment="CENTER_LEFT" alignment="CENTER_LEFT" spacing="12">
|
||||
<Label fx:id="lblIcon" />
|
||||
<TwoLineListItem titleFill="white" subtitleFill="white" subtitle="%update.bubble.subtitle" fx:id="lblLatestVersion" pickOnBounds="false"
|
||||
style="-jfx-title-font-weight: BOLD;" />
|
||||
|
||||
<StackPane.margin>
|
||||
<Insets left="16" top="9" right="12" bottom="9" />
|
||||
</StackPane.margin>
|
||||
</HBox>
|
||||
|
||||
<JFXButton fx:id="closeUpdateButton" StackPane.alignment="TOP_RIGHT" styleClass="toggle-icon-tiny">
|
||||
<StackPane.margin>
|
||||
<Insets topRightBottomLeft="5" />
|
||||
</StackPane.margin>
|
||||
</JFXButton>
|
||||
</StackPane>
|
||||
|
||||
<StackPane maxWidth="230" maxHeight="55" StackPane.alignment="BOTTOM_RIGHT">
|
||||
<JFXButton prefWidth="230" prefHeight="55" buttonType="RAISED" styleClass="jfx-button-raised"
|
||||
onMouseClicked="#launch" fx:id="btnLaunch">
|
||||
<graphic>
|
||||
<VBox alignment="CENTER" translateX="-7" maxWidth="200">
|
||||
<Label style="-fx-font-size: 16;" text="%version.launch"/>
|
||||
<Label style="-fx-font-size: 12px;" fx:id="lblCurrentGame"/>
|
||||
</VBox>
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
<Rectangle fx:id="separator" translateX="95" height="57" fill="gray" width="1" mouseTransparent="true"/>
|
||||
<JFXButton prefWidth="230" prefHeight="55" buttonType="RAISED" styleClass="jfx-button-raised"
|
||||
style="-fx-font-size: 15;" onMouseClicked="#onMenu" fx:id="btnMenu" />
|
||||
</StackPane>
|
||||
</StackPane>
|
||||
</fx:root>
|
||||
@@ -30,6 +30,8 @@
|
||||
</JFXTabPane>
|
||||
|
||||
<HBox alignment="TOP_RIGHT" style="-fx-padding: 4;" spacing="3" pickOnBounds="false">
|
||||
<JFXButton fx:id="btnTestGame" maxHeight="40.0" minHeight="40.0" onMouseClicked="#onTestGame"
|
||||
styleClass="toggle-icon3" />
|
||||
<JFXButton fx:id="btnBrowseMenu" maxHeight="40.0" minHeight="40.0" onMouseClicked="#onBrowseMenu"
|
||||
styleClass="toggle-icon3">
|
||||
<graphic>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 292 KiB |
@@ -43,6 +43,7 @@ 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.server_response_malformed=Invalid server response. The authentication server may have a failure.
|
||||
account.injector.add=Add an authentication server
|
||||
account.injector.empty=Empty (Click the plus button right to add)
|
||||
account.injector.manage=Manage authentication servers
|
||||
account.injector.manage.title=Authentication servers
|
||||
account.injector.http=Warning: This server is using HTTP, which will cause your password be transmitted in clear text.
|
||||
@@ -55,7 +56,7 @@ account.methods.authlib_injector=authlib-injector
|
||||
account.methods.offline=Offline
|
||||
account.methods.yggdrasil=Mojang
|
||||
account.missing=No Account
|
||||
account.missing.add=Click the button on the right to add
|
||||
account.missing.add=Click here to add
|
||||
account.password=Password
|
||||
account.username=Name
|
||||
|
||||
@@ -109,8 +110,6 @@ folder.resourcepacks=Resource packs
|
||||
folder.saves=Saves
|
||||
folder.screenshots=Screenshots
|
||||
|
||||
general.prompt.empty=(None)
|
||||
|
||||
help=Documentations
|
||||
help.detail=For datapacks, modpacks, etc. makers.
|
||||
|
||||
@@ -140,6 +139,7 @@ lang=English
|
||||
lang.default=Use system language
|
||||
|
||||
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
|
||||
launch.advice.forge2760_liteloader=Forge 2760 and above is not compatible with LiteLoader, please consider downgrade Forge to 2759 or earlier.
|
||||
launch.advice.java8_1_13=Minecraft 1.13 and later versions can only run on Java 8 or later.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.0_51. You are supposed to install the latest version of Java 8.
|
||||
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or later Java versions.
|
||||
@@ -164,8 +164,8 @@ launcher=Launcher
|
||||
launcher.background=Background Image
|
||||
launcher.background.choose=Choose background path.
|
||||
launcher.background.default=Default
|
||||
launcher.common_directory=Cache Directory (Store downloaded files)
|
||||
launcher.common_directory.choose=Choose cache directory.
|
||||
launcher.common_directory=Download Cache Directory
|
||||
launcher.common_directory.choose=Choose download cache directory
|
||||
launcher.common_directory.default=Default
|
||||
launcher.common_directory.disabled=Disabled
|
||||
launcher.contact=Contact Us
|
||||
@@ -217,11 +217,12 @@ modpack.name=Modpack Name
|
||||
modpack.not_a_valid_name=Not a valid modpack name
|
||||
modpack.scan=Scanning this modpack
|
||||
modpack.task.install=Import Modpack
|
||||
modpack.task.install.error=Failed to install the modpack. Maybe the files is incorrect, or a management issue occurred.
|
||||
modpack.task.install.error=This modpack file cannot be recognized. Only Curse, MultiMC modpacks are supported.
|
||||
modpack.task.install.will=Install the modpack:
|
||||
modpack.type.curse=Curse
|
||||
modpack.type.curse.completion=Install relative files to Curse modpack
|
||||
modpack.type.curse.error=Unable to complete this Curse modpack. Please retry.
|
||||
modpack.type.curse.not_found=Some of required resources are deleted and cannot be downloaded. Please consider the latest version or other modpacks.
|
||||
modpack.type.hmcl=HMCL
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.unsupported=Unsupported modpack, only HMCL, MultiMC, Curse modpacks are supported.
|
||||
@@ -325,7 +326,9 @@ settings.launcher.common_path.tooltip=This app will save all game libraries and
|
||||
settings.launcher.download_source=Download Source
|
||||
settings.launcher.enable_game_list=Display game list in main page
|
||||
settings.launcher.language=Language
|
||||
settings.launcher.log_font=Log Font
|
||||
settings.launcher.log=Log
|
||||
settings.launcher.log.font=Log Font
|
||||
settings.launcher.log.dir=Open Log Directory
|
||||
settings.launcher.proxy=Proxy
|
||||
settings.launcher.proxy.authentication=Proxy Authentication
|
||||
settings.launcher.proxy.disable=Use system proxies
|
||||
|
||||
@@ -43,6 +43,7 @@ account.failed.invalid_token=請嘗試登出並重新輸入密碼登入
|
||||
account.failed.no_character=該帳號沒有角色
|
||||
account.failed.server_response_malformed=無法解析認證伺服器響應,可能是伺服器故障
|
||||
account.injector.add=新增認證伺服器
|
||||
account.injector.empty=無(點擊右側加號添加)
|
||||
account.injector.manage=管理認證伺服器
|
||||
account.injector.manage.title=認證伺服器
|
||||
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
|
||||
@@ -55,7 +56,7 @@ account.methods.authlib_injector=authlib-injector 登入
|
||||
account.methods.offline=離線模式
|
||||
account.methods.yggdrasil=正版登入
|
||||
account.missing=沒有遊戲帳戶
|
||||
account.missing.add=點擊右邊按鈕添加
|
||||
account.missing.add=點擊此處添加帳戶
|
||||
account.password=密碼
|
||||
account.username=使用者名稱
|
||||
|
||||
@@ -109,8 +110,6 @@ folder.resourcepacks=資源包資料夾
|
||||
folder.saves=遊戲存檔資料夾
|
||||
folder.screenshots=截圖資料夾
|
||||
|
||||
general.prompt.empty=(無)
|
||||
|
||||
help=HMCL 幫助文檔
|
||||
help.detail=可查閱資料包、整合包製作指南等內容。
|
||||
|
||||
@@ -140,6 +139,7 @@ lang=正體中文
|
||||
lang.default=跟隨系統語言
|
||||
|
||||
launch.advice.different_platform=您的系統為 64 位元,但是 Java 是 32 位元,建議您安裝 64 位 Java。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容。請刪除 LiteLoader 或者更換 Forge 至 2759 或更舊的版本。
|
||||
launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。
|
||||
launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod Minecraft 版本 不支援 Java 9 或更高版本,請使用 Java 8。
|
||||
@@ -164,8 +164,8 @@ launcher=啟動器
|
||||
launcher.background=背景位址
|
||||
launcher.background.choose=選擇背景路徑
|
||||
launcher.background.default=預設(自動尋找啟動器同目錄下的 background.png/jpg 及 bg 資料夾內的圖片)
|
||||
launcher.common_directory=緩存目錄(檔案下載緩存)
|
||||
launcher.common_directory.choose=選擇緩存目錄
|
||||
launcher.common_directory=檔案下載緩存目錄
|
||||
launcher.common_directory.choose=選擇檔案下載緩存目錄
|
||||
launcher.common_directory.default=預設
|
||||
launcher.common_directory.disabled=停用
|
||||
launcher.contact=聯絡我們
|
||||
@@ -217,11 +217,12 @@ modpack.name=整合包名稱
|
||||
modpack.not_a_valid_name=整合包名稱無效
|
||||
modpack.scan=解析整合包
|
||||
modpack.task.install=匯入整合包
|
||||
modpack.task.install.error=安裝失敗,可能是整合包格式錯誤或操作檔案失敗。
|
||||
modpack.task.install.error=無法識別該整合包,目前僅支持導入 Curse、MultiMC、HMCL 整合包。
|
||||
modpack.task.install.will=將會安裝整合包:
|
||||
modpack.type.curse=Curse
|
||||
modpack.type.curse.completion=下載 Curse 整合包相關檔案
|
||||
modpack.type.curse.error=無法完成 Curse 整合包的下載,請多次重試或設定代理
|
||||
modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載,請嘗試該整合包的最新版本或者安裝其他整合包。
|
||||
modpack.type.hmcl=HMCL
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.unsupported=不支援該整合包。僅 HMCL、MultiMC、Curse 整合包受支援。
|
||||
@@ -325,7 +326,9 @@ settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及依賴庫
|
||||
settings.launcher.download_source=下載來源
|
||||
settings.launcher.enable_game_list=在首頁內顯示遊戲列表
|
||||
settings.launcher.language=語言
|
||||
settings.launcher.log_font=記錄字體
|
||||
settings.launcher.log=記錄
|
||||
settings.launcher.log.font=記錄字體
|
||||
settings.launcher.log.dir=打開記錄資料夾
|
||||
settings.launcher.proxy=代理
|
||||
settings.launcher.proxy.authentication=身份驗證
|
||||
settings.launcher.proxy.disable=使用系統代理
|
||||
|
||||
@@ -43,6 +43,7 @@ account.failed.invalid_token=请尝试登出并重新输入密码登录
|
||||
account.failed.no_character=该帐号没有角色
|
||||
account.failed.server_response_malformed=无法解析认证服务器响应,可能是服务器故障
|
||||
account.injector.add=添加认证服务器
|
||||
account.injector.empty=无(点击右侧加号添加)
|
||||
account.injector.manage=管理认证服务器
|
||||
account.injector.manage.title=认证服务器
|
||||
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
|
||||
@@ -55,7 +56,7 @@ account.methods.authlib_injector=外置登录 (authlib-injector)
|
||||
account.methods.offline=离线模式
|
||||
account.methods.yggdrasil=正版登录
|
||||
account.missing=没有游戏账户
|
||||
account.missing.add=点击右边按钮添加
|
||||
account.missing.add=点击此处添加账户
|
||||
account.password=密码
|
||||
account.username=用户名
|
||||
|
||||
@@ -109,8 +110,6 @@ folder.resourcepacks=资源包文件夹
|
||||
folder.saves=存档文件夹
|
||||
folder.screenshots=截图文件夹
|
||||
|
||||
general.prompt.empty=(无)
|
||||
|
||||
help=HMCL 帮助文档
|
||||
help.detail=可查阅数据包、整合包制作指南等内容。
|
||||
|
||||
@@ -140,6 +139,7 @@ lang=简体中文
|
||||
lang.default=跟随系统语言
|
||||
|
||||
launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2759 或更旧的版本。
|
||||
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装最新版的 Java 8。
|
||||
launch.advice.java9=低于 1.13 的有安装 Mod 的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
|
||||
@@ -164,8 +164,8 @@ launcher=启动器
|
||||
launcher.background=背景地址
|
||||
launcher.background.choose=选择背景路径
|
||||
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg 及 bg 文件夹内的图片)
|
||||
launcher.common_directory=缓存目录(文件下载缓存)
|
||||
launcher.common_directory.choose=选择缓存目录
|
||||
launcher.common_directory=文件下载缓存目录
|
||||
launcher.common_directory.choose=选择文件下载缓存目录
|
||||
launcher.common_directory.default=默认(%AppData%/.minecraft 或者 ~/.minecraft)
|
||||
launcher.common_directory.disabled=禁用(总是使用游戏路径)
|
||||
launcher.contact=联系我们
|
||||
@@ -217,11 +217,12 @@ modpack.name=整合包名称
|
||||
modpack.not_a_valid_name=不是一个有效的整合包名称
|
||||
modpack.scan=解析整合包
|
||||
modpack.task.install=导入整合包
|
||||
modpack.task.install.error=安装失败,可能是整合包格式不正确或操作文件失败。
|
||||
modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、MultiMC、HMCL 整合包。
|
||||
modpack.task.install.will=将会安装整合包:
|
||||
modpack.type.curse=Curse
|
||||
modpack.type.curse.completion=下载 Curse 整合包相关文件
|
||||
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
|
||||
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。
|
||||
modpack.type.hmcl=HMCL
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.unsupported=该整合包不被支持。仅 HMCL、MultiMC、Curse 整合包受支持。
|
||||
@@ -325,7 +326,9 @@ settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库
|
||||
settings.launcher.download_source=下载源
|
||||
settings.launcher.enable_game_list=在主页内显示游戏列表
|
||||
settings.launcher.language=语言
|
||||
settings.launcher.log_font=日志字体
|
||||
settings.launcher.log=日志
|
||||
settings.launcher.log.font=日志字体
|
||||
settings.launcher.log.dir=打开日志文件夹
|
||||
settings.launcher.proxy=代理
|
||||
settings.launcher.proxy.authentication=身份验证
|
||||
settings.launcher.proxy.disable=使用系统代理
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 941 KiB |
@@ -35,9 +35,12 @@ public class LibraryDownloadTask extends Task {
|
||||
protected final Library library;
|
||||
protected final String url;
|
||||
protected boolean xz;
|
||||
private final Library originalLibrary;
|
||||
private boolean cached = false;
|
||||
|
||||
public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) {
|
||||
this.originalLibrary = library;
|
||||
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
|
||||
if (library.is("net.minecraftforge", "forge"))
|
||||
@@ -86,7 +89,7 @@ public class LibraryDownloadTask extends Task {
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
Optional<Path> libPath = cacheRepository.getLibrary(library);
|
||||
Optional<Path> libPath = cacheRepository.getLibrary(originalLibrary);
|
||||
if (libPath.isPresent()) {
|
||||
try {
|
||||
FileUtils.copyFile(libPath.get().toFile(), jar);
|
||||
|
||||
@@ -47,7 +47,7 @@ public final class EventManager<T extends Event> {
|
||||
register(consumer, EventPriority.NORMAL);
|
||||
}
|
||||
|
||||
public void register(Consumer<T> consumer, EventPriority priority) {
|
||||
public synchronized void register(Consumer<T> consumer, EventPriority priority) {
|
||||
if (!handlers.get(priority).contains(consumer))
|
||||
handlers.put(priority, consumer);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ public final class EventManager<T extends Event> {
|
||||
register(t -> runnable.run(), priority);
|
||||
}
|
||||
|
||||
public Event.Result fireEvent(T event) {
|
||||
public synchronized Event.Result fireEvent(T event) {
|
||||
for (EventPriority priority : EventPriority.values()) {
|
||||
for (Consumer<T> handler : handlers.get(priority))
|
||||
handler.accept(event);
|
||||
|
||||
@@ -27,10 +27,12 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
@@ -103,6 +105,7 @@ public final class CurseCompletionTask extends Task {
|
||||
|
||||
AtomicBoolean flag = new AtomicBoolean(true);
|
||||
AtomicInteger finished = new AtomicInteger(0);
|
||||
AtomicBoolean notFound = new AtomicBoolean(false);
|
||||
|
||||
// Because in China, Curse is too difficult to visit,
|
||||
// if failed, ignore it and retry next time.
|
||||
@@ -113,6 +116,9 @@ public final class CurseCompletionTask extends Task {
|
||||
if (StringUtils.isBlank(file.getFileName())) {
|
||||
try {
|
||||
return file.withFileName(NetworkUtils.detectFileName(file.getUrl()));
|
||||
} catch (FileNotFoundException e) {
|
||||
notFound.set(true);
|
||||
return file;
|
||||
} catch (IOException ioe) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), ioe);
|
||||
flag.set(false);
|
||||
@@ -132,9 +138,13 @@ public final class CurseCompletionTask extends Task {
|
||||
}
|
||||
|
||||
// Let this task fail if the curse manifest has not been completed.
|
||||
if (!flag.get())
|
||||
// But continue other downloads.
|
||||
if (!flag.get() || notFound.get())
|
||||
dependencies.add(Task.of(() -> {
|
||||
throw new CurseCompletionException();
|
||||
if (notFound.get())
|
||||
throw new CurseCompletionException(new FileNotFoundException());
|
||||
else
|
||||
throw new CurseCompletionException();
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -218,12 +218,18 @@ public class FileDownloadTask extends Task {
|
||||
|
||||
HttpURLConnection con = NetworkUtils.createConnection(url);
|
||||
if (checkETag) repository.injectConnection(con);
|
||||
con.connect();
|
||||
con = NetworkUtils.resolveConnection(con);
|
||||
|
||||
if (con.getResponseCode() == 304) {
|
||||
if (con.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
// Handle cache
|
||||
Path cache = repository.getCachedRemoteFile(con);
|
||||
FileUtils.copyFile(cache.toFile(), file);
|
||||
try {
|
||||
Path cache = repository.getCachedRemoteFile(con);
|
||||
FileUtils.copyFile(cache.toFile(), file);
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to use cached file, redownload it", e);
|
||||
repository.removeRemoteEntry(con);
|
||||
}
|
||||
} else if (con.getResponseCode() / 100 != 2) {
|
||||
throw new IOException("Server error, response code: " + con.getResponseCode());
|
||||
}
|
||||
|
||||
@@ -96,11 +96,17 @@ public final class GetTask extends TaskResult<String> {
|
||||
if (checkETag) repository.injectConnection(conn);
|
||||
conn.connect();
|
||||
|
||||
if (conn.getResponseCode() == 304) {
|
||||
if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
// Handle cache
|
||||
Path cache = repository.getCachedRemoteFile(conn);
|
||||
setResult(FileUtils.readText(cache));
|
||||
return;
|
||||
try {
|
||||
Path cache = repository.getCachedRemoteFile(conn);
|
||||
setResult(FileUtils.readText(cache));
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to use cached file, redownload it", e);
|
||||
repository.removeRemoteEntry(conn);
|
||||
continue;
|
||||
}
|
||||
} else if (conn.getResponseCode() / 100 != 2) {
|
||||
throw new IOException("Server error, response code: " + conn.getResponseCode());
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -45,6 +46,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class CacheRepository {
|
||||
private Path commonDirectory;
|
||||
@@ -69,7 +71,7 @@ public class CacheRepository {
|
||||
} else
|
||||
index = new HashMap<>();
|
||||
} catch (IOException | JsonParseException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to read index file", e);
|
||||
LOG.log(Level.WARNING, "Unable to read index file", e);
|
||||
index = new HashMap<>();
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
@@ -162,6 +164,16 @@ public class CacheRepository {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void removeRemoteEntry(URLConnection conn) {
|
||||
String url = conn.getURL().toString();
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
index.remove(url);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void injectConnection(URLConnection conn) {
|
||||
String url = conn.getURL().toString();
|
||||
lock.readLock().lock();
|
||||
@@ -189,7 +201,7 @@ public class CacheRepository {
|
||||
Lock writeLock = lock.writeLock();
|
||||
writeLock.lock();
|
||||
try {
|
||||
index.put(url, eTagItem);
|
||||
index.compute(eTagItem.url, updateEntity(eTagItem));
|
||||
saveETagIndex();
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
@@ -208,13 +220,31 @@ public class CacheRepository {
|
||||
Lock writeLock = lock.writeLock();
|
||||
writeLock.lock();
|
||||
try {
|
||||
index.put(url, eTagItem);
|
||||
index.compute(eTagItem.url, updateEntity(eTagItem));
|
||||
saveETagIndex();
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private BiFunction<String, ETagItem, ETagItem> updateEntity(ETagItem newItem) {
|
||||
return (key, oldItem) -> {
|
||||
if (oldItem == null) {
|
||||
return newItem;
|
||||
} else if (oldItem.compareTo(newItem) < 0) {
|
||||
Path cached = getFile(SHA1, oldItem.hash);
|
||||
try {
|
||||
Files.deleteIfExists(cached);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Cannot delete old file");
|
||||
}
|
||||
return newItem;
|
||||
} else {
|
||||
return oldItem;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private final Map<String, ETagItem> joinETagIndexes(Collection<ETagItem>... indexes) {
|
||||
Map<String, ETagItem> eTags = new ConcurrentHashMap<>();
|
||||
@@ -223,12 +253,7 @@ public class CacheRepository {
|
||||
.reduce(Stream.empty(), Stream::concat);
|
||||
|
||||
stream.forEach(eTag -> {
|
||||
eTags.compute(eTag.url, (key, oldValue) -> {
|
||||
if (oldValue == null || oldValue.compareTo(eTag) < 0)
|
||||
return eTag;
|
||||
else
|
||||
return oldValue;
|
||||
});
|
||||
eTags.compute(eTag.url, updateEntity(eTag));
|
||||
});
|
||||
|
||||
return eTags;
|
||||
|
||||
@@ -94,6 +94,7 @@ public final class FileUtils {
|
||||
}
|
||||
|
||||
public static void writeBytes(File file, byte[] array) throws IOException {
|
||||
Files.createDirectories(file.toPath().getParent());
|
||||
Files.write(file.toPath(), array);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
@@ -57,13 +59,76 @@ public final class NetworkUtils {
|
||||
|
||||
public static HttpURLConnection createConnection(URL url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.setUseCaches(false);
|
||||
connection.setConnectTimeout(15000);
|
||||
connection.setReadTimeout(15000);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see <a href="https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a>
|
||||
* @param location
|
||||
* @return
|
||||
*/
|
||||
public static String encodeLocation(String location) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean left = true;
|
||||
for (char ch : location.toCharArray()) {
|
||||
switch (ch) {
|
||||
case ' ':
|
||||
if (left) sb.append("%20");
|
||||
else sb.append('+');
|
||||
break;
|
||||
case '?':
|
||||
left = false;
|
||||
default:
|
||||
if (ch >= 0x80)
|
||||
sb.append(encodeURL(Character.toString(ch)));
|
||||
else
|
||||
sb.append(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method aims to solve problem when "Location" in stupid server's response is not encoded.
|
||||
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
||||
* @param conn
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static HttpURLConnection resolveConnection(HttpURLConnection conn) throws IOException {
|
||||
int redirect = 0;
|
||||
while (true) {
|
||||
|
||||
conn.setUseCaches(false);
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(15000);
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
Map<String, List<String>> properties = conn.getRequestProperties();
|
||||
int code = conn.getResponseCode();
|
||||
if (code >= 300 && code <= 307 && code != 306 && code != 304) {
|
||||
String newURL = conn.getHeaderField("Location");
|
||||
conn.disconnect();
|
||||
|
||||
if (redirect > 20) {
|
||||
throw new IOException("Too much redirects");
|
||||
}
|
||||
|
||||
HttpURLConnection redirected = (HttpURLConnection) new URL(conn.getURL(), encodeLocation(newURL)).openConnection();
|
||||
properties.forEach((key, value) -> value.forEach(element -> redirected.addRequestProperty(key, element)));
|
||||
conn = redirected;
|
||||
++redirect;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
public static String doGet(URL url) throws IOException {
|
||||
return IOUtils.readFullyAsString(createConnection(url).getInputStream());
|
||||
}
|
||||
@@ -110,10 +175,12 @@ public final class NetworkUtils {
|
||||
}
|
||||
|
||||
public static String detectFileName(URL url) throws IOException {
|
||||
HttpURLConnection conn = createConnection(url);
|
||||
conn.connect();
|
||||
if (conn.getResponseCode() / 100 != 2)
|
||||
throw new IOException("Response code " + conn.getResponseCode());
|
||||
HttpURLConnection conn = resolveConnection(createConnection(url));
|
||||
int code = conn.getResponseCode();
|
||||
if (code == 404)
|
||||
throw new FileNotFoundException();
|
||||
if (code / 100 != 2)
|
||||
throw new IOException(url + ": response code " + conn.getResponseCode());
|
||||
|
||||
return detectFileName(conn);
|
||||
}
|
||||
|
||||
@@ -332,4 +332,6 @@ public class VersionNumber implements Comparable<VersionNumber> {
|
||||
public int hashCode() {
|
||||
return canonical.hashCode();
|
||||
}
|
||||
|
||||
public static Comparator<String> VERSION_COMPARATOR = Comparator.comparing(VersionNumber::asVersion);
|
||||
}
|
||||
Binary file not shown.
Reference in new issue
Block a user