Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31ed4268b6 | ||
|
|
c698eaac6f | ||
|
|
d0dd23fa14 | ||
|
|
c6f1aa21c5 | ||
|
|
a3dc7798e7 | ||
|
|
88f6fd1965 | ||
|
|
9062c2e447 | ||
|
|
e3d2a4bf9d | ||
|
|
c2d609cdb4 | ||
|
|
b3721e29c6 | ||
|
|
eab4463c2d | ||
|
|
394873864f | ||
|
|
758216245f | ||
|
|
6edfd112a2 | ||
|
|
625215b068 | ||
|
|
4759e4957c | ||
|
|
b62f9494ae | ||
|
|
12b47e7d51 | ||
|
|
9ced29aa93 | ||
|
|
5095640ba2 | ||
|
|
c889d7cb3f | ||
|
|
84636f3f50 | ||
|
|
9a6f07d7c9 | ||
|
|
e8f5088049 | ||
|
|
0bee306d67 | ||
|
|
8d75b2d413 | ||
|
|
426ea607e8 | ||
|
|
507cade50d | ||
|
|
f0c7f54cc6 | ||
|
|
62c38482f6 | ||
|
|
0ff6ca18ed | ||
|
|
bbe6c388a3 | ||
|
|
639bc58e84 | ||
|
|
c0bcafd747 | ||
|
|
bcaa019551 | ||
|
|
38725abc48 | ||
|
|
68a02966d0 |
No files matched your search
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@@ -80,13 +80,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshVersions() {
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this));
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
}
|
||||
|
||||
public void changeDirectory(File newDirectory) {
|
||||
setBaseDirectory(newDirectory);
|
||||
refreshVersionsAsync().start();
|
||||
@@ -168,6 +161,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return new Image("file:" + iconFile.getAbsolutePath());
|
||||
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
|
||||
return new Image("/assets/img/furnace.png");
|
||||
else if ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
|
||||
return new Image("/assets/img/furnace.png");
|
||||
else
|
||||
return new Image("/assets/img/grass.png");
|
||||
}
|
||||
|
||||
@@ -180,12 +180,9 @@ public final class TexturesLoader {
|
||||
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||
8 * scale, 8 * scale, 16 * scale, 16 * scale,
|
||||
null);
|
||||
|
||||
if (skin.getWidth() == skin.getHeight()) {
|
||||
g.drawImage(skin,
|
||||
0, 0, size, size,
|
||||
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
||||
}
|
||||
g.drawImage(skin,
|
||||
0, 0, size, size,
|
||||
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
||||
|
||||
g.dispose();
|
||||
return avatar;
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
@@ -19,12 +19,13 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -36,6 +37,8 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||
Pane placeholder = new Pane();
|
||||
|
||||
StackPane contentPane = new StackPane();
|
||||
{
|
||||
@@ -48,9 +51,12 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
list.setSpacing(10);
|
||||
list.setPadding(new Insets(10));
|
||||
|
||||
VBox content = new VBox();
|
||||
content.getChildren().setAll(list, placeholder);
|
||||
|
||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(list);
|
||||
scrollPane.setContent(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
@@ -64,7 +70,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
JFXButton btnAdd = new JFXButton();
|
||||
FXUtils.setLimitWidth(btnAdd, 40);
|
||||
FXUtils.setLimitHeight(btnAdd, 40);
|
||||
btnAdd.getStyleClass().setAll("jfx-button-raised-round");
|
||||
btnAdd.getStyleClass().add("jfx-button-raised-round");
|
||||
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1));
|
||||
btnAdd.setOnMouseClicked(e -> skinnable.add());
|
||||
@@ -72,7 +78,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
FXUtils.setLimitWidth(btnRefresh, 40);
|
||||
FXUtils.setLimitHeight(btnRefresh, 40);
|
||||
btnRefresh.getStyleClass().setAll("jfx-button-raised-round");
|
||||
btnRefresh.getStyleClass().add("jfx-button-raised-round");
|
||||
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnRefresh.setGraphic(SVG.refresh(Theme.whiteFillBinding(), -1, -1));
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
@@ -86,7 +92,13 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
});
|
||||
}
|
||||
|
||||
contentPane.getChildren().setAll(scrollPane, vBox);
|
||||
// Keep a blank space to prevent buttons from blocking up mod items.
|
||||
BorderPane group = new BorderPane();
|
||||
group.setPickOnBounds(false);
|
||||
group.setBottom(vBox);
|
||||
placeholder.minHeightProperty().bind(vBox.heightProperty());
|
||||
|
||||
contentPane.getChildren().setAll(scrollPane, group);
|
||||
}
|
||||
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
@@ -162,4 +162,8 @@ public final class SVG {
|
||||
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
|
||||
}
|
||||
}
|
||||
@@ -247,4 +247,9 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
|
||||
protected void onHelp() {
|
||||
FXUtils.openLink(Metadata.HELP_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSponsor() {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.*;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
@@ -75,6 +76,19 @@ public abstract class SettingsView extends StackPane {
|
||||
{
|
||||
ComponentList settingsPane = new ComponentList();
|
||||
{
|
||||
{
|
||||
StackPane sponsorPane = new StackPane();
|
||||
sponsorPane.setCursor(Cursor.HAND);
|
||||
sponsorPane.setOnMouseClicked(e -> onSponsor());
|
||||
{
|
||||
Label label = new Label(i18n("sponsor.hmcl"));
|
||||
label.setWrapText(true);
|
||||
label.prefWidthProperty().bind(sponsorPane.widthProperty());
|
||||
sponsorPane.getChildren().add(label);
|
||||
}
|
||||
settingsPane.getContent().add(sponsorPane);
|
||||
}
|
||||
|
||||
ComponentSublist updatePane = new ComponentSublist();
|
||||
updatePane.setTitle(i18n("update"));
|
||||
updatePane.setHasSubtitle(true);
|
||||
@@ -83,7 +97,7 @@ public abstract class SettingsView extends StackPane {
|
||||
|
||||
lblUpdate = new Label(i18n("update"));
|
||||
lblUpdateSub = new Label();
|
||||
lblUpdateSub.getStyleClass().setAll("subtitle-label");
|
||||
lblUpdateSub.getStyleClass().add("subtitle-label");
|
||||
|
||||
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
|
||||
updatePane.setHeaderLeft(headerLeft);
|
||||
@@ -92,7 +106,7 @@ public abstract class SettingsView extends StackPane {
|
||||
{
|
||||
btnUpdate = new JFXButton();
|
||||
btnUpdate.setOnMouseClicked(e -> onUpdate());
|
||||
btnUpdate.getStyleClass().setAll("toggle-icon4");
|
||||
btnUpdate.getStyleClass().add("toggle-icon4");
|
||||
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 20, 20));
|
||||
|
||||
updatePane.setHeaderRight(btnUpdate);
|
||||
@@ -124,7 +138,7 @@ public abstract class SettingsView extends StackPane {
|
||||
|
||||
Label help = new Label(i18n("help"));
|
||||
Label helpSubtitle = new Label(i18n("help.detail"));
|
||||
helpSubtitle.getStyleClass().setAll("subtitle-label");
|
||||
helpSubtitle.getStyleClass().add("subtitle-label");
|
||||
|
||||
headerLeft.getChildren().setAll(help, helpSubtitle);
|
||||
updatePane.setLeft(headerLeft);
|
||||
@@ -133,7 +147,7 @@ public abstract class SettingsView extends StackPane {
|
||||
{
|
||||
JFXButton btnExternal = new JFXButton();
|
||||
btnExternal.setOnMouseClicked(e -> onHelp());
|
||||
btnExternal.getStyleClass().setAll("toggle-icon4");
|
||||
btnExternal.getStyleClass().add("toggle-icon4");
|
||||
btnExternal.setGraphic(SVG.openInNew(Theme.blackFillBinding(), -1, -1));
|
||||
|
||||
updatePane.setRight(btnExternal);
|
||||
@@ -337,7 +351,7 @@ public abstract class SettingsView extends StackPane {
|
||||
{
|
||||
JFXButton logButton = new JFXButton(i18n("settings.launcher.launcher_log.export"));
|
||||
logButton.setOnMouseClicked(e -> onExportLogs());
|
||||
logButton.getStyleClass().setAll("jfx-button-border");
|
||||
logButton.getStyleClass().add("jfx-button-border");
|
||||
|
||||
logPane.setHeaderRight(logButton);
|
||||
}
|
||||
@@ -491,4 +505,5 @@ public abstract class SettingsView extends StackPane {
|
||||
protected abstract void onUpdate();
|
||||
protected abstract void onHelp();
|
||||
protected abstract void onExportLogs();
|
||||
protected abstract void onSponsor();
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
@@ -103,7 +103,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
right.getChildren().add(btnRemove);
|
||||
root.setRight(right);
|
||||
|
||||
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
|
||||
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
|
||||
JFXDepthManager.setDepth(root, 1);
|
||||
|
||||
getChildren().setAll(root);
|
||||
|
||||
@@ -31,6 +31,9 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class AccountLoginPane extends StackPane {
|
||||
private final Account oldAccount;
|
||||
@@ -64,6 +67,7 @@ public class AccountLoginPane extends StackPane {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
progressBar.setVisible(false);
|
||||
}, e -> {
|
||||
LOG.log(Level.INFO, "Failed to login with password: " + oldAccount, e);
|
||||
if (e instanceof NoSelectedCharacterException) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import com.jfoenix.controls.*;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
||||
@@ -83,7 +83,7 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER);
|
||||
right.setMouseTransparent(true);
|
||||
right.getStyleClass().setAll("toggle-icon4");
|
||||
right.getStyleClass().add("toggle-icon4");
|
||||
FXUtils.setLimitWidth(right, 40);
|
||||
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
|
||||
newGraphic -> {
|
||||
@@ -99,7 +99,7 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
|
||||
visible -> root.setRight(visible ? right : null));
|
||||
|
||||
stackPane.setStyle("-fx-padding: 10 16 10 16;");
|
||||
stackPane.getStyleClass().setAll("transparent");
|
||||
stackPane.getStyleClass().add("transparent");
|
||||
stackPane.setPickOnBounds(false);
|
||||
stackPane.getChildren().setAll(root);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ public class IconedItem extends RipplerContainer {
|
||||
hBox.getChildren().add(icon);
|
||||
}
|
||||
|
||||
hBox.getStyleClass().setAll("iconed-item-container");
|
||||
hBox.getStyleClass().add("iconed-item-container");
|
||||
Label textLabel = new Label();
|
||||
textLabel.setId("label");
|
||||
textLabel.setMouseTransparent(true);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
|
||||
|
||||
private final CheckBox checkBox;
|
||||
private boolean showLabel;
|
||||
private ObservableValue<Boolean> booleanProperty;
|
||||
|
||||
public JFXCheckBoxTreeTableCell() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
public JFXCheckBoxTreeTableCell(
|
||||
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
|
||||
this(getSelectedProperty, null);
|
||||
}
|
||||
|
||||
public JFXCheckBoxTreeTableCell(
|
||||
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty,
|
||||
final StringConverter<T> converter) {
|
||||
this.getStyleClass().add("check-box-tree-table-cell");
|
||||
this.checkBox = new JFXCheckBox();
|
||||
setGraphic(null);
|
||||
setSelectedStateCallback(getSelectedProperty);
|
||||
setConverter(converter);
|
||||
}
|
||||
|
||||
private ObjectProperty<StringConverter<T>> converter =
|
||||
new SimpleObjectProperty<StringConverter<T>>(this, "converter") {
|
||||
protected void invalidated() {
|
||||
updateShowLabel();
|
||||
}
|
||||
};
|
||||
|
||||
public final ObjectProperty<StringConverter<T>> converterProperty() {
|
||||
return converter;
|
||||
}
|
||||
public final void setConverter(StringConverter<T> value) {
|
||||
converterProperty().set(value);
|
||||
}
|
||||
public final StringConverter<T> getConverter() {
|
||||
return converterProperty().get();
|
||||
}
|
||||
|
||||
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
|
||||
selectedStateCallback =
|
||||
new SimpleObjectProperty<Callback<Integer, ObservableValue<Boolean>>>(
|
||||
this, "selectedStateCallback");
|
||||
|
||||
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
|
||||
return selectedStateCallback;
|
||||
}
|
||||
|
||||
public final void setSelectedStateCallback(Callback<Integer, ObservableValue<Boolean>> value) {
|
||||
selectedStateCallbackProperty().set(value);
|
||||
}
|
||||
|
||||
public final Callback<Integer, ObservableValue<Boolean>> getSelectedStateCallback() {
|
||||
return selectedStateCallbackProperty().get();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
StringConverter<T> c = getConverter();
|
||||
|
||||
if (showLabel) {
|
||||
setText(c.toString(item));
|
||||
}
|
||||
setGraphic(checkBox);
|
||||
|
||||
if (booleanProperty instanceof BooleanProperty) {
|
||||
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
|
||||
}
|
||||
ObservableValue<?> obsValue = getSelectedProperty();
|
||||
if (obsValue instanceof BooleanProperty) {
|
||||
booleanProperty = (ObservableValue<Boolean>) obsValue;
|
||||
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
|
||||
}
|
||||
|
||||
checkBox.disableProperty().bind(Bindings.not(
|
||||
getTreeTableView().editableProperty().and(
|
||||
getTableColumn().editableProperty()).and(
|
||||
editableProperty())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShowLabel() {
|
||||
this.showLabel = converter != null;
|
||||
this.checkBox.setAlignment(showLabel ? Pos.CENTER_LEFT : Pos.CENTER);
|
||||
}
|
||||
|
||||
private ObservableValue<?> getSelectedProperty() {
|
||||
return getSelectedStateCallback() != null ?
|
||||
getSelectedStateCallback().call(getIndex()) :
|
||||
getTableColumn().getCellObservableValue(getIndex());
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ public class PopupMenu extends Control {
|
||||
public static Node wrapPopupMenuItem(Node node) {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getChildren().setAll(node);
|
||||
pane.getStyleClass().setAll("menu-container");
|
||||
pane.getStyleClass().add("menu-container");
|
||||
node.setMouseTransparent(true);
|
||||
return new RipplerContainer(pane);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.NamedArg;
|
||||
@@ -64,68 +63,28 @@ public class RipplerContainer extends StackPane {
|
||||
mask.resize(buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset());
|
||||
return mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
this.ripplerPane.setOnMousePressed(event -> {
|
||||
if (releaseManualRippler != null)
|
||||
releaseManualRippler.run();
|
||||
releaseManualRippler = null;
|
||||
createRipple(event.getX(), event.getY());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private Transition clickedAnimation;
|
||||
private final CornerRadii defaultRadii = new CornerRadii(3);
|
||||
private Runnable releaseManualRippler;
|
||||
|
||||
public RipplerContainer(@NamedArg("container") Node container) {
|
||||
setContainer(container);
|
||||
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
buttonContainer.getChildren().add(buttonRippler);
|
||||
setOnMousePressed(event -> {
|
||||
if (clickedAnimation != null) {
|
||||
clickedAnimation.setRate(1);
|
||||
clickedAnimation.play();
|
||||
}
|
||||
});
|
||||
setOnMouseReleased(event -> {
|
||||
if (clickedAnimation != null) {
|
||||
clickedAnimation.setRate(-1);
|
||||
clickedAnimation.play();
|
||||
}
|
||||
});
|
||||
focusedProperty().addListener((a, b, newValue) -> {
|
||||
if (newValue) {
|
||||
if (!isPressed())
|
||||
buttonRippler.showOverlay();
|
||||
buttonRippler.setOverlayVisible(true);
|
||||
} else {
|
||||
buttonRippler.hideOverlay();
|
||||
buttonRippler.setOverlayVisible(false);
|
||||
}
|
||||
});
|
||||
pressedProperty().addListener(o -> buttonRippler.hideOverlay());
|
||||
pressedProperty().addListener(o -> buttonRippler.setOverlayVisible(false));
|
||||
setPickOnBounds(false);
|
||||
|
||||
buttonContainer.setPickOnBounds(false);
|
||||
buttonContainer.shapeProperty().bind(shapeProperty());
|
||||
buttonContainer.borderProperty().bind(borderProperty());
|
||||
buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (getBackground() == null || isJavaDefaultBackground(getBackground()) || isJavaDefaultClickedBackground(getBackground()))
|
||||
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
|
||||
|
||||
try {
|
||||
return new Background(new BackgroundFill(getBackground() != null ? getBackground().getFills().get(0).getFill() : Color.TRANSPARENT,
|
||||
getBackground() != null ? getBackground().getFills().get(0).getRadii() : defaultRadii, Insets.EMPTY));
|
||||
} catch (Exception e) {
|
||||
return getBackground();
|
||||
}
|
||||
}, backgroundProperty()));
|
||||
|
||||
ripplerFillProperty().addListener((a, b, newValue) -> buttonRippler.setRipplerFill(newValue));
|
||||
if (getBackground() == null || isJavaDefaultBackground(getBackground()))
|
||||
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null)));
|
||||
buttonRippler.ripplerFillProperty().bind(ripplerFillProperty());
|
||||
|
||||
containerProperty().addListener(o -> updateChildren());
|
||||
updateChildren();
|
||||
@@ -153,24 +112,6 @@ public class RipplerContainer extends StackPane {
|
||||
getChildren().get(i).setPickOnBounds(false);
|
||||
}
|
||||
|
||||
private boolean isJavaDefaultBackground(Background background) {
|
||||
try {
|
||||
String firstFill = background.getFills().get(0).getFill().toString();
|
||||
return "0xffffffba".equals(firstFill) || "0xffffffbf".equals(firstFill) || "0xffffffbd".equals(firstFill);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isJavaDefaultClickedBackground(Background background) {
|
||||
try {
|
||||
String firstFill = background.getFills().get(0).getFill().toString();
|
||||
return "0x039ed3ff".equals(firstFill);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Node getContainer() {
|
||||
return container.get();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.application.Platform;
|
||||
|
||||
@@ -17,15 +17,11 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
|
||||
@@ -45,16 +41,15 @@ import java.util.Map;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class TaskListPane extends StackPane {
|
||||
private final JFXListView<Task> listBox = new JFXListView<>();
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final Map<Task, ProgressListNode> nodes = new HashMap<>();
|
||||
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
|
||||
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
|
||||
|
||||
public TaskListPane() {
|
||||
getChildren().setAll(listBox);
|
||||
listBox.setSpacing(0);
|
||||
|
||||
listBox.setPadding(Insets.EMPTY);
|
||||
listBox.setCellFactory(listView -> new ProgressListNode());
|
||||
getChildren().setAll(listBox);
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty finishedTasksProperty() {
|
||||
@@ -70,7 +65,7 @@ public final class TaskListPane extends StackPane {
|
||||
@Override
|
||||
public void onStart() {
|
||||
Platform.runLater(() -> {
|
||||
listBox.getItems().clear();
|
||||
listBox.clear();
|
||||
finishedTasks.set(0);
|
||||
totTasks.set(0);
|
||||
});
|
||||
@@ -112,66 +107,64 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
}
|
||||
|
||||
Platform.runLater(() -> listBox.getItems().add(task));
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
Platform.runLater(() -> listBox.add(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Task task) {
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
node.unbind();
|
||||
Platform.runLater(() -> {
|
||||
if (listBox.getItems().remove(task))
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
listBox.remove(node);
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailed(Task task, Throwable throwable) {
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
Platform.runLater(() -> {
|
||||
node.setThrowable(throwable);
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class ProgressListNode extends ListCell<Task> {
|
||||
private final BorderPane borderPane = new BorderPane();
|
||||
private static class ProgressListNode extends BorderPane {
|
||||
private final JFXProgressBar bar = new JFXProgressBar();
|
||||
private final Label title = new Label();
|
||||
private final Label state = new Label();
|
||||
|
||||
{
|
||||
borderPane.setLeft(title);
|
||||
borderPane.setRight(state);
|
||||
borderPane.setBottom(bar);
|
||||
borderPane.setMinWidth(0);
|
||||
borderPane.setPrefWidth(1);
|
||||
public ProgressListNode(Task task) {
|
||||
bar.progressProperty().bind(task.progressProperty());
|
||||
title.setText(task.getName());
|
||||
state.textProperty().bind(task.messageProperty());
|
||||
|
||||
setPadding(Insets.EMPTY);
|
||||
setLeft(title);
|
||||
setRight(state);
|
||||
setBottom(bar);
|
||||
|
||||
bar.minWidthProperty().bind(widthProperty());
|
||||
bar.prefWidthProperty().bind(widthProperty());
|
||||
bar.maxWidthProperty().bind(widthProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Task item, boolean empty) {
|
||||
boolean wasEmpty = isEmpty();
|
||||
Task oldTask = getItem();
|
||||
public void unbind() {
|
||||
bar.progressProperty().unbind();
|
||||
state.textProperty().unbind();
|
||||
}
|
||||
|
||||
if (!wasEmpty && oldTask != null) {
|
||||
bar.progressProperty().unbind();
|
||||
state.textProperty().unbind();
|
||||
}
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(borderPane);
|
||||
bar.visibleProperty().bind(Bindings.createBooleanBinding(() -> item.progressProperty().get() != -1, item.progressProperty()));
|
||||
bar.progressProperty().bind(item.progressProperty());
|
||||
state.textProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (item.getState() == Task.TaskState.FAILED) {
|
||||
return item.getLastException().getLocalizedMessage();
|
||||
} else {
|
||||
return item.messageProperty().get();
|
||||
}
|
||||
}, item.messageProperty(), item.stateProperty()));
|
||||
title.setText(item.getName());
|
||||
}
|
||||
public void setThrowable(Throwable throwable) {
|
||||
unbind();
|
||||
state.setText(throwable.getLocalizedMessage());
|
||||
bar.setProgress(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
Decorator skinnable = getSkinnable();
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.getStyleClass().setAll("jfx-decorator", "resize-border");
|
||||
root.getStyleClass().addAll("jfx-decorator", "resize-border");
|
||||
root.setPrefHeight(519);
|
||||
root.setPrefWidth(800);
|
||||
root.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
@@ -83,7 +83,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
StackPane drawerWrapper = new StackPane();
|
||||
skinnable.setDrawerWrapper(drawerWrapper);
|
||||
drawerWrapper.getStyleClass().setAll("jfx-decorator-drawer");
|
||||
drawerWrapper.getStyleClass().add("jfx-decorator-drawer");
|
||||
drawerWrapper.backgroundProperty().bind(skinnable.backgroundProperty());
|
||||
FXUtils.setOverflowHidden(drawerWrapper, true);
|
||||
{
|
||||
@@ -92,10 +92,10 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
{
|
||||
BorderPane leftRootPane = new BorderPane();
|
||||
FXUtils.setLimitWidth(leftRootPane, 200);
|
||||
leftRootPane.getStyleClass().setAll("jfx-decorator-content-container");
|
||||
leftRootPane.getStyleClass().add("jfx-decorator-content-container");
|
||||
|
||||
StackPane drawerContainer = new StackPane();
|
||||
drawerContainer.getStyleClass().setAll("gray-background");
|
||||
drawerContainer.getStyleClass().add("gray-background");
|
||||
Bindings.bindContent(drawerContainer.getChildren(), skinnable.drawerProperty());
|
||||
leftRootPane.setCenter(drawerContainer);
|
||||
|
||||
@@ -111,7 +111,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
{
|
||||
contentPlaceHolder = new StackPane();
|
||||
contentPlaceHolder.getStyleClass().setAll("jfx-decorator-content-container");
|
||||
contentPlaceHolder.getStyleClass().add("jfx-decorator-content-container");
|
||||
contentPlaceHolder.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
FXUtils.setOverflowHidden(contentPlaceHolder, true);
|
||||
Bindings.bindContent(contentPlaceHolder.getChildren(), skinnable.contentProperty());
|
||||
@@ -189,14 +189,14 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
{
|
||||
JFXButton backNavButton = new JFXButton();
|
||||
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
|
||||
backNavButton.getStyleClass().setAll("jfx-decorator-button");
|
||||
backNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
|
||||
backNavButton.visibleProperty().bind(skinnable.canBackProperty());
|
||||
|
||||
closeNavButton = new JFXButton();
|
||||
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
|
||||
closeNavButton.getStyleClass().setAll("jfx-decorator-button");
|
||||
closeNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
|
||||
|
||||
@@ -219,7 +219,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
VBox navCenter = new VBox();
|
||||
navCenter.setAlignment(Pos.CENTER_LEFT);
|
||||
Label titleLabel = new Label();
|
||||
titleLabel.getStyleClass().setAll("jfx-decorator-title");
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
titleLabel.textProperty().bind(skinnable.drawerTitleProperty());
|
||||
navCenter.getChildren().setAll(titleLabel);
|
||||
navBar.setCenter(navCenter);
|
||||
@@ -228,7 +228,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
navRight.setAlignment(Pos.CENTER_RIGHT);
|
||||
refreshNavButton = new JFXButton();
|
||||
refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), -1, -1));
|
||||
refreshNavButton.getStyleClass().setAll("jfx-decorator-button");
|
||||
refreshNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
refreshNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
|
||||
refreshNavButton.visibleProperty().bind(skinnable.canRefreshProperty());
|
||||
@@ -251,12 +251,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
StackPane pane = new StackPane(minus);
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
btnMin.setGraphic(pane);
|
||||
btnMin.getStyleClass().setAll("jfx-decorator-button");
|
||||
btnMin.getStyleClass().add("jfx-decorator-button");
|
||||
btnMin.setOnAction(e -> skinnable.minimize());
|
||||
|
||||
JFXButton btnClose = new JFXButton();
|
||||
btnClose.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
|
||||
btnClose.getStyleClass().setAll("jfx-decorator-button");
|
||||
btnClose.getStyleClass().add("jfx-decorator-button");
|
||||
btnClose.setOnAction(e -> skinnable.close());
|
||||
|
||||
buttonsContainer.getChildren().setAll(separator, btnMin, btnClose);
|
||||
|
||||
@@ -52,7 +52,7 @@ public class DecoratorWizardDisplayer extends StackPane implements TaskExecutorD
|
||||
wizardController.setProvider(provider);
|
||||
wizardController.onStart();
|
||||
|
||||
getStyleClass().setAll("white-background");
|
||||
getStyleClass().add("white-background");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,12 +21,17 @@ import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -82,7 +87,7 @@ public final class InstallerWizardProvider implements WizardProvider {
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_message", i18n("install.failed"));
|
||||
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> alertFailureMessage(exception, next));
|
||||
|
||||
Task ret = Task.ofResult("version", () -> version);
|
||||
|
||||
@@ -114,4 +119,13 @@ public final class InstallerWizardProvider implements WizardProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void alertFailureMessage(Exception exception, Runnable next) {
|
||||
if (exception instanceof LibraryDownloadException) {
|
||||
Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||
} else if (exception instanceof DownloadException) {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||
} else {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,12 @@ import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -111,10 +109,8 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
} else {
|
||||
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageBox.INFORMATION_MESSAGE, next);
|
||||
}
|
||||
} else if (exception instanceof DownloadException) {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||
} else {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
|
||||
InstallerWizardProvider.alertFailureMessage(exception, next);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+1
-14
@@ -22,14 +22,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -70,16 +66,7 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", new FailureCallback() {
|
||||
@Override
|
||||
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
|
||||
if (exception instanceof DownloadException) {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||
} else {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
|
||||
}
|
||||
}
|
||||
});
|
||||
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> InstallerWizardProvider.alertFailureMessage(exception, next));
|
||||
|
||||
return finishVersionDownloadingAsync(settings);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -43,7 +44,7 @@ import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class VersionsPage extends StackPane implements WizardPage, Refreshable {
|
||||
public final class VersionsPage extends BorderPane implements WizardPage, Refreshable {
|
||||
private final String gameVersion;
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final String libraryId;
|
||||
@@ -59,6 +60,8 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
|
||||
@FXML
|
||||
private StackPane emptyPane;
|
||||
@FXML
|
||||
private StackPane root;
|
||||
@FXML
|
||||
private JFXCheckBox chkRelease;
|
||||
@FXML
|
||||
private JFXCheckBox chkSnapshot;
|
||||
@@ -69,7 +72,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
|
||||
@FXML
|
||||
private VBox centrePane;
|
||||
|
||||
private final TransitionHandler transitionHandler = new TransitionHandler(this);
|
||||
private final TransitionHandler transitionHandler;
|
||||
private final VersionList<?> versionList;
|
||||
private TaskExecutor executor;
|
||||
|
||||
@@ -83,6 +86,8 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
|
||||
|
||||
transitionHandler = new TransitionHandler(root);
|
||||
|
||||
if (versionList.hasType()) {
|
||||
centrePane.getChildren().setAll(checkPane, list);
|
||||
} else
|
||||
@@ -122,8 +127,8 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
getChildren().setAll(spinner);
|
||||
executor = versionList.refreshAsync(downloadProvider).finalized((variables, isDependentsSucceeded) -> {
|
||||
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
|
||||
executor = versionList.refreshAsync(gameVersion, downloadProvider).finalized((variables, isDependentsSucceeded) -> {
|
||||
if (isDependentsSucceeded) {
|
||||
List<VersionsPageItem> items = loadVersions();
|
||||
|
||||
@@ -169,4 +174,9 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
|
||||
|
||||
@FXML
|
||||
private void onBack() { controller.onPrev(true); }
|
||||
|
||||
@FXML
|
||||
private void onSponsor() {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/bmclapi_sponsor");
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
@@ -87,7 +87,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||
right.getChildren().add(btnManage);
|
||||
root.setRight(right);
|
||||
|
||||
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
|
||||
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
|
||||
JFXDepthManager.setDepth(root, 1);
|
||||
|
||||
getChildren().setAll(root);
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -31,8 +30,8 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
public class GameListSkin extends SkinBase<GameList> {
|
||||
@@ -51,7 +50,7 @@ public class GameListSkin extends SkinBase<GameList> {
|
||||
|
||||
{
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.getStyleClass().setAll("jfx-tool-bar-second");
|
||||
toolbar.getStyleClass().add("jfx-tool-bar-second");
|
||||
JFXDepthManager.setDepth(toolbar, 1);
|
||||
toolbar.setPickOnBounds(false);
|
||||
|
||||
@@ -91,10 +90,9 @@ public class GameListSkin extends SkinBase<GameList> {
|
||||
}
|
||||
|
||||
{
|
||||
StackPane center = new StackPane();
|
||||
|
||||
JFXSpinner spinner = new JFXSpinner();
|
||||
spinner.getStyleClass().setAll("first-spinner");
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToWidth(true);
|
||||
@@ -109,9 +107,7 @@ public class GameListSkin extends SkinBase<GameList> {
|
||||
scrollPane.setContent(gameList);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.loadingProperty(),
|
||||
loading -> center.getChildren().setAll(loading ? spinner : scrollPane));
|
||||
|
||||
center.setContent(scrollPane);
|
||||
root.setCenter(center);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.mod.ModInfo;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModItem extends BorderPane {
|
||||
|
||||
public ModItem(ModInfo info, Consumer<ModItem> deleteCallback) {
|
||||
JFXCheckBox chkEnabled = new JFXCheckBox();
|
||||
BorderPane.setAlignment(chkEnabled, Pos.CENTER);
|
||||
setLeft(chkEnabled);
|
||||
|
||||
TwoLineListItem modItem = new TwoLineListItem();
|
||||
BorderPane.setAlignment(modItem, Pos.CENTER);
|
||||
setCenter(modItem);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
JFXUtilities.runInFX(() -> {
|
||||
FXUtils.installFastTooltip(btnRemove, i18n("mods.remove"));
|
||||
});
|
||||
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
|
||||
setRight(btnRemove);
|
||||
|
||||
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
|
||||
JFXDepthManager.setDepth(this, 1);
|
||||
modItem.setTitle(info.getFileName());
|
||||
StringBuilder message = new StringBuilder(info.getName());
|
||||
if (StringUtils.isNotBlank(info.getVersion()))
|
||||
message.append(", ").append(i18n("archive.version")).append(": ").append(info.getVersion());
|
||||
if (StringUtils.isNotBlank(info.getGameVersion()))
|
||||
message.append(", ").append(i18n("archive.game_version")).append(": ").append(info.getGameVersion());
|
||||
if (StringUtils.isNotBlank(info.getAuthors()))
|
||||
message.append(", ").append(i18n("archive.author")).append(": ").append(info.getAuthors());
|
||||
modItem.setSubtitle(message.toString());
|
||||
chkEnabled.setSelected(info.isActive());
|
||||
chkEnabled.selectedProperty().addListener((a, b, newValue) ->
|
||||
info.activeProperty().set(newValue));
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXTabPane;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.mod.ModInfo;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
@@ -37,16 +46,18 @@ import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModListPage extends ListPage<ModItem> {
|
||||
public final class ModListPage extends Control {
|
||||
private final ListProperty<ModListPageSkin.ModInfoObject> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
|
||||
|
||||
private JFXTabPane parentTab;
|
||||
private ModManager modManager;
|
||||
|
||||
public ModListPage() {
|
||||
setRefreshable(true);
|
||||
|
||||
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
|
||||
mods.forEach(it -> {
|
||||
@@ -61,6 +72,10 @@ public final class ModListPage extends ListPage<ModItem> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new ModListPageSkin(this);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
loadMods(modManager);
|
||||
}
|
||||
@@ -71,50 +86,22 @@ public final class ModListPage extends ListPage<ModItem> {
|
||||
|
||||
public void loadMods(ModManager modManager) {
|
||||
this.modManager = modManager;
|
||||
Task.of(variables -> {
|
||||
Task.ofResult("list", variables -> {
|
||||
synchronized (ModListPage.this) {
|
||||
JFXUtilities.runInFX(() -> loadingProperty().set(true));
|
||||
|
||||
modManager.refreshMods();
|
||||
|
||||
// Surprisingly, if there are a great number of mods, this processing will cause a long UI pause,
|
||||
// constructing UI elements.
|
||||
// We must do this asynchronously.
|
||||
LinkedList<ModItem> list = new LinkedList<>();
|
||||
for (ModInfo modInfo : modManager.getMods()) {
|
||||
ModItem item = new ModItem(modInfo, i -> {
|
||||
try {
|
||||
modManager.removeMods(modInfo);
|
||||
} catch (IOException ignore) {
|
||||
// Fail to remove mods if the game is running or the mod is absent.
|
||||
}
|
||||
loadMods(modManager);
|
||||
});
|
||||
modInfo.activeProperty().addListener((a, b, newValue) -> {
|
||||
if (newValue)
|
||||
item.getStyleClass().remove("disabled");
|
||||
else
|
||||
item.getStyleClass().add("disabled");
|
||||
});
|
||||
if (!modInfo.isActive())
|
||||
item.getStyleClass().add("disabled");
|
||||
|
||||
list.add(item);
|
||||
}
|
||||
|
||||
variables.set("list", list);
|
||||
return new LinkedList<>(modManager.getMods());
|
||||
}
|
||||
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
|
||||
}).finalizedResult(Schedulers.javafx(), (list, isDependentsSucceeded) -> {
|
||||
loadingProperty().set(false);
|
||||
if (isDependentsSucceeded)
|
||||
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||
if (newValue != null && newValue.getUserData() == ModListPage.this)
|
||||
itemsProperty().setAll(variables.<List<ModItem>>get("list"));
|
||||
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).collect(Collectors.toList()));
|
||||
});
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.choose_mod"));
|
||||
@@ -151,4 +138,54 @@ public final class ModListPage extends ListPage<ModItem> {
|
||||
public void setParentTab(JFXTabPane parentTab) {
|
||||
this.parentTab = parentTab;
|
||||
}
|
||||
|
||||
public void removeSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
|
||||
try {
|
||||
modManager.removeMods(selectedItems.stream()
|
||||
.map(TreeItem::getValue)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
.toArray(ModInfo[]::new));
|
||||
loadMods(modManager);
|
||||
} catch (IOException ignore) {
|
||||
// Fail to remove mods if the game is running or the mod is absent.
|
||||
}
|
||||
}
|
||||
|
||||
public void enableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.map(TreeItem::getValue)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
.forEach(info -> info.setActive(true));
|
||||
}
|
||||
|
||||
public void disableSelectedMods(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.map(TreeItem::getValue)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
.forEach(info -> info.setActive(false));
|
||||
}
|
||||
|
||||
public ObservableList<ModListPageSkin.ModInfoObject> getItems() {
|
||||
return items.get();
|
||||
}
|
||||
|
||||
public void setItems(ObservableList<ModListPageSkin.ModInfoObject> items) {
|
||||
this.items.set(items);
|
||||
}
|
||||
|
||||
public ListProperty<ModListPageSkin.ModInfoObject> itemsProperty() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return loading.get();
|
||||
}
|
||||
|
||||
public void setLoading(boolean loading) {
|
||||
this.loading.set(loading);
|
||||
}
|
||||
|
||||
public BooleanProperty loadingProperty() {
|
||||
return loading;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Callback;
|
||||
import org.jackhuang.hmcl.mod.ModInfo;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
private static Node wrap(Node node) {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.setPadding(new Insets(0, 5, 0, 2));
|
||||
stackPane.getChildren().setAll(node);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
public ModListPageSkin(ModListPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
JFXTreeTableView<ModInfoObject> tableView = new JFXTreeTableView<>();
|
||||
|
||||
{
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.getStyleClass().add("jfx-tool-bar-second");
|
||||
JFXDepthManager.setDepth(toolbar, 1);
|
||||
toolbar.setPickOnBounds(false);
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnRefresh.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.foregroundFillBinding(), -1, -1)));
|
||||
btnRefresh.setText(i18n("button.refresh"));
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
toolbar.getChildren().add(btnRefresh);
|
||||
|
||||
JFXButton btnAddMod = new JFXButton();
|
||||
btnAddMod.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnAddMod.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
btnAddMod.setGraphic(wrap(SVG.plus(Theme.foregroundFillBinding(), -1, -1)));
|
||||
btnAddMod.setText(i18n("mods.add"));
|
||||
btnAddMod.setOnMouseClicked(e -> skinnable.add());
|
||||
toolbar.getChildren().add(btnAddMod);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnRemove.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
btnRemove.setGraphic(wrap(SVG.delete(Theme.foregroundFillBinding(), -1, -1)));
|
||||
btnRemove.setText(i18n("mods.remove"));
|
||||
btnRemove.setOnMouseClicked(e -> skinnable.removeSelectedMods(tableView.getSelectionModel().getSelectedItems()));
|
||||
toolbar.getChildren().add(btnRemove);
|
||||
|
||||
JFXButton btnEnable = new JFXButton();
|
||||
btnEnable.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnEnable.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
btnEnable.setGraphic(wrap(SVG.check(Theme.foregroundFillBinding(), -1, -1)));
|
||||
btnEnable.setText(i18n("mods.enable"));
|
||||
btnEnable.setOnMouseClicked(e -> skinnable.enableSelectedMods(tableView.getSelectionModel().getSelectedItems()));
|
||||
toolbar.getChildren().add(btnEnable);
|
||||
|
||||
JFXButton btnDisable = new JFXButton();
|
||||
btnDisable.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnDisable.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
btnDisable.setGraphic(wrap(SVG.close(Theme.foregroundFillBinding(), -1, -1)));
|
||||
btnDisable.setText(i18n("mods.disable"));
|
||||
btnDisable.setOnMouseClicked(e -> skinnable.disableSelectedMods(tableView.getSelectionModel().getSelectedItems()));
|
||||
toolbar.getChildren().add(btnDisable);
|
||||
|
||||
root.setTop(toolbar);
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
tableView.setShowRoot(false);
|
||||
tableView.setEditable(true);
|
||||
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
tableView.setRoot(new RecursiveTreeItem<>(skinnable.getItems(), RecursiveTreeObject::getChildren));
|
||||
|
||||
JFXTreeTableColumn<ModInfoObject, Boolean> activeColumn = new JFXTreeTableColumn<>();
|
||||
setupCellValueFactory(activeColumn, ModInfoObject::activeProperty);
|
||||
activeColumn.setCellFactory(c -> new JFXCheckBoxTreeTableCell<>());
|
||||
activeColumn.setEditable(true);
|
||||
activeColumn.setMaxWidth(40);
|
||||
activeColumn.setMinWidth(40);
|
||||
|
||||
JFXTreeTableColumn<ModInfoObject, String> fileNameColumn = new JFXTreeTableColumn<>();
|
||||
fileNameColumn.setText(i18n("archive.name"));
|
||||
setupCellValueFactory(fileNameColumn, ModInfoObject::fileNameProperty);
|
||||
fileNameColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(40).multiply(0.8));
|
||||
|
||||
JFXTreeTableColumn<ModInfoObject, String> versionColumn = new JFXTreeTableColumn<>();
|
||||
versionColumn.setText(i18n("archive.version"));
|
||||
versionColumn.setPrefWidth(100);
|
||||
setupCellValueFactory(versionColumn, ModInfoObject::versionProperty);
|
||||
versionColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(40).multiply(0.2));
|
||||
|
||||
tableView.getColumns().setAll(activeColumn, fileNameColumn, versionColumn);
|
||||
|
||||
tableView.setColumnResizePolicy(JFXTreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
center.setContent(tableView);
|
||||
root.setCenter(center);
|
||||
}
|
||||
|
||||
getChildren().setAll(root);
|
||||
}
|
||||
|
||||
private <T> void setupCellValueFactory(JFXTreeTableColumn<ModInfoObject, T> column, Function<ModInfoObject, ObservableValue<T>> mapper) {
|
||||
column.setCellValueFactory(new Callback<TreeTableColumn.CellDataFeatures<ModInfoObject, T>, ObservableValue<T>>() {
|
||||
@Override
|
||||
public ObservableValue<T> call(TreeTableColumn.CellDataFeatures<ModInfoObject, T> param) {
|
||||
if (column.validateValue(param))
|
||||
return mapper.apply(param.getValue().getValue());
|
||||
else
|
||||
return column.getComputedValue(param);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> {
|
||||
private final BooleanProperty active;
|
||||
private final StringProperty fileName;
|
||||
private final StringProperty version;
|
||||
private final ModInfo modInfo;
|
||||
|
||||
public ModInfoObject(ModInfo modInfo) {
|
||||
this.modInfo = modInfo;
|
||||
this.active = modInfo.activeProperty();
|
||||
this.fileName = new SimpleStringProperty(modInfo.getFileName());
|
||||
this.version = new SimpleStringProperty(modInfo.getVersion());
|
||||
}
|
||||
|
||||
public BooleanProperty activeProperty() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public StringProperty fileNameProperty() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public StringProperty versionProperty() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public ModInfo getModInfo() {
|
||||
return modInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,15 +113,15 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
FXUtils.smoothScrolling(scroll);
|
||||
|
||||
Task.of(variables -> variables.set("list", JavaVersion.getJavas()))
|
||||
.subscribe(Schedulers.javafx(), variables -> {
|
||||
javaItem.loadChildren(
|
||||
(variables.<List<JavaVersion>>get("list")).stream()
|
||||
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion() + i18n("settings.game.java_directory.bit", javaVersion.getPlatform().getBit()), javaVersion.getBinary().toString(), javaVersion))
|
||||
.collect(Collectors.toList()));
|
||||
javaItemsLoaded = true;
|
||||
initializeSelectedJava();
|
||||
});
|
||||
Task.ofResult(JavaVersion::getJavas).thenResult(Schedulers.javafx(), list -> {
|
||||
javaItem.loadChildren(list.stream()
|
||||
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion() + i18n("settings.game.java_directory.bit",
|
||||
javaVersion.getPlatform().getBit()), javaVersion.getBinary().toString(), javaVersion))
|
||||
.collect(Collectors.toList()));
|
||||
javaItemsLoaded = true;
|
||||
initializeSelectedJava();
|
||||
return null;
|
||||
}).start();
|
||||
|
||||
javaItem.setSelectedData(null);
|
||||
javaItem.setFallbackData(JavaVersion.fromCurrentEnvironment());
|
||||
|
||||
@@ -81,8 +81,7 @@ public class Versions {
|
||||
|
||||
public static void updateGameAssets(Profile profile, String version) {
|
||||
Version resolvedVersion = profile.getRepository().getResolvedVersion(version);
|
||||
TaskExecutor executor = new GameAssetIndexDownloadTask(profile.getDependency(), resolvedVersion)
|
||||
.then(new GameAssetDownloadTask(profile.getDependency(), resolvedVersion))
|
||||
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), resolvedVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY)
|
||||
.executor();
|
||||
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"));
|
||||
executor.start();
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.wizard;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.utils.JFXUtilities;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
|
||||
@@ -103,7 +103,10 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
"-- System Details --\n" +
|
||||
" Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
|
||||
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
|
||||
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n";
|
||||
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
|
||||
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
|
||||
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
|
||||
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
|
||||
|
||||
LOG.log(Level.SEVERE, text);
|
||||
|
||||
|
||||
Binary file not shown.
@@ -132,6 +132,16 @@
|
||||
-fx-fill: -fx-base-darker-color;
|
||||
}
|
||||
|
||||
.sponsor-pane {
|
||||
-fx-padding: 16;
|
||||
-fx-background-color: white;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.sponsor-pane .label {
|
||||
-fx-wrap-text: true;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Tab Pane *
|
||||
@@ -342,12 +352,11 @@
|
||||
|
||||
.jfx-tool-bar HBox {
|
||||
-fx-alignment: center;
|
||||
/* -fx-spacing: 25.0;*/
|
||||
-fx-padding: 0.0 5.0;
|
||||
}
|
||||
|
||||
.jfx-tool-bar .jfx-decorator-button {
|
||||
-fx-cursor: head;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.jfx-tool-bar Label {
|
||||
@@ -447,33 +456,6 @@
|
||||
-fx-fill: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button {
|
||||
-fx-font-size: 16.0px;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button .radio {
|
||||
-fx-stroke-width: 2.0px;
|
||||
-fx-fill: transparent;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button-blue {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-jfx-selected-color: -fx-base-color;
|
||||
-jfx-unselected-color: #212121;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button-red {
|
||||
-fx-text-fill: #f44336;
|
||||
-jfx-selected-color: #f44336;
|
||||
-jfx-unselected-color: #b71c1c;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button-green {
|
||||
-fx-text-fill: #4caf50;
|
||||
-jfx-selected-color: #4caf50;
|
||||
-jfx-unselected-color: #1b5e20;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Slider *
|
||||
@@ -514,18 +496,12 @@
|
||||
-fx-font-size: 10.0;
|
||||
}
|
||||
|
||||
/******************************************************/
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Rippler *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
/*.jfx-rippler {
|
||||
-fx-rippler-fill: -fx-base-color;
|
||||
-fx-mask-type: RECT;
|
||||
}*/
|
||||
.jfx-rippler:hover {
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
@@ -536,62 +512,29 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
.custom-jfx-button-raised .jfx-rippler {
|
||||
-jfx-rippler-fill: YELLOW;
|
||||
}
|
||||
|
||||
.custom-jfx-button-raised {
|
||||
-fx-padding: 0.7em 0.57em;
|
||||
-fx-font-size: 14.0px;
|
||||
-jfx-button-type: RAISED;
|
||||
-fx-background-color: rgb(102.0, 153.0, 102.0);
|
||||
-fx-pref-width: 200.0;
|
||||
-fx-text-fill: WHITE;
|
||||
}
|
||||
|
||||
.circle-jfx-button-raised .jfx-rippler {
|
||||
-jfx-rippler-fill: YELLOW;
|
||||
}
|
||||
|
||||
.circle-jfx-button-raised {
|
||||
-fx-padding: 0.7em 0.57em;
|
||||
-fx-font-size: 14.0px;
|
||||
-jfx-button-type: RAISED;
|
||||
-fx-background-color: rgb(102.0, 153.0, 102.0);
|
||||
-fx-pref-width: 200.0;
|
||||
-fx-text-fill: WHITE;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
.jfx-button {
|
||||
-jfx-disable-visual-focus: true;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.jfx-button-raised {
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: -fx-base-color;
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.jfx-button-raised .jfx-rippler {
|
||||
-jfx-rippler-fill: white;
|
||||
}
|
||||
|
||||
.jfx-button-raised .label {
|
||||
-fx-text-fill: white;
|
||||
.jfx-button-raised, .jfx-button-raised * {
|
||||
-fx-text-fill: -fx-base-text-fill;
|
||||
-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-border, .jfx-button-border * {
|
||||
-fx-text-fill: -fx-base-darker-color;
|
||||
}
|
||||
|
||||
.jfx-button-raised-round {
|
||||
@@ -610,20 +553,6 @@
|
||||
-jfx-checked-color: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.custom-jfx-check-box {
|
||||
-jfx-checked-color: RED;
|
||||
}
|
||||
|
||||
.custom-jfx-check-box-all-colored {
|
||||
-jfx-checked-color: -fx-base-color;
|
||||
-jfx-unchecked-color: -fx-base-color;
|
||||
-fx-text-fill: -fx-base-color;
|
||||
}
|
||||
|
||||
.custom-jfx-check-box-text-colored {
|
||||
-fx-text-fill: rgb(153.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Progress Bar *
|
||||
@@ -647,15 +576,6 @@
|
||||
-fx-background-color: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.custom-jfx-progress-bar > .bar {
|
||||
-fx-background-color: rgb(255.0, 128.0, 0.0);
|
||||
}
|
||||
|
||||
.custom-jfx-progress-bar-stroke > .bar {
|
||||
-fx-background-color: -fx-base-color;
|
||||
-fx-padding: 6;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Textfield *
|
||||
@@ -708,47 +628,6 @@
|
||||
-jfx-expanded: false;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
|
||||
.custom-jfx-list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
|
||||
-fx-background-color: rgba(255, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.custom-jfx-list-view {
|
||||
-fx-background-insets: 0.0;
|
||||
-jfx-cell-horizontal-margin: 0.0;
|
||||
-jfx-cell-vertical-margin: 5.0;
|
||||
-jfx-expanded: false;
|
||||
-fx-max-width: 200.0px;
|
||||
/* important to hide the list change of height */
|
||||
-fx-background-color: TRANSPARENT;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .jfx-rippler {
|
||||
-jfx-rippler-fill: RED;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view1 {
|
||||
-jfx-vertical-gap: 10.0;
|
||||
-fx-pref-width: 150px;
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view-icon,
|
||||
.jfx-list-cell:selected .label .custom-jfx-list-view-icon {
|
||||
-fx-fill: -fx-base-color;
|
||||
-fx-padding: 0.0 10.0 0.0 5.0;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view-icon-container {
|
||||
-fx-pref-width: 40px;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .sublist-item {
|
||||
-fx-border-color: #e0e0e0;
|
||||
-fx-border-width: 1 0 1 0;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
-fx-background-color: transparent;
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
|
||||
@@ -798,20 +677,6 @@
|
||||
-jfx-mask-type: CIRCLE;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .jfx-list-cell .sublist-header > .drop-icon {
|
||||
-fx-background-color: GRAY;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .jfx-list-cell:filled:hover .sublist-header > .drop-icon {
|
||||
-fx-background-color: BLACK;
|
||||
}
|
||||
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX SUBLIST IMPORTANT *
|
||||
@@ -851,10 +716,6 @@
|
||||
-fx-padding: 0 0 0 12;
|
||||
}
|
||||
|
||||
/*.custom-jfx-list-view .sublist-container {
|
||||
-fx-padding : 0 0 5 0;
|
||||
}*/
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Toggle Button *
|
||||
@@ -865,14 +726,6 @@
|
||||
-jfx-toggle-color: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.custom-jfx-toggle-button {
|
||||
-jfx-toggle-color: #4285F4;
|
||||
}
|
||||
|
||||
.custom-jfx-toggle-button-red {
|
||||
-jfx-toggle-color: red;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
-fx-font-size: 14.0px;
|
||||
}
|
||||
@@ -1038,6 +891,14 @@
|
||||
-fx-stroke-width: 3.0;
|
||||
}
|
||||
|
||||
.large-spinner-pane .jfx-spinner {
|
||||
-jfx-radius: 20;
|
||||
}
|
||||
|
||||
.large-spinner-pane .jfx-spinner {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.second-spinner {
|
||||
-jfx-radius: 30;
|
||||
}
|
||||
@@ -1190,7 +1051,7 @@
|
||||
|
||||
.tree-table-view .column-header .label {
|
||||
-fx-text-fill: #949494;
|
||||
-fx-padding: 16 0 16 0;
|
||||
-fx-padding: 8 0 8 0;
|
||||
}
|
||||
|
||||
.tree-table-view .column-header .arrow, .tree-table-view .column-header .sort-order-dot {
|
||||
@@ -1208,8 +1069,8 @@
|
||||
|
||||
.tree-table-view .tree-table-cell {
|
||||
-fx-border-width: 0 0 0 0;
|
||||
-fx-padding: 16 0 16 0;
|
||||
-fx-alignment: top-center;
|
||||
-fx-padding: 8 0 8 0;
|
||||
/*-fx-alignment: top-center;*/
|
||||
}
|
||||
|
||||
.tree-table-view .column-overlay {
|
||||
@@ -1362,12 +1223,3 @@
|
||||
.fit-width {
|
||||
-fx-pref-width: 100%;
|
||||
}
|
||||
/*
|
||||
.jfx-scroll-pane .main-header {
|
||||
-fx-background-image: url("../bg1.jpg");
|
||||
}
|
||||
|
||||
.jfx-scroll-pane .condensed-header {
|
||||
-fx-background-image: url("../bg4.jpg");
|
||||
}
|
||||
*/
|
||||
@@ -14,7 +14,7 @@
|
||||
<fx:define>
|
||||
<Insets fx:id="insets" bottom="12" />
|
||||
</fx:define>
|
||||
<SpinnerPane fx:id="spinnerPane">
|
||||
<SpinnerPane fx:id="spinnerPane" styleClass="large-spinner-pane">
|
||||
<VBox fx:id="borderPane" alignment="CENTER" FXUtils.limitWidth="500">
|
||||
<HBox style="-fx-padding: 0 0 16 5;"><Label text="%modpack.task.install" /></HBox>
|
||||
<ComponentList>
|
||||
|
||||
@@ -5,22 +5,31 @@
|
||||
<?import javafx.scene.layout.*?>
|
||||
<fx:root xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
type="StackPane"
|
||||
type="BorderPane"
|
||||
prefHeight="400.0" prefWidth="600.0">
|
||||
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
|
||||
<VBox fx:id="centrePane">
|
||||
<HBox fx:id="checkPane" spacing="10" style="-fx-padding: 10;">
|
||||
<JFXCheckBox fx:id="chkRelease" text="%version.game.release" selected="true" />
|
||||
<JFXCheckBox fx:id="chkSnapshot" text="%version.game.snapshot" />
|
||||
<JFXCheckBox fx:id="chkOld" text="%version.game.old" />
|
||||
</HBox>
|
||||
<JFXListView fx:id="list" styleClass="jfx-list-view" VBox.vgrow="ALWAYS">
|
||||
</JFXListView>
|
||||
</VBox>
|
||||
<StackPane fx:id="failedPane">
|
||||
<Label onMouseClicked="#onRefresh" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.refresh" />
|
||||
</StackPane>
|
||||
<StackPane fx:id="emptyPane">
|
||||
<Label onMouseClicked="#onBack" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.empty" />
|
||||
</StackPane>
|
||||
<top>
|
||||
<StackPane styleClass="sponsor-pane" onMouseClicked="#onSponsor">
|
||||
<Label text="%sponsor.bmclapi" />
|
||||
</StackPane>
|
||||
</top>
|
||||
<center>
|
||||
<StackPane fx:id="root">
|
||||
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
|
||||
<VBox fx:id="centrePane">
|
||||
<HBox fx:id="checkPane" spacing="10" style="-fx-padding: 10;">
|
||||
<JFXCheckBox fx:id="chkRelease" text="%version.game.release" selected="true" />
|
||||
<JFXCheckBox fx:id="chkSnapshot" text="%version.game.snapshot" />
|
||||
<JFXCheckBox fx:id="chkOld" text="%version.game.old" />
|
||||
</HBox>
|
||||
<JFXListView fx:id="list" styleClass="jfx-list-view" VBox.vgrow="ALWAYS">
|
||||
</JFXListView>
|
||||
</VBox>
|
||||
<StackPane fx:id="failedPane">
|
||||
<Label onMouseClicked="#onRefresh" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.refresh" />
|
||||
</StackPane>
|
||||
<StackPane fx:id="emptyPane">
|
||||
<Label onMouseClicked="#onBack" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.empty" />
|
||||
</StackPane>
|
||||
</StackPane>
|
||||
</center>
|
||||
</fx:root>
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
# Contributors: dxNeil
|
||||
about.copyright=Copyright
|
||||
about.copyright.statement=Copyright (c) 2018 huangyuhui.
|
||||
about.copyright.statement=Copyright (c) 2019 huangyuhui.
|
||||
about.author=Author
|
||||
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
|
||||
about.thanks_to=Thanks to
|
||||
@@ -66,6 +66,7 @@ account.username=Name
|
||||
|
||||
archive.author=Authors
|
||||
archive.game_version=Game
|
||||
archive.name=Name
|
||||
archive.version=Version
|
||||
|
||||
assets.download=Download assets
|
||||
@@ -125,6 +126,7 @@ input.url=Must be a valid URL.
|
||||
install=Install New Game
|
||||
install.failed=Failed to install
|
||||
install.failed.downloading=Failed to install due to some files not downloaded successfully
|
||||
install.failed.downloading.detail=Failed to download file: %s
|
||||
install.installer.choose=Choose a %s version
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=Game
|
||||
@@ -252,6 +254,9 @@ mods.add=Add mods
|
||||
mods.add.failed=Failed to add mods %s.
|
||||
mods.add.success=Successfully added mods %s.
|
||||
mods.choose_mod=Choose your mods
|
||||
mods.enable=Enable
|
||||
mods.disable=Disable
|
||||
mods.name=Name
|
||||
mods.remove=Remove
|
||||
|
||||
datapack=Data packs
|
||||
@@ -364,6 +369,9 @@ settings.type.global.manage=Global Game Settings
|
||||
settings.type.global.edit=Configure global game settings
|
||||
settings.type.special.enable=Enable specialized settings for this game
|
||||
|
||||
sponsor.bmclapi=To obtain a stable download service, please consider support BMCLAPI. Click here for more information
|
||||
sponsor.hmcl=HMCL is a free, open source Minecraft launcher the enables users to easily manage multiple, separate installations of Minecraft. We're using Afdian so that we can continue to pay for our hosting and project development. Click here for more information.
|
||||
|
||||
update=Update
|
||||
update.channel.dev=Update to development version
|
||||
update.channel.stable=Update to stable version
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#
|
||||
|
||||
about.copyright=版權
|
||||
about.copyright.statement=Copyright (c) 2018 huangyuhui.
|
||||
about.copyright.statement=Copyright (c) 2019 huangyuhui.
|
||||
about.author=作者
|
||||
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
|
||||
about.thanks_to=鳴謝
|
||||
@@ -65,6 +65,7 @@ account.username=使用者名稱
|
||||
|
||||
archive.author=作者
|
||||
archive.game_version=遊戲版本
|
||||
archive.name=名稱
|
||||
archive.version=版本
|
||||
|
||||
assets.download=下載資源
|
||||
@@ -124,6 +125,7 @@ input.url=必須為有效連結
|
||||
install=新增遊戲
|
||||
install.failed=安裝失敗
|
||||
install.failed.downloading=安裝失敗,部分文件未能完成下載
|
||||
install.failed.downloading.detail=未能下載檔案:%s
|
||||
install.installer.choose=選擇 %s 版本
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=遊戲
|
||||
@@ -251,6 +253,8 @@ mods.add=新增模組
|
||||
mods.add.failed=新增模組 %s 失敗。
|
||||
mods.add.success=成功新增模組 %s。
|
||||
mods.choose_mod=選擇模組
|
||||
mods.enable=啟用
|
||||
mods.disable=禁用
|
||||
mods.remove=刪除
|
||||
|
||||
datapack=資料包
|
||||
@@ -363,6 +367,9 @@ settings.type.global.manage=全域遊戲設定
|
||||
settings.type.global.edit=編輯全域遊戲設定
|
||||
settings.type.special.enable=啟用遊戲特別設定(不影響其他遊戲版本)
|
||||
|
||||
sponsor.bmclapi=請贊助 BMCLAPI 從而獲得穩定高速的下載服務,點擊此處查閱詳細訊息。
|
||||
sponsor.hmcl=HMCL 是一個免費、開源的 Minecraft 啟動器,允許玩家方便快捷地安裝、管理、運行遊戲。您的贊助將幫助 HMCL 獲得更好的發展、支持穩定高速的遊戲安裝與文件下載服務。點擊此處查閱更多詳細訊息。
|
||||
|
||||
update=啟動器更新
|
||||
update.channel.dev=更新到開發版
|
||||
update.channel.stable=更新到推薦版本
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#
|
||||
|
||||
about.copyright=版权
|
||||
about.copyright.statement=Copyright (c) 2018 huangyuhui.
|
||||
about.copyright.statement=Copyright (c) 2019 huangyuhui.
|
||||
about.author=作者
|
||||
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
|
||||
about.thanks_to=鸣谢
|
||||
@@ -65,6 +65,7 @@ account.username=用户名
|
||||
|
||||
archive.author=作者
|
||||
archive.game_version=游戏版本
|
||||
archive.name=名称
|
||||
archive.version=版本
|
||||
|
||||
assets.download=下载资源
|
||||
@@ -124,6 +125,7 @@ input.url=必须是合法的链接
|
||||
install=添加游戏
|
||||
install.failed=安装失败
|
||||
install.failed.downloading=安装失败,部分文件未能完成下载
|
||||
install.failed.downloading.detail=未能下载文件:%s
|
||||
install.installer.choose=选择 %s 版本
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=游戏
|
||||
@@ -251,6 +253,8 @@ mods.add=添加模组
|
||||
mods.add.failed=添加模组 %s 失败。
|
||||
mods.add.success=成功添加模组 %s。
|
||||
mods.choose_mod=选择模组
|
||||
mods.enable=启用
|
||||
mods.disable=禁用
|
||||
mods.remove=删除
|
||||
|
||||
datapack=数据包
|
||||
@@ -363,6 +367,9 @@ settings.type.global.manage=全局游戏设置
|
||||
settings.type.global.edit=编辑全局版本设置
|
||||
settings.type.special.enable=启用游戏特定设置(不影响其他游戏版本)
|
||||
|
||||
sponsor.bmclapi=请赞助 BMCLAPI 从而获得稳定高速的下载服务,点击此处查阅详细信息。
|
||||
sponsor.hmcl=HMCL 是一个免费、开源的 Minecraft 启动器,允许玩家方便快捷地安装、管理、运行游戏。您的赞助将帮助 HMCL 获得更好的发展、支持稳定高速的游戏安装与文件下载服务。点击此处查阅更多详细信息。
|
||||
|
||||
update=启动器更新
|
||||
update.channel.dev=更新到开发版
|
||||
update.channel.stable=更新到推荐版本
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
return new Arguments().addJVMArguments(
|
||||
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
|
||||
"-Dauthlibinjector.side=client",
|
||||
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
|
||||
"-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
|
||||
}
|
||||
}
|
||||
|
||||
private class AuthlibInjectorVersionInfo {
|
||||
private static class AuthlibInjectorVersionInfo {
|
||||
@SerializedName("build_number")
|
||||
public int buildNumber;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.download.forge.ForgeVersionList;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeBMCLVersionList;
|
||||
import org.jackhuang.hmcl.download.game.GameVersionList;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderBMCLVersionList;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
|
||||
@@ -44,7 +44,7 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
case "game":
|
||||
return GameVersionList.INSTANCE;
|
||||
case "forge":
|
||||
return ForgeVersionList.INSTANCE;
|
||||
return ForgeBMCLVersionList.INSTANCE;
|
||||
case "liteloader":
|
||||
return LiteLoaderBMCLVersionList.INSTANCE;
|
||||
case "optifine":
|
||||
@@ -59,8 +59,8 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
return baseURL
|
||||
.replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.replace("https://libraries.minecraft.net", "http://bmclapi2.bangbang93.com/libraries")
|
||||
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "http://bmclapi2.bangbang93.com/maven")
|
||||
.replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
|
||||
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven")
|
||||
.replace("http://dl.liteloader.com/versions/versions.json", "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
|
||||
.replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven")
|
||||
.replace("https://authlib-injector.yushi.moe", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector");
|
||||
|
||||
@@ -19,10 +19,7 @@ package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.download.game.LibrariesUniqueTask;
|
||||
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
|
||||
import org.jackhuang.hmcl.download.game.*;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
@@ -74,7 +71,13 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
@Override
|
||||
public Task checkGameCompletionAsync(Version version) {
|
||||
return new ParallelTask(
|
||||
new GameAssetDownloadTask(this, version),
|
||||
Task.ofThen(var -> {
|
||||
if (!repository.getVersionJar(version).exists())
|
||||
return new GameDownloadTask(this, null, version);
|
||||
else
|
||||
return null;
|
||||
}),
|
||||
new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY),
|
||||
new GameLibrariesTask(this, version)
|
||||
);
|
||||
}
|
||||
@@ -87,7 +90,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
@Override
|
||||
public Task installLibraryAsync(String gameVersion, Version version, String libraryId, String libraryVersion) {
|
||||
VersionList<?> versionList = getVersionList(libraryId);
|
||||
return versionList.loadAsync(getDownloadProvider())
|
||||
return versionList.loadAsync(gameVersion, getDownloadProvider())
|
||||
.then(variables -> installLibraryAsync(version, versionList.getVersion(gameVersion, libraryVersion)
|
||||
.orElseThrow(() -> new IllegalStateException("Remote library " + libraryId + " has no version " + libraryVersion))));
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class DefaultGameBuilder extends GameBuilder {
|
||||
version = version.setId(name).setJar(null);
|
||||
variables.set("version", version);
|
||||
Task result = downloadGameAsync(gameVersion, version).then(new ParallelTask(
|
||||
new GameAssetDownloadTask(dependencyManager, version),
|
||||
new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY),
|
||||
new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
|
||||
).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version))); // using [with] because download failure here are tolerant.
|
||||
|
||||
|
||||
@@ -40,6 +40,16 @@ public class MaintainTask extends TaskResult<Version> {
|
||||
}
|
||||
|
||||
public static Version maintain(Version version) {
|
||||
if (version.getMainClass().contains("launchwrapper")) {
|
||||
return maintainGameWithLaunchWrapper(version);
|
||||
} else {
|
||||
// Vanilla Minecraft does not need maintain
|
||||
// Forge 1.13 support not implemented, not compatible with OptiFine currently.
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
private static Version maintainGameWithLaunchWrapper(Version version) {
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
|
||||
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.download.forge.ForgeBMCLVersionList;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeVersionList;
|
||||
import org.jackhuang.hmcl.download.game.GameVersionList;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList;
|
||||
@@ -44,7 +45,7 @@ public class MojangDownloadProvider implements DownloadProvider {
|
||||
case "game":
|
||||
return GameVersionList.INSTANCE;
|
||||
case "forge":
|
||||
return ForgeVersionList.INSTANCE;
|
||||
return ForgeBMCLVersionList.INSTANCE;
|
||||
case "liteloader":
|
||||
return LiteLoaderVersionList.INSTANCE;
|
||||
case "optifine":
|
||||
@@ -56,6 +57,7 @@ public class MojangDownloadProvider implements DownloadProvider {
|
||||
|
||||
@Override
|
||||
public String injectURL(String baseURL) {
|
||||
return baseURL;
|
||||
return baseURL
|
||||
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven");
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,14 @@ public abstract class VersionList<T extends RemoteVersion> {
|
||||
return !versions.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the version list that contains the remote versions which depends on the specific game version has been loaded.
|
||||
* @param gameVersion the remote version depends on
|
||||
*/
|
||||
public boolean isLoaded(String gameVersion) {
|
||||
return !versions.get(gameVersion).isEmpty();
|
||||
}
|
||||
|
||||
public abstract boolean hasType();
|
||||
|
||||
protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
@@ -56,6 +64,15 @@ public abstract class VersionList<T extends RemoteVersion> {
|
||||
*/
|
||||
public abstract Task refreshAsync(DownloadProvider downloadProvider);
|
||||
|
||||
/**
|
||||
* @param gameVersion the remote version depends on
|
||||
* @param downloadProvider DownloadProvider
|
||||
* @return the task to reload the remote version list.
|
||||
*/
|
||||
public Task refreshAsync(String gameVersion, DownloadProvider downloadProvider) {
|
||||
return refreshAsync(downloadProvider);
|
||||
}
|
||||
|
||||
public Task loadAsync(DownloadProvider downloadProvider) {
|
||||
return Task.ofThen(variables -> {
|
||||
lock.readLock().lock();
|
||||
@@ -70,6 +87,20 @@ public abstract class VersionList<T extends RemoteVersion> {
|
||||
});
|
||||
}
|
||||
|
||||
public Task loadAsync(String gameVersion, DownloadProvider downloadProvider) {
|
||||
return Task.ofThen(variables -> {
|
||||
lock.readLock().lock();
|
||||
boolean loaded;
|
||||
|
||||
try {
|
||||
loaded = isLoaded(gameVersion);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
return loaded ? null : refreshAsync(gameVersion, downloadProvider);
|
||||
});
|
||||
}
|
||||
|
||||
protected Collection<T> getVersionsImpl(String gameVersion) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
|
||||
public static final ForgeBMCLVersionList INSTANCE = new ForgeBMCLVersionList();
|
||||
|
||||
private ForgeBMCLVersionList() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task loadAsync(DownloadProvider downloadProvider) {
|
||||
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task refreshAsync(DownloadProvider downloadProvider) {
|
||||
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task refreshAsync(String gameVersion, DownloadProvider downloadProvider) {
|
||||
final GetTask task = new GetTask(NetworkUtils.toURL("https://bmclapi2.bangbang93.com/forge/minecraft/" + gameVersion));
|
||||
return new Task() {
|
||||
@Override
|
||||
public Collection<? extends Task> getDependents() {
|
||||
return Collections.singleton(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
List<ForgeVersion> forgeVersions = JsonUtils.GSON.fromJson(task.getResult(), new TypeToken<List<ForgeVersion>>() {
|
||||
}.getType());
|
||||
versions.clear(gameVersion);
|
||||
if (forgeVersions == null) return;
|
||||
for (ForgeVersion version : forgeVersions) {
|
||||
if (version == null)
|
||||
continue;
|
||||
String jar = null;
|
||||
for (ForgeVersion.File file : version.getFiles())
|
||||
if ("installer".equals(file.getCategory())) {
|
||||
String classifier = gameVersion + "-" + version.getVersion()
|
||||
+ (StringUtils.isNotBlank(version.getBranch()) ? "-" + version.getBranch() : "");
|
||||
String fileName = "forge-" + classifier + "-" + file.getCategory() + "." + file.getFormat();
|
||||
jar = "https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/" + classifier + "/" + fileName;
|
||||
}
|
||||
|
||||
if (jar == null)
|
||||
continue;
|
||||
versions.put(gameVersion, new ForgeRemoteVersion(
|
||||
version.getGameVersion(), version.getVersion(), jar
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public static final class ForgeVersion implements Validation {
|
||||
|
||||
private final String branch;
|
||||
private final String mcversion;
|
||||
private final String version;
|
||||
private final int build;
|
||||
private final List<File> files;
|
||||
|
||||
/**
|
||||
* No-arg constructor for Gson.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public ForgeVersion() {
|
||||
this(null, null, null, 0, null);
|
||||
}
|
||||
|
||||
public ForgeVersion(String branch, String mcversion, String version, int build, List<File> files) {
|
||||
this.branch = branch;
|
||||
this.mcversion = mcversion;
|
||||
this.version = version;
|
||||
this.build = build;
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public String getBranch() {
|
||||
return branch;
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
return mcversion;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public int getBuild() {
|
||||
return build;
|
||||
}
|
||||
|
||||
public List<File> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (files == null)
|
||||
throw new JsonParseException("ForgeVersion files cannot be null");
|
||||
if (version == null)
|
||||
throw new JsonParseException("ForgeVersion version cannot be null");
|
||||
if (mcversion == null)
|
||||
throw new JsonParseException("ForgeVersion mcversion cannot be null");
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public static final class File {
|
||||
private final String format;
|
||||
private final String category;
|
||||
private final String hash;
|
||||
|
||||
public File() {
|
||||
this("", "", "");
|
||||
}
|
||||
|
||||
public File(String format, String category, String hash) {
|
||||
this.format = format;
|
||||
this.category = category;
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
public String getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,24 +18,17 @@
|
||||
package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.SimpleVersionProvider;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -45,31 +38,48 @@ public final class ForgeInstallTask extends TaskResult<Version> {
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final Version version;
|
||||
private final File installer = new File("forge-installer.jar").getAbsoluteFile();
|
||||
private Path installer;
|
||||
private final ForgeRemoteVersion remote;
|
||||
private final List<Task> dependents = new LinkedList<>();
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
private Task downloadFileTask() {
|
||||
return new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), installer);
|
||||
}
|
||||
private Task dependent;
|
||||
private TaskResult<Version> dependency;
|
||||
|
||||
public ForgeInstallTask(DefaultDependencyManager dependencyManager, Version version, ForgeRemoteVersion remoteVersion) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.remote = remoteVersion;
|
||||
}
|
||||
|
||||
dependents.add(downloadFileTask());
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
installer = Files.createTempFile("forge-installer", ".jar");
|
||||
|
||||
dependent = new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), installer.toFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPostExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postExecute() throws Exception {
|
||||
Files.deleteIfExists(installer);
|
||||
setResult(dependency.getResult());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependents() {
|
||||
return dependents;
|
||||
return Collections.singleton(dependent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getDependencies() {
|
||||
return dependencies;
|
||||
public Collection<Task> getDependencies() {
|
||||
return Collections.singleton(dependency);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,38 +93,10 @@ public final class ForgeInstallTask extends TaskResult<Version> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
try (ZipFile zipFile = new ZipFile(installer)) {
|
||||
InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json"));
|
||||
if (stream == null)
|
||||
throw new IOException("Malformed forge installer file, install_profile.json does not exist.");
|
||||
String json = IOUtils.readFullyAsString(stream);
|
||||
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class);
|
||||
|
||||
// unpack the universal jar in the installer file.
|
||||
Library forgeLibrary = Library.fromName(installProfile.getInstall().getPath());
|
||||
File forgeFile = dependencyManager.getGameRepository().getLibraryFile(version, forgeLibrary);
|
||||
if (!FileUtils.makeFile(forgeFile))
|
||||
throw new IOException("Cannot make directory " + forgeFile.getParent());
|
||||
|
||||
ZipEntry forgeEntry = zipFile.getEntry(installProfile.getInstall().getFilePath());
|
||||
try (InputStream is = zipFile.getInputStream(forgeEntry); OutputStream os = new FileOutputStream(forgeFile)) {
|
||||
IOUtils.copyTo(is, os);
|
||||
}
|
||||
|
||||
// resolve the version
|
||||
SimpleVersionProvider provider = new SimpleVersionProvider();
|
||||
provider.addVersion(version);
|
||||
|
||||
setResult(installProfile.getVersionInfo()
|
||||
.setInheritsFrom(version.getId())
|
||||
.resolve(provider).setJar(null)
|
||||
.setId(version.getId()).setLogging(Collections.emptyMap()));
|
||||
|
||||
dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo()));
|
||||
}
|
||||
|
||||
if (!installer.delete())
|
||||
throw new IOException("Unable to delete installer file" + installer);
|
||||
public void execute() {
|
||||
if (VersionNumber.VERSION_COMPARATOR.compare("1.13", remote.getGameVersion()) <= 0)
|
||||
dependency = new ForgeNewInstallTask(dependencyManager, version, installer);
|
||||
else
|
||||
dependency = new ForgeOldInstallTask(dependencyManager, version, installer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import org.jackhuang.hmcl.game.Artifact;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Immutable
|
||||
public class ForgeNewInstallProfile {
|
||||
|
||||
private final int spec;
|
||||
private final String minecraft;
|
||||
private final String json;
|
||||
private final List<Library> libraries;
|
||||
private final List<Processor> processors;
|
||||
private final Map<String, Datum> data;
|
||||
|
||||
public ForgeNewInstallProfile(int spec, String minecraft, String json, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
|
||||
this.spec = spec;
|
||||
this.minecraft = minecraft;
|
||||
this.json = json;
|
||||
this.libraries = libraries;
|
||||
this.processors = processors;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for install_profile.json.
|
||||
*/
|
||||
public int getSpec() {
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vanilla game version that this installer supports.
|
||||
*/
|
||||
public String getMinecraft() {
|
||||
return minecraft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version json to be installed.
|
||||
* @return path of the version json relative to the installer JAR file.
|
||||
*/
|
||||
public String getJson() {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Libraries that processors depend on.
|
||||
* @return the required dependencies.
|
||||
*/
|
||||
public List<Library> getLibraries() {
|
||||
return libraries == null ? Collections.emptyList() : libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tasks to be executed to setup modded environment.
|
||||
*/
|
||||
public List<Processor> getProcessors() {
|
||||
if (processors == null) return Collections.emptyList();
|
||||
return processors.stream().filter(p -> p.isSide("client")).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for processors.
|
||||
*/
|
||||
public Map<String, String> getData() {
|
||||
if (data == null)
|
||||
return new HashMap<>();
|
||||
|
||||
return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getClient()));
|
||||
}
|
||||
|
||||
public static class Processor {
|
||||
private final List<String> sides;
|
||||
private final Artifact jar;
|
||||
private final List<Artifact> classpath;
|
||||
private final List<String> args;
|
||||
private final Map<String, String> outputs;
|
||||
|
||||
public Processor(List<String> sides, Artifact jar, List<Artifact> classpath, List<String> args, Map<String, String> outputs) {
|
||||
this.sides = sides;
|
||||
this.jar = jar;
|
||||
this.classpath = classpath;
|
||||
this.args = args;
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check which side this processor should be run on. We only support client install currently.
|
||||
* @param side can be one of "client", "server", "extract".
|
||||
* @return true if the processor can run on the side.
|
||||
*/
|
||||
public boolean isSide(String side) {
|
||||
return sides == null || sides.contains(side);
|
||||
}
|
||||
|
||||
/**
|
||||
* The executable jar of this processor task. Will be executed in installation process.
|
||||
* @return the artifact path of executable jar.
|
||||
*/
|
||||
public Artifact getJar() {
|
||||
return jar;
|
||||
}
|
||||
|
||||
/**
|
||||
* The dependencies of this processor task.
|
||||
* @return the artifact path of dependencies.
|
||||
*/
|
||||
public List<Artifact> getClasspath() {
|
||||
return classpath == null ? Collections.emptyList() : classpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments to pass to the processor jar.
|
||||
* Each item can be in one of the following formats:
|
||||
* [artifact]: An artifact path, used for locating files.
|
||||
* {entry}: Get corresponding value of the entry in {@link ForgeNewInstallProfile#getData()}
|
||||
* {MINECRAFT_JAR}: path of the Minecraft jar.
|
||||
* {SIDE}: values other than "client" will be ignored.
|
||||
* @return arguments to pass to the processor jar.
|
||||
* @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction)
|
||||
*/
|
||||
public List<String> getArgs() {
|
||||
return args == null ? Collections.emptyList() : args;
|
||||
}
|
||||
|
||||
/**
|
||||
* File-checksum pairs, used for verifying the output file is correct.
|
||||
* Arguments to pass to the processor jar.
|
||||
* Keys can be in one of [artifact] or {entry}. Should be file path.
|
||||
* Values can be in one of {entry} or 'literal'. Should be SHA-1 checksum.
|
||||
* @return files output from this processor.
|
||||
* @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction)
|
||||
*/
|
||||
public Map<String, String> getOutputs() {
|
||||
return outputs == null ? Collections.emptyMap() : outputs;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Datum {
|
||||
private final String client;
|
||||
|
||||
public Datum(String client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be in the following formats:
|
||||
* [value]: An artifact path.
|
||||
* 'value': A string literal.
|
||||
* value: A file in the installer package, to be extracted to a temp folder, and then have the absolute path in replacements.
|
||||
* @return Value to use for the client install
|
||||
*/
|
||||
public String getClient() {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.DigestUtils.digest;
|
||||
import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class ForgeNewInstallTask extends TaskResult<Version> {
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DefaultGameRepository gameRepository;
|
||||
private final Version version;
|
||||
private final Path installer;
|
||||
private final List<Task> dependents = new LinkedList<>();
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
private ForgeNewInstallProfile profile;
|
||||
private Version forgeVersion;
|
||||
|
||||
public ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, Path installer) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.gameRepository = dependencyManager.getGameRepository();
|
||||
this.version = version;
|
||||
this.installer = installer;
|
||||
}
|
||||
|
||||
private <E extends Exception> String parseLiteral(String literal, Map<String, String> var, ExceptionalFunction<String, String, E> plainConverter) throws E {
|
||||
if (StringUtils.isSurrounded(literal, "{", "}"))
|
||||
return var.get(StringUtils.removeSurrounding(literal, "{", "}"));
|
||||
else if (StringUtils.isSurrounded(literal, "'", "'"))
|
||||
return StringUtils.removeSurrounding(literal, "'");
|
||||
else if (StringUtils.isSurrounded(literal, "[", "]"))
|
||||
return gameRepository.getArtifactFile(version, new Artifact(StringUtils.removeSurrounding(literal, "[", "]"))).toString();
|
||||
else
|
||||
return plainConverter.apply(literal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "version";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
||||
profile = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
|
||||
forgeVersion = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath(profile.getJson())), Version.class);
|
||||
|
||||
for (Library library : profile.getLibraries()) {
|
||||
Path file = fs.getPath("maven").resolve(library.getPath());
|
||||
if (Files.exists(file)) {
|
||||
Path dest = gameRepository.getLibraryFile(version, library).toPath();
|
||||
FileUtils.copyFile(file, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependents.add(new GameLibrariesTask(dependencyManager, version, profile.getLibraries()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path temp = Files.createTempDirectory("forge_installer");
|
||||
int finished = 0;
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
||||
List<ForgeNewInstallProfile.Processor> processors = profile.getProcessors();
|
||||
Map<String, String> data = profile.getData();
|
||||
|
||||
updateProgress(0, processors.size());
|
||||
|
||||
for (Map.Entry<String, String> entry : data.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
data.put(key, parseLiteral(value,
|
||||
Collections.emptyMap(),
|
||||
str -> {
|
||||
Path dest = temp.resolve(str);
|
||||
FileUtils.copyFile(fs.getPath(str), dest);
|
||||
return dest.toString();
|
||||
}));
|
||||
}
|
||||
|
||||
data.put("SIDE", "client");
|
||||
data.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
|
||||
|
||||
for (ForgeNewInstallProfile.Processor processor : processors) {
|
||||
Map<String, String> outputs = new HashMap<>();
|
||||
boolean miss = false;
|
||||
|
||||
for (Map.Entry<String, String> entry : processor.getOutputs().entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
key = parseLiteral(key, data, ExceptionalFunction.identity());
|
||||
value = parseLiteral(value, data, ExceptionalFunction.identity());
|
||||
|
||||
if (key == null || value == null) {
|
||||
throw new Exception("Invalid forge installation configuration");
|
||||
}
|
||||
|
||||
outputs.put(key, value);
|
||||
|
||||
Path artifact = Paths.get(key);
|
||||
if (Files.exists(artifact)) {
|
||||
String code;
|
||||
try (InputStream stream = Files.newInputStream(artifact)) {
|
||||
code = encodeHex(digest("SHA-1", stream));
|
||||
}
|
||||
|
||||
if (!Objects.equals(code, value)) {
|
||||
Files.delete(artifact);
|
||||
LOG.info("Found existing file is not valid: " + artifact);
|
||||
|
||||
miss = true;
|
||||
}
|
||||
} else {
|
||||
miss = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!processor.getOutputs().isEmpty() && !miss) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Path jar = gameRepository.getArtifactFile(version, processor.getJar());
|
||||
if (!Files.isRegularFile(jar))
|
||||
throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess");
|
||||
|
||||
String mainClass;
|
||||
try (JarFile jarFile = new JarFile(jar.toFile())) {
|
||||
mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(mainClass))
|
||||
throw new Exception("Game processor jar does not have main class " + jar);
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
|
||||
command.add("-cp");
|
||||
|
||||
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
|
||||
for (Artifact artifact : processor.getClasspath()) {
|
||||
Path file = gameRepository.getArtifactFile(version, artifact);
|
||||
if (!Files.isRegularFile(file))
|
||||
throw new Exception("Game processor dependency missing");
|
||||
classpath.add(file.toString());
|
||||
}
|
||||
classpath.add(jar.toString());
|
||||
command.add(String.join(OperatingSystem.PATH_SEPARATOR, classpath));
|
||||
|
||||
command.add(mainClass);
|
||||
|
||||
List<String> args = processor.getArgs().stream().map(arg -> {
|
||||
String parsed = parseLiteral(arg, data, ExceptionalFunction.identity());
|
||||
if (parsed == null)
|
||||
throw new IllegalStateException("Invalid forge installation configuration");
|
||||
return parsed;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
command.addAll(args);
|
||||
|
||||
LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString());
|
||||
Process process = new ProcessBuilder(command).start();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
for (String line; (line = reader.readLine()) != null;) {
|
||||
System.out.println(line);
|
||||
}
|
||||
}
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0)
|
||||
throw new IllegalStateException("Game processor exited abnormally");
|
||||
|
||||
for (Map.Entry<String, String> entry : outputs.entrySet()) {
|
||||
Path artifact = Paths.get(entry.getKey());
|
||||
if (!Files.isRegularFile(artifact))
|
||||
throw new FileNotFoundException("File missing: " + artifact);
|
||||
|
||||
String code;
|
||||
try (InputStream stream = Files.newInputStream(artifact)) {
|
||||
code = encodeHex(digest("SHA-1", stream));
|
||||
}
|
||||
|
||||
if (!Objects.equals(code, entry.getValue())) {
|
||||
Files.delete(artifact);
|
||||
throw new ChecksumMismatchException("SHA-1", entry.getValue(), code);
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(++finished, processors.size());
|
||||
}
|
||||
}
|
||||
|
||||
// resolve the version
|
||||
SimpleVersionProvider provider = new SimpleVersionProvider();
|
||||
provider.addVersion(version);
|
||||
|
||||
setResult(forgeVersion
|
||||
.setInheritsFrom(version.getId())
|
||||
.resolve(provider).setJar(null)
|
||||
.setId(version.getId()).setLogging(Collections.emptyMap()));
|
||||
|
||||
dependencies.add(dependencyManager.checkLibraryCompletionAsync(forgeVersion));
|
||||
|
||||
FileUtils.deleteDirectory(temp.toFile());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ForgeOldInstallTask extends TaskResult<Version> {
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final Version version;
|
||||
private final Path installer;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
public ForgeOldInstallTask(DefaultDependencyManager dependencyManager, Version version, Path installer) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.installer = installer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "version";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
try (ZipFile zipFile = new ZipFile(installer.toFile())) {
|
||||
InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json"));
|
||||
if (stream == null)
|
||||
throw new IOException("Malformed forge installer file, install_profile.json does not exist.");
|
||||
String json = IOUtils.readFullyAsString(stream);
|
||||
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class);
|
||||
|
||||
// unpack the universal jar in the installer file.
|
||||
Library forgeLibrary = Library.fromName(installProfile.getInstall().getPath());
|
||||
File forgeFile = dependencyManager.getGameRepository().getLibraryFile(version, forgeLibrary);
|
||||
if (!FileUtils.makeFile(forgeFile))
|
||||
throw new IOException("Cannot make directory " + forgeFile.getParent());
|
||||
|
||||
ZipEntry forgeEntry = zipFile.getEntry(installProfile.getInstall().getFilePath());
|
||||
try (InputStream is = zipFile.getInputStream(forgeEntry); OutputStream os = new FileOutputStream(forgeFile)) {
|
||||
IOUtils.copyTo(is, os);
|
||||
}
|
||||
|
||||
// resolve the version
|
||||
SimpleVersionProvider provider = new SimpleVersionProvider();
|
||||
provider.addVersion(version);
|
||||
|
||||
setResult(installProfile.getVersionInfo()
|
||||
.setInheritsFrom(version.getId())
|
||||
.resolve(provider).setJar(null)
|
||||
.setId(version.getId()).setLogging(Collections.emptyMap()));
|
||||
|
||||
dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,12 +47,11 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
@Override
|
||||
public Task refreshAsync(DownloadProvider downloadProvider) {
|
||||
final GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(FORGE_LIST)));
|
||||
final List<Task> dependents = Collections.singletonList(task);
|
||||
return new Task() {
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependents() {
|
||||
return dependents;
|
||||
return Collections.singleton(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -53,13 +53,13 @@ public final class GameAssetDownloadTask extends Task {
|
||||
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
|
||||
* @param version the <b>resolved</b> version
|
||||
*/
|
||||
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version) {
|
||||
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version, boolean forceDownloadingIndex) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.assetIndexInfo = version.getAssetIndex();
|
||||
this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
|
||||
|
||||
if (!assetIndexFile.exists())
|
||||
if (!assetIndexFile.exists() || forceDownloadingIndex)
|
||||
dependents.add(new GameAssetIndexDownloadTask(dependencyManager, version));
|
||||
}
|
||||
|
||||
@@ -99,5 +99,7 @@ public final class GameAssetDownloadTask extends Task {
|
||||
updateProgress(++progress, index.getObjects().size());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static final boolean DOWNLOAD_INDEX_FORCIBLY = true;
|
||||
public static final boolean DOWNLOAD_INDEX_IF_NECESSARY = false;
|
||||
}
|
||||
@@ -55,14 +55,18 @@ public final class GameDownloadTask extends Task {
|
||||
@Override
|
||||
public void execute() {
|
||||
File jar = dependencyManager.getGameRepository().getVersionJar(version);
|
||||
|
||||
dependencies.add(new FileDownloadTask(
|
||||
|
||||
FileDownloadTask task = new FileDownloadTask(
|
||||
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
|
||||
jar,
|
||||
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()))
|
||||
.setCaching(true)
|
||||
.setCacheRepository(dependencyManager.getCacheRepository())
|
||||
.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory().resolve("jars").resolve(gameVersion + ".jar")));
|
||||
.setCacheRepository(dependencyManager.getCacheRepository());
|
||||
|
||||
if (gameVersion != null)
|
||||
task.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory().resolve("jars").resolve(gameVersion + ".jar"));
|
||||
|
||||
dependencies.add(task);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public final class GameLibrariesTask extends Task {
|
||||
|
||||
private final AbstractDependencyManager dependencyManager;
|
||||
private final Version version;
|
||||
private final List<Library> libraries;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
/**
|
||||
@@ -45,8 +46,20 @@ public final class GameLibrariesTask extends Task {
|
||||
* @param version the <b>resolved</b> version
|
||||
*/
|
||||
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) {
|
||||
this(dependencyManager, version, version.getLibraries());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
|
||||
* @param version the <b>resolved</b> version
|
||||
*/
|
||||
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, List<Library> libraries) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.libraries = libraries;
|
||||
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@@ -57,7 +70,7 @@ public final class GameLibrariesTask extends Task {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
version.getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||
libraries.stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
|
||||
if (!file.exists())
|
||||
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download.game;
|
||||
import org.jackhuang.hmcl.download.AbstractDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DefaultCacheRepository;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -88,10 +89,10 @@ public class LibraryDownloadTask extends Task {
|
||||
if (cached) return;
|
||||
|
||||
if (!isDependentsSucceeded()) {
|
||||
// Since FileDownloadTask wraps the actual exception with another IOException.
|
||||
// Since FileDownloadTask wraps the actual exception with DownloadException.
|
||||
// We should extract it letting the error message clearer.
|
||||
Throwable t = task.getLastException();
|
||||
if (t.getCause() != null)
|
||||
if (t instanceof DownloadException)
|
||||
throw new LibraryDownloadException(library, t.getCause());
|
||||
else
|
||||
throw new LibraryDownloadException(library, t);
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Immutable
|
||||
@JsonAdapter(Artifact.Serializer.class)
|
||||
public final class Artifact {
|
||||
|
||||
private final String group;
|
||||
private final String name;
|
||||
private final String version;
|
||||
private final String classifier;
|
||||
private final String extension;
|
||||
|
||||
private final String descriptor;
|
||||
private final String fileName;
|
||||
private final String path;
|
||||
|
||||
public Artifact(String descriptor) {
|
||||
this.descriptor = descriptor;
|
||||
|
||||
String[] arr = descriptor.split(":", 4);
|
||||
if (arr.length != 3 && arr.length != 4)
|
||||
throw new IllegalArgumentException("Artifact name is malformed");
|
||||
|
||||
String ext = null;
|
||||
int last = arr.length - 1;
|
||||
String[] splitted = arr[last].split("@");
|
||||
if (splitted.length == 2) {
|
||||
arr[last] = splitted[0];
|
||||
ext = splitted[1];
|
||||
} else if (splitted.length > 2) {
|
||||
throw new IllegalArgumentException("Artifact name is malformed");
|
||||
}
|
||||
|
||||
this.group = arr[0].replace("\\", "/");
|
||||
this.name = arr[1];
|
||||
this.version = arr[2];
|
||||
this.classifier = arr.length >= 4 ? arr[3] : null;
|
||||
this.extension = ext == null ? "jar" : ext;
|
||||
|
||||
String fileName = this.name + "-" + this.version;
|
||||
if (classifier != null) fileName += "-" + this.classifier;
|
||||
this.fileName = fileName + "." + this.extension;
|
||||
this.path = String.format("%s/%s/%s/%s", this.group.replace(".", "/"), this.name, this.version, this.fileName);
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getClassifier() {
|
||||
return classifier;
|
||||
}
|
||||
|
||||
public String getExtension() {
|
||||
return extension;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public Path getPath(Path root) {
|
||||
return root.resolve(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonDeserializer<Artifact>, JsonSerializer<Artifact> {
|
||||
@Override
|
||||
public JsonElement serialize(Artifact src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Artifact deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return json.isJsonPrimitive() ? new Artifact(json.getAsJsonPrimitive().getAsString()) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,18 +20,19 @@ package org.jackhuang.hmcl.game;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.event.*;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
/**
|
||||
* An implementation of classic Minecraft game repository.
|
||||
*
|
||||
@@ -81,6 +82,10 @@ public class DefaultGameRepository implements GameRepository {
|
||||
return new File(getBaseDirectory(), "libraries/" + lib.getPath());
|
||||
}
|
||||
|
||||
public Path getArtifactFile(Version version, Artifact artifact) {
|
||||
return artifact.getPath(getBaseDirectory().toPath().resolve("libraries"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getRunDirectory(String id) {
|
||||
return getBaseDirectory();
|
||||
@@ -112,7 +117,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
}
|
||||
|
||||
public Version readVersionJson(File file) throws IOException, JsonParseException {
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), Version.class);
|
||||
return JsonUtils.fromNonNullJson(FileUtils.readText(file), Version.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,13 +176,13 @@ public class DefaultGameRepository implements GameRepository {
|
||||
List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json");
|
||||
jsons.forEach(f -> {
|
||||
if (!f.delete())
|
||||
Logging.LOG.warning("Unable to delete file " + f);
|
||||
LOG.warning("Unable to delete file " + f);
|
||||
});
|
||||
// remove the version from version list regardless of whether the directory was removed successfully or not.
|
||||
try {
|
||||
FileUtils.deleteDirectory(removedFile);
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e);
|
||||
LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -202,25 +207,31 @@ public class DefaultGameRepository implements GameRepository {
|
||||
// we will find the only json and rename it to correct name.
|
||||
if (!json.exists()) {
|
||||
List<File> jsons = FileUtils.listFilesByExtension(dir, "json");
|
||||
if (jsons.size() == 1)
|
||||
if (jsons.size() == 1) {
|
||||
LOG.info("Renaming json file " + jsons.get(0) + " to " + json);
|
||||
if (!jsons.get(0).renameTo(json)) {
|
||||
Logging.LOG.warning("Cannot rename json file " + jsons.get(0) + " to " + json + ", ignoring version " + id);
|
||||
LOG.warning("Cannot rename json file, ignoring version " + id);
|
||||
return Stream.empty();
|
||||
}
|
||||
} else {
|
||||
LOG.info("No available json file found, ignoring version " + id);
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
|
||||
Version version;
|
||||
try {
|
||||
version = Objects.requireNonNull(readVersionJson(json));
|
||||
version = readVersionJson(json);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Malformed version json " + id, e);
|
||||
// JsonSyntaxException or IOException or NullPointerException(!!)
|
||||
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
|
||||
return Stream.empty();
|
||||
|
||||
try {
|
||||
version = Objects.requireNonNull(readVersionJson(json));
|
||||
version = readVersionJson(json);
|
||||
} catch (Exception e2) {
|
||||
Logging.LOG.log(Level.SEVERE, "User corrected version json is still malformed");
|
||||
LOG.log(Level.SEVERE, "User corrected version json is still malformed", e2);
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
@@ -230,7 +241,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
try {
|
||||
FileUtils.writeText(json, JsonUtils.GSON.toJson(version));
|
||||
} catch (Exception e) {
|
||||
Logging.LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it.");
|
||||
LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it.", e);
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
@@ -246,7 +257,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, resolved)) != Event.Result.DENY)
|
||||
versions.put(version.getId(), version);
|
||||
} catch (VersionNotFoundException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Ignoring version " + version.getId() + " because it inherits from a nonexistent version.");
|
||||
LOG.log(Level.WARNING, "Ignoring version " + version.getId() + " because it inherits from a nonexistent version.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,10 +269,8 @@ public class DefaultGameRepository implements GameRepository {
|
||||
if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY)
|
||||
return;
|
||||
|
||||
Schedulers.newThread().schedule(() -> {
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
});
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -278,7 +287,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
try {
|
||||
return reconstructAssets(version, assetId);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e);
|
||||
LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e);
|
||||
return getAssetDirectory(version, assetId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
/**
|
||||
@@ -74,8 +75,8 @@ public class DownloadInfo implements Validation {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (StringUtils.isBlank(url))
|
||||
throw new JsonParseException("DownloadInfo url can not be null");
|
||||
throw new TolerableValidationException();
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPool;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantType;
|
||||
@@ -30,6 +36,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
@@ -37,6 +44,16 @@ import java.util.stream.StreamSupport;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class GameVersion {
|
||||
private static Optional<String> getVersionFromJson(Path versionJson) {
|
||||
try {
|
||||
MinecraftVersion version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), MinecraftVersion.class);
|
||||
return Optional.ofNullable(version.name);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse version.json", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException {
|
||||
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||
|
||||
@@ -74,6 +91,13 @@ public final class GameVersion {
|
||||
return Optional.empty();
|
||||
|
||||
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
|
||||
Path versionJson = gameJar.getPath("version.json");
|
||||
if (Files.exists(versionJson)) {
|
||||
Optional<String> result = getVersionFromJson(versionJson);
|
||||
if (result.isPresent())
|
||||
return result;
|
||||
}
|
||||
|
||||
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
||||
if (Files.exists(minecraft)) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
||||
@@ -88,4 +112,24 @@ public final class GameVersion {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MinecraftVersion {
|
||||
public String name;
|
||||
|
||||
@SerializedName("release_target")
|
||||
public String releaseTarget;
|
||||
|
||||
public String id;
|
||||
|
||||
public boolean stable;
|
||||
|
||||
@SerializedName("world_version")
|
||||
public int worldVersion;
|
||||
|
||||
@SerializedName("protocol_version")
|
||||
public int protocolVersion;
|
||||
|
||||
@SerializedName("pack_version")
|
||||
public int packVersion;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -54,7 +55,7 @@ public class IdDownloadInfo extends DownloadInfo {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
super.validate();
|
||||
|
||||
if (StringUtils.isBlank(id))
|
||||
|
||||
@@ -45,13 +45,13 @@ public class Library implements Comparable<Library> {
|
||||
private final String classifier;
|
||||
private final String url;
|
||||
private final LibrariesDownloadInfo downloads;
|
||||
private final LibraryDownloadInfo download;
|
||||
private transient final LibraryDownloadInfo download;
|
||||
private final ExtractRules extract;
|
||||
private final Map<OperatingSystem, String> natives;
|
||||
private final List<CompatibilityRule> rules;
|
||||
private final List<String> checksums;
|
||||
|
||||
private final String path;
|
||||
private transient final String path;
|
||||
|
||||
public Library(String groupId, String artifactId, String version) {
|
||||
this(groupId, artifactId, version, null, null, null);
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
/**
|
||||
@@ -66,7 +67,7 @@ public final class LoggingInfo implements Validation {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
file.validate();
|
||||
if (StringUtils.isBlank(argument))
|
||||
throw new JsonParseException("LoggingInfo.argument is empty.");
|
||||
|
||||
@@ -124,7 +124,6 @@ public final class ModManager {
|
||||
for (ModInfo modInfo : modInfos) {
|
||||
Files.deleteIfExists(modInfo.getFile());
|
||||
}
|
||||
refreshMods();
|
||||
}
|
||||
|
||||
public Path disableMod(Path file) throws IOException {
|
||||
|
||||
@@ -40,7 +40,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -133,7 +132,7 @@ public final class MultiMCModpackInstallTask extends Task {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Version version = Objects.requireNonNull(repository.readVersionJson(name));
|
||||
Version version = repository.readVersionJson(name);
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
|
||||
|
||||
@@ -20,12 +20,14 @@ package org.jackhuang.hmcl.task;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
public class DownloadException extends IOException {
|
||||
|
||||
private final URL url;
|
||||
|
||||
public DownloadException(URL url, Throwable cause) {
|
||||
super("Unable to download " + url + ", " + cause.getMessage(), cause);
|
||||
super("Unable to download " + url + ", " + cause.getMessage(), requireNonNull(cause));
|
||||
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
package org.jackhuang.hmcl.task;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
@@ -55,18 +58,14 @@ public abstract class Task {
|
||||
this.significance = significance;
|
||||
}
|
||||
|
||||
private ReadOnlyObjectWrapper<TaskState> state = new ReadOnlyObjectWrapper<>(this, "state", TaskState.READY);
|
||||
private TaskState state = TaskState.READY;
|
||||
|
||||
public TaskState getState() {
|
||||
return state.get();
|
||||
return state;
|
||||
}
|
||||
|
||||
void setState(TaskState state) {
|
||||
this.state.setValue(state);
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<TaskState> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
private Throwable lastException = null;
|
||||
@@ -340,10 +339,6 @@ public abstract class Task {
|
||||
});
|
||||
}
|
||||
|
||||
public static Task empty() {
|
||||
return of(ExceptionalConsumer.empty());
|
||||
}
|
||||
|
||||
public static Task of(String name, ExceptionalRunnable<?> runnable) {
|
||||
return of(name, ExceptionalConsumer.fromRunnable(runnable));
|
||||
}
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -203,14 +204,6 @@ public final class Lang {
|
||||
}
|
||||
}
|
||||
|
||||
public static Double toDoubleOrNull(Object string) {
|
||||
try {
|
||||
return Double.parseDouble(string.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first non-null reference in given list.
|
||||
* @param t nullable references list.
|
||||
|
||||
@@ -88,4 +88,11 @@ public final class SimpleMultimap<K, V> {
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
public void clear(K key) {
|
||||
if (map.containsKey(key))
|
||||
map.get(key).clear();
|
||||
else
|
||||
map.put(key, valuer.get());
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Collection;
|
||||
@@ -24,8 +26,6 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@@ -128,6 +128,10 @@ public final class StringUtils {
|
||||
return index == -1 ? missingDelimiterValue : str.substring(index + delimiter.length());
|
||||
}
|
||||
|
||||
public static boolean isSurrounded(String str, String prefix, String suffix) {
|
||||
return str.startsWith(prefix) && str.endsWith(suffix);
|
||||
}
|
||||
|
||||
public static String removeSurrounding(String str, String delimiter) {
|
||||
return removeSurrounding(str, delimiter, delimiter);
|
||||
}
|
||||
|
||||
@@ -23,4 +23,8 @@ package org.jackhuang.hmcl.util.function;
|
||||
*/
|
||||
public interface ExceptionalFunction<T, R, E extends Exception> {
|
||||
R apply(T t) throws E;
|
||||
|
||||
static <T, E extends RuntimeException> ExceptionalFunction<T, T, E> identity() {
|
||||
return t -> t;
|
||||
}
|
||||
}
|
||||
+6
-15
@@ -15,24 +15,15 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
package org.jackhuang.hmcl.util.gson;
|
||||
|
||||
/**
|
||||
* What's circle dependency?
|
||||
* When C inherits from B, and B inherits from something else, and finally inherits from C again.
|
||||
*
|
||||
* @author huangyuhui
|
||||
* This exception gets thrown by implementations of {@link Validation#validate()} if you want to replace
|
||||
* the nullable JSON-parsed object which does not satisfy the constraint with null value.
|
||||
* @see Validation
|
||||
*/
|
||||
public final class CircleDependencyException extends GameException {
|
||||
public final class TolerableValidationException extends Exception {
|
||||
|
||||
public CircleDependencyException() {
|
||||
}
|
||||
|
||||
public CircleDependencyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CircleDependencyException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
public TolerableValidationException() {
|
||||
}
|
||||
}
|
||||
@@ -36,5 +36,5 @@ public interface Validation {
|
||||
*
|
||||
* @throws JsonParseException if fields are filled in wrong format or wrong type.
|
||||
*/
|
||||
void validate() throws JsonParseException;
|
||||
void validate() throws JsonParseException, TolerableValidationException;
|
||||
}
|
||||
+15
-4
@@ -40,8 +40,14 @@ public final class ValidationTypeAdapterFactory implements TypeAdapterFactory {
|
||||
return new TypeAdapter<T>() {
|
||||
@Override
|
||||
public void write(JsonWriter writer, T t) throws IOException {
|
||||
if (t instanceof Validation)
|
||||
((Validation) t).validate();
|
||||
if (t instanceof Validation) {
|
||||
try {
|
||||
((Validation) t).validate();
|
||||
} catch (TolerableValidationException e) {
|
||||
delegate.write(writer, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
delegate.write(writer, t);
|
||||
}
|
||||
@@ -49,8 +55,13 @@ public final class ValidationTypeAdapterFactory implements TypeAdapterFactory {
|
||||
@Override
|
||||
public T read(JsonReader reader) throws IOException {
|
||||
T t = delegate.read(reader);
|
||||
if (t instanceof Validation)
|
||||
((Validation) t).validate();
|
||||
if (t instanceof Validation) {
|
||||
try {
|
||||
((Validation) t).validate();
|
||||
} catch (TolerableValidationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -275,6 +275,22 @@ public final class FileUtils {
|
||||
Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public static void copyFile(Path srcFile, Path destFile)
|
||||
throws IOException {
|
||||
Objects.requireNonNull(srcFile, "Source must not be null");
|
||||
Objects.requireNonNull(destFile, "Destination must not be null");
|
||||
if (!Files.exists(srcFile))
|
||||
throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
|
||||
if (Files.isDirectory(srcFile))
|
||||
throw new IOException("Source '" + srcFile + "' exists but is a directory");
|
||||
Path parentFile = destFile.getParent();
|
||||
Files.createDirectories(parentFile);
|
||||
if (Files.exists(destFile) && !Files.isWritable(destFile))
|
||||
throw new IOException("Destination '" + destFile + "' exists but is read-only");
|
||||
|
||||
Files.copy(srcFile, destFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public static void moveFile(File srcFile, File destFile) throws IOException {
|
||||
copyFile(srcFile, destFile);
|
||||
srcFile.delete();
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.*;
|
||||
/**
|
||||
* Copied from org.apache.maven.artifact.versioning.ComparableVersion
|
||||
* Apache License 2.0
|
||||
* @see <a href="http://maven.apache.org/pom.html#Version_Order_Specification">Specification</a>
|
||||
*/
|
||||
public class VersionNumber implements Comparable<VersionNumber> {
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -23,7 +23,8 @@
|
||||
<ProjectGuid>{672B1019-E741-4C0D-A986-627E2ACE157B}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>HMCL</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
|
||||
<WindowsTargetPlatformVersion>7.0</WindowsTargetPlatformVersion>
|
||||
<ProjectName>HMCLauncher</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
@@ -94,6 +95,7 @@
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>version.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
@@ -129,6 +131,7 @@
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>version.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
@@ -152,19 +155,26 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="HMCL.h" />
|
||||
<ClInclude Include="java.h" />
|
||||
<ClInclude Include="lang.h" />
|
||||
<ClInclude Include="main.h" />
|
||||
<ClInclude Include="os.h" />
|
||||
<ClInclude Include="Resource.h" />
|
||||
<ClInclude Include="stdafx.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="Version.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="HMCL.cpp" />
|
||||
<ClCompile Include="java.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
<ClCompile Include="os.cpp" />
|
||||
<ClCompile Include="stdafx.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Version.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="HMCL.rc" />
|
||||
|
||||
@@ -24,7 +24,19 @@
|
||||
<ClInclude Include="Resource.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="HMCL.h">
|
||||
<ClInclude Include="main.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Version.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="java.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="os.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="lang.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
@@ -32,7 +44,16 @@
|
||||
<ClCompile Include="stdafx.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HMCL.cpp">
|
||||
<ClCompile Include="main.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Version.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="java.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="os.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "stdafx.h"
|
||||
#include "Version.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
Version::Version(const wstring & rawString)
|
||||
{
|
||||
int idx = 0;
|
||||
ver[0] = ver[1] = ver[2] = ver[3] = 0;
|
||||
for (auto &i : rawString)
|
||||
{
|
||||
if (idx >= 4) break;
|
||||
if (i == '.') ++idx;
|
||||
else if (i == '_') ++idx;
|
||||
else if (isdigit(i)) ver[idx] = ver[idx] * 10 + (i - L'0');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
class Version
|
||||
{
|
||||
public:
|
||||
int ver[4];
|
||||
|
||||
Version(const std::wstring &rawString);
|
||||
|
||||
template <typename T>
|
||||
Version(std::initializer_list<T> ver_list)
|
||||
{
|
||||
int i = 0;
|
||||
for (const auto &data : ver_list)
|
||||
{
|
||||
if (i >= 4)
|
||||
break;
|
||||
ver[i++] = data;
|
||||
}
|
||||
}
|
||||
|
||||
bool operator<(const Version &other) const
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (ver[i] != other.ver[i])
|
||||
return ver[i] < other.ver[i];
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator<=(const Version &other) const
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (ver[i] != other.ver[i])
|
||||
return ver[i] < other.ver[i];
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "stdafx.h"
|
||||
#include "java.h"
|
||||
#include "os.h"
|
||||
|
||||
const Version JAVA_8(L"1.8"), JAVA_11(L"11");
|
||||
|
||||
const LPCWSTR JDK_OLD = L"SOFTWARE\\JavaSoft\\Java Development Kit";
|
||||
const LPCWSTR JRE_OLD = L"SOFTWARE\\JavaSoft\\Java Runtime Environment";
|
||||
const LPCWSTR JDK_NEW = L"SOFTWARE\\JavaSoft\\JDK";
|
||||
const LPCWSTR JRE_NEW = L"SOFTWARE\\JavaSoft\\JRE";
|
||||
|
||||
bool oldJavaFound = false, newJavaFound = false;
|
||||
|
||||
bool FindJavaByRegistryKey(HKEY rootKey, LPCWSTR subKey, std::wstring & path)
|
||||
{
|
||||
WCHAR javaVer[MAX_KEY_LENGTH]; // buffer for subkey name, special for JavaVersion
|
||||
DWORD cbName; // size of name string
|
||||
DWORD cSubKeys = 0; // number of subkeys
|
||||
DWORD cbMaxSubKey; // longest subkey size
|
||||
DWORD cValues; // number of values for key
|
||||
DWORD cchMaxValue; // longest value name
|
||||
DWORD cbMaxValueData; // longest value data
|
||||
LSTATUS result;
|
||||
|
||||
HKEY hKey;
|
||||
if (ERROR_SUCCESS != (result = RegOpenKeyEx(rootKey, subKey, 0, KEY_WOW64_64KEY | KEY_READ, &hKey)))
|
||||
return false;
|
||||
|
||||
RegQueryInfoKey(
|
||||
hKey, // key handle
|
||||
NULL, // buffer for class name
|
||||
NULL, // size of class string
|
||||
NULL, // reserved
|
||||
&cSubKeys, // number of subkeys
|
||||
&cbMaxSubKey, // longest subkey size
|
||||
NULL, // longest class string
|
||||
&cValues, // number of values for this key
|
||||
&cchMaxValue, // longest value name
|
||||
&cbMaxValueData, // longest value data
|
||||
NULL, // security descriptor
|
||||
NULL); // last write time
|
||||
|
||||
if (!cSubKeys)
|
||||
return false;
|
||||
|
||||
bool flag = false;
|
||||
for (DWORD i = 0; i < cSubKeys; ++i)
|
||||
{
|
||||
cbName = MAX_KEY_LENGTH;
|
||||
if (ERROR_SUCCESS != (result = RegEnumKeyEx(hKey, i, javaVer, &cbName, NULL, NULL, NULL, NULL)))
|
||||
continue;
|
||||
|
||||
HKEY javaKey;
|
||||
if (ERROR_SUCCESS != RegOpenKeyEx(hKey, javaVer, 0, KEY_READ, &javaKey))
|
||||
continue;
|
||||
|
||||
if (ERROR_SUCCESS == MyRegQueryValue(javaKey, L"JavaHome", REG_SZ, path))
|
||||
{
|
||||
if (Version(javaVer) < JAVA_8)
|
||||
oldJavaFound = true;
|
||||
else if (JAVA_11 <= Version(javaVer))
|
||||
newJavaFound = true;
|
||||
else
|
||||
flag = true;
|
||||
}
|
||||
|
||||
if (flag)
|
||||
break;
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
|
||||
return flag;
|
||||
}
|
||||
|
||||
bool FindJavaInRegistry(std::wstring & path)
|
||||
{
|
||||
return FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JDK_OLD, path) ||
|
||||
FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JRE_OLD, path) ||
|
||||
FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JDK_NEW, path) ||
|
||||
FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JRE_NEW, path);
|
||||
}
|
||||
|
||||
bool FindJava(std::wstring & path)
|
||||
{
|
||||
return FindJavaInRegistry(path) ||
|
||||
ERROR_SUCCESS == MyGetEnvironmentVariable(L"JAVA_HOME", path);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include "Version.h"
|
||||
|
||||
// Find Java installation in system registry
|
||||
bool FindJavaInRegistry(std::wstring &path);
|
||||
|
||||
// Find Java Installation in registry and environment variable
|
||||
bool FindJava(std::wstring &path);
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#define ERROR_PROMPT L"Java installation cannot be found in this computer, please download it from https://java.com \n" \
|
||||
L"未能在这台电脑上找到Java 8~Java 10,请从 https://java.com 下载安装Java"
|
||||
@@ -0,0 +1,92 @@
|
||||
#include "stdafx.h"
|
||||
#include "main.h"
|
||||
#include "os.h"
|
||||
#include "java.h"
|
||||
#include "lang.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
Version J8(TEXT("8")), J11(TEXT("11"));
|
||||
|
||||
void RawLaunchJVM(const wstring &javaPath, const wstring &jarPath)
|
||||
{
|
||||
if (MyCreateProcess(L"\"" + javaPath + L"\" -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=15 -jar \"" + jarPath + L"\""))
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
void LaunchJVM(const wstring &javaPath, const wstring &jarPath)
|
||||
{
|
||||
Version javaVersion(L"");
|
||||
if (!MyGetFileVersionInfo(javaPath, javaVersion))
|
||||
return;
|
||||
|
||||
if (J8 <= javaVersion && javaVersion < J11)
|
||||
{
|
||||
RawLaunchJVM(javaPath, jarPath);
|
||||
}
|
||||
}
|
||||
|
||||
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
|
||||
{
|
||||
wstring path, exeName;
|
||||
|
||||
// Since Jar file is appended to this executable, we should first get the location of JAR file.
|
||||
if (ERROR_SUCCESS != MyGetModuleFileName(NULL, exeName))
|
||||
return 1;
|
||||
|
||||
// TODO: check whether the bundled JRE is valid.
|
||||
// First try the Java packaged together.
|
||||
bool is64Bit = false;
|
||||
GetArch(is64Bit); // if failed to determine architecture of operating system, consider 32-bit.
|
||||
|
||||
if (is64Bit)
|
||||
{
|
||||
LaunchJVM(L"jre-x64\\bin\\javaw.exe", exeName);
|
||||
}
|
||||
else
|
||||
{
|
||||
LaunchJVM(L"jre-x86\\bin\\javaw.exe", exeName);
|
||||
}
|
||||
|
||||
if (FindJava(path))
|
||||
LaunchJVM(path + L"\\bin\\javaw.exe", exeName);
|
||||
|
||||
// Or we try to search Java in C:\Program Files.
|
||||
{
|
||||
WIN32_FIND_DATA data;
|
||||
HANDLE hFind = FindFirstFile(L"C:\\Program Files\\Java\\*", &data); // Search all subdirectory
|
||||
|
||||
if (hFind != INVALID_HANDLE_VALUE) {
|
||||
do {
|
||||
wstring javaw = wstring(L"C:\\Program Files\\Java\\") + data.cFileName + wstring(L"\\bin\\javaw.exe");
|
||||
if (FindFirstFileExists(javaw.c_str(), 0)) {
|
||||
LaunchJVM(javaw, exeName);
|
||||
}
|
||||
} while (FindNextFile(hFind, &data));
|
||||
FindClose(hFind);
|
||||
}
|
||||
}
|
||||
|
||||
// Consider C:\Program Files (x86)\Java
|
||||
{
|
||||
WIN32_FIND_DATA data;
|
||||
HANDLE hFind = FindFirstFile(L"C:\\Program Files (x86)\\Java\\*", &data); // Search all subdirectory
|
||||
|
||||
if (hFind != INVALID_HANDLE_VALUE) {
|
||||
do {
|
||||
wstring javaw = wstring(L"C:\\Program Files (x86)\\Java\\") + data.cFileName + L"\\bin\\javaw.exe";
|
||||
if (FindFirstFileExists(javaw.c_str(), 0)) {
|
||||
LaunchJVM(javaw, exeName);
|
||||
}
|
||||
} while (FindNextFile(hFind, &data));
|
||||
FindClose(hFind);
|
||||
}
|
||||
}
|
||||
|
||||
// Try java in PATH
|
||||
RawLaunchJVM(L"javaw", exeName);
|
||||
|
||||
MessageBox(NULL, ERROR_PROMPT, L"Error", MB_ICONERROR | MB_OK);
|
||||
ShellExecute(0, 0, L"https://java.com/", 0, 0, SW_SHOW);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include "resource.h"
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "stdafx.h"
|
||||
#include "os.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
LSTATUS MyRegQueryValue(HKEY hKey, LPCWSTR subKey, DWORD dwType, wstring &out)
|
||||
{
|
||||
DWORD dwSize = 0;
|
||||
LSTATUS ret = RegQueryValueEx(hKey, subKey, 0, &dwType, NULL, &dwSize);
|
||||
if (ret != ERROR_SUCCESS) return ret;
|
||||
WCHAR *buffer = new WCHAR[dwSize];
|
||||
ret = RegQueryValueEx(hKey, subKey, 0, &dwType, (LPBYTE)buffer, &dwSize);
|
||||
if (ret != ERROR_SUCCESS) return ret;
|
||||
out = buffer;
|
||||
delete[] buffer;
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
LSTATUS MyGetModuleFileName(HMODULE hModule, std::wstring &out)
|
||||
{
|
||||
DWORD res, size = MAX_PATH;
|
||||
out = wstring();
|
||||
out.resize(size);
|
||||
while ((res = GetModuleFileName(hModule, &out[0], size)) == size)
|
||||
{
|
||||
out.resize(size += MAX_PATH);
|
||||
}
|
||||
if (res == 0)
|
||||
return GetLastError();
|
||||
else
|
||||
{
|
||||
out.resize(size - MAX_PATH + res);
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
LSTATUS MyGetEnvironmentVariable(LPCWSTR name, std::wstring & out)
|
||||
{
|
||||
DWORD res, size = MAX_PATH;
|
||||
out = wstring();
|
||||
out.resize(size);
|
||||
while ((res = GetEnvironmentVariable(name, &out[0], size)) == size)
|
||||
{
|
||||
out.resize(size += MAX_PATH);
|
||||
}
|
||||
if (res == 0)
|
||||
return GetLastError();
|
||||
else
|
||||
{
|
||||
out.resize(size - MAX_PATH + res);
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
bool MyCreateProcess(const std::wstring &command)
|
||||
{
|
||||
wstring writable_command = command;
|
||||
STARTUPINFO si;
|
||||
PROCESS_INFORMATION pi;
|
||||
si.cb = sizeof(si);
|
||||
ZeroMemory(&si, sizeof(si));
|
||||
ZeroMemory(&pi, sizeof(pi));
|
||||
|
||||
return (CreateProcess(NULL, &writable_command[0], NULL, NULL, false, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi));
|
||||
}
|
||||
|
||||
bool FindFirstFileExists(LPCWSTR lpPath, DWORD dwFilter)
|
||||
{
|
||||
WIN32_FIND_DATA fd;
|
||||
HANDLE hFind = FindFirstFile(lpPath, &fd);
|
||||
bool bFilter = (false == dwFilter) ? true : fd.dwFileAttributes & dwFilter;
|
||||
bool ret = ((hFind != INVALID_HANDLE_VALUE) && bFilter) ? true : false;
|
||||
FindClose(hFind);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool GetArch(bool & is64Bit)
|
||||
{
|
||||
#if _WIN64
|
||||
isWindows64bit = true;
|
||||
return true;
|
||||
#elif _WIN32
|
||||
typedef BOOL(WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL);
|
||||
|
||||
BOOL isWow64 = FALSE;
|
||||
|
||||
// IsWow64Process is not available on all supported versions of Windows.
|
||||
// Use GetModuleHandle to get a handle to the DLL that contains the function
|
||||
// and GetProcAddress to get a pointer to the function if available.
|
||||
|
||||
LPFN_ISWOW64PROCESS fnIsWow64Process = (LPFN_ISWOW64PROCESS)
|
||||
GetProcAddress(GetModuleHandle(TEXT("kernel32")), "IsWow64Process");
|
||||
|
||||
if (fnIsWow64Process)
|
||||
{
|
||||
if (!fnIsWow64Process(GetCurrentProcess(), &isWow64))
|
||||
return false;
|
||||
|
||||
is64Bit = isWow64;
|
||||
return true;
|
||||
}
|
||||
else // IsWow64Process is not supported, fail to detect.
|
||||
return false;
|
||||
|
||||
#else
|
||||
#error _WIN64 and _WIN32 are both undefined.
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MyGetFileVersionInfo(const std::wstring & filePath, Version &version)
|
||||
{
|
||||
DWORD verHandle = 0;
|
||||
UINT size = 0;
|
||||
LPBYTE lpBuffer = NULL;
|
||||
VS_FIXEDFILEINFO *pFileInfo;
|
||||
DWORD dwSize = GetFileVersionInfoSize(filePath.c_str(), NULL);
|
||||
|
||||
if (!dwSize)
|
||||
return false;
|
||||
|
||||
LPBYTE data = new BYTE[dwSize];
|
||||
if (!GetFileVersionInfo(filePath.c_str(), 0, dwSize, data))
|
||||
{
|
||||
delete[] data;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VerQueryValue(data, TEXT("\\"), (LPVOID*)&pFileInfo, &size))
|
||||
{
|
||||
delete[] data;
|
||||
return false;
|
||||
}
|
||||
|
||||
version = Version{
|
||||
(pFileInfo->dwFileVersionMS >> 16) & 0xFFFF,
|
||||
(pFileInfo->dwFileVersionMS >> 0) & 0xFFFF,
|
||||
(pFileInfo->dwFileVersionLS >> 16) & 0xFFFF,
|
||||
(pFileInfo->dwFileVersionLS >> 0) & 0xFFFF
|
||||
};
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
#include "Version.h"
|
||||
|
||||
const int MAX_KEY_LENGTH = 255;
|
||||
const int MAX_VALUE_NAME = 16383;
|
||||
|
||||
// Query registry value of class root hKey, key path subKey, stores result in parameter out.
|
||||
LSTATUS MyRegQueryValue(HKEY hKey, LPCWSTR subKey, DWORD dwType, std::wstring &out);
|
||||
|
||||
// Get module file name, stores result in parameter out.
|
||||
LSTATUS MyGetModuleFileName(HMODULE hModule, std::wstring &out);
|
||||
|
||||
// Get environment variable by name, C++ style, stores the value in parameter out.
|
||||
LSTATUS MyGetEnvironmentVariable(LPCWSTR name, std::wstring &out);
|
||||
|
||||
// Create process by invoking CreateProcess, only pass command.
|
||||
bool MyCreateProcess(const std::wstring &command);
|
||||
|
||||
// Check if file lpPath exists.
|
||||
bool FindFirstFileExists(LPCWSTR lpPath, DWORD dwFilter);
|
||||
|
||||
bool GetArch(bool &is64Bit);
|
||||
|
||||
bool MyGetFileVersionInfo(const std::wstring &filePath, Version &version);
|
||||
Binary file not shown.
Reference in new issue
Block a user