Compare commits

..
23 Commits
Author SHA1 Message Date
huanghongxun 340ee751e8 Suppress VersionNotFoundException 2018-10-14 19:10:04 +08:00
huanghongxun cf70f72f9c Fix #471 2018-10-14 19:04:27 +08:00
huanghongxun 16ea88a659 Fixed failing to download file when cached entry was removed by user 2018-10-14 19:04:21 +08:00
huanghongxun eaa6790013 Update welcome.png 2018-10-14 18:50:47 +08:00
huanghongxun af571d533d Maintain statement when no account 2018-10-14 12:45:52 +08:00
huanghongxun 004c3c588a Log navigation to catch bugs 2018-10-14 12:39:22 +08:00
huanghongxun 2a44167c22 Suppress NPE 2018-10-14 12:39:03 +08:00
huanghongxun cf6b735026 Removed JFXDrawer and JFXHamburger in JFoenix.jar 2018-10-14 12:20:03 +08:00
huanghongxun 5918b04b48 Prevent from calling register and fireEvent at the same time 2018-10-13 11:17:16 +08:00
huanghongxun 8004cb3a64 Change default red to darker one 2018-10-11 22:26:50 +08:00
huanghongxun 9981423927 Change separator fill in title bar and launch button to reduce color difference 2018-10-11 22:20:41 +08:00
huanghongxun 0e5250dfb3 Satisfy requirements metioned in #469 2018-10-11 22:06:34 +08:00
huanghongxun 2dc956b357 Fix request mojang server for skin info every time 2018-10-11 22:05:24 +08:00
huanghongxun b8bd0a08e0 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-11 14:05:35 +08:00
huanghongxun fb31eebcf2 Add a warning that forge 2760 is not compatible with liteloader 2018-10-11 14:04:52 +08:00
huanghongxun 3c8689a3f1 Close launching progress dialog when game crashed before opening window 2018-10-11 13:51:37 +08:00
huanghongxun 2957d66dd6 Add test game button in versions page 2018-10-11 13:41:11 +08:00
huanghongxun 6f6efe0874 Rename DecoratorControl to Decorator 2018-10-11 13:32:39 +08:00
huanghongxun 355fc04069 Drop modpack file to install 2018-10-11 13:32:11 +08:00
huanghongxun b4738f383b Fix not on FX thread 2018-10-11 12:59:06 +08:00
huanghongxun b12711969c Rewrite encode location 2018-10-10 16:37:04 +08:00
huanghongxun 94727353c4 Fix response code 400 when downloading mods from curse 2018-10-10 15:27:27 +08:00
huanghongxun 5eb7449b74 Delete old files if etag was updated 2018-10-10 01:36:31 +08:00
42 changed files with 735 additions and 353 deletions

No files matched your search

@@ -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,49 @@
*/
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.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 +135,60 @@ 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::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 +197,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;
}
}
@@ -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");
@@ -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);
}
}
@@ -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,7 @@ public class GameAdvancedListItem extends AdvancedListItem {
public GameAdvancedListItem() {
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null) {
if (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;
@@ -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 *
@@ -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>
@@ -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

@@ -55,7 +55,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
@@ -140,6 +140,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 +165,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 +218,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.
@@ -55,7 +55,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=使用者名稱
@@ -140,6 +140,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 +165,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 +218,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 整合包受支援。
@@ -55,7 +55,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=用户名
@@ -140,6 +140,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 +165,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 +218,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 整合包受支持。
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,7 +94,8 @@ public final class FileUtils {
}
public static void writeBytes(File file, byte[] array) throws IOException {
Files.write(file.toPath(), array);
Files.createDirectories(file.toPath().getParent());
Files.write(file.toPath(), array, StandardOpenOption.CREATE);
}
public static void deleteDirectory(File directory)
@@ -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);
}
BIN
View File
Binary file not shown.