Compare commits

..
16 Commits
68 changed files with 1083 additions and 363 deletions

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;
@@ -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;
@@ -37,6 +37,7 @@ 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();
@@ -69,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());
@@ -77,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());
@@ -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 <[email protected]> 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;
@@ -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);
}
}
}
@@ -18,21 +18,18 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.CurseCompletionException;
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;
@@ -112,12 +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 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(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);
}
}
});
@@ -21,16 +21,11 @@ import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
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;
@@ -71,18 +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 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(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 <[email protected]> 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 <[email protected]> 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());
@@ -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;
Binary file not shown.
+27 -4
View File
@@ -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 *
@@ -346,7 +356,7 @@
}
.jfx-tool-bar .jfx-decorator-button {
-fx-cursor: head;
-fx-cursor: hand;
}
.jfx-tool-bar Label {
@@ -502,6 +512,11 @@
* *
*******************************************************************************/
.jfx-button {
-jfx-disable-visual-focus: true;
-fx-cursor: hand;
}
.jfx-button-raised {
-fx-background-color: -fx-base-color;
}
@@ -876,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;
}
@@ -1028,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 {
@@ -1046,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 {
@@ -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 ([email protected])
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 ([email protected])
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 ([email protected])
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 @@
*/
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":
@@ -90,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))));
}
@@ -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);
}
}
@@ -1,3 +1,20 @@
/*
* 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;
@@ -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
@@ -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);
@@ -1,3 +1,20 @@
/*
* 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.*;
@@ -124,7 +124,6 @@ public final class ModManager {
for (ModInfo modInfo : modInfos) {
Files.deleteIfExists(modInfo.getFile());
}
refreshMods();
}
public Path disableMod(Path file) throws IOException {
@@ -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;
}
@@ -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());
}
}
Binary file not shown.
Binary file not shown.
+4
View File
@@ -24,6 +24,7 @@
<Keyword>Win32Proj</Keyword>
<RootNamespace>HMCL</RootNamespace>
<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'">
@@ -153,6 +156,7 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="java.h" />
<ClInclude Include="lang.h" />
<ClInclude Include="main.h" />
<ClInclude Include="os.h" />
<ClInclude Include="Resource.h" />
+3
View File
@@ -36,6 +36,9 @@
<ClInclude Include="os.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="lang.h">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="stdafx.cpp">
+3 -1
View File
@@ -1,7 +1,9 @@
#include "stdafx.h"
#include "Version.h"
Version::Version(const std::wstring & rawString)
using namespace std;
Version::Version(const wstring & rawString)
{
int idx = 0;
ver[0] = ver[1] = ver[2] = ver[3] = 0;
+20
View File
@@ -9,6 +9,18 @@ public:
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)
@@ -16,5 +28,13 @@ public:
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;
}
};
+1 -1
View File
@@ -58,7 +58,7 @@ bool FindJavaByRegistryKey(HKEY rootKey, LPCWSTR subKey, std::wstring & path)
{
if (Version(javaVer) < JAVA_8)
oldJavaFound = true;
else if (!(Version(javaVer) < JAVA_11))
else if (JAVA_11 <= Version(javaVer))
newJavaFound = true;
else
flag = true;
+4
View File
@@ -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"
+19 -6
View File
@@ -2,16 +2,30 @@
#include "main.h"
#include "os.h"
#include "java.h"
#include "lang.h"
using namespace std;
Version J8(TEXT("8")), J11(TEXT("11"));
void LaunchJVM(const wstring &javaPath, const wstring &jarPath)
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;
@@ -37,9 +51,6 @@ int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmd
if (FindJava(path))
LaunchJVM(path + L"\\bin\\javaw.exe", exeName);
// Try java in PATH
LaunchJVM(L"javaw", exeName);
// Or we try to search Java in C:\Program Files.
{
WIN32_FIND_DATA data;
@@ -72,8 +83,10 @@ int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmd
}
}
MessageBox(NULL, 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", L"Error", MB_ICONERROR | MB_OK);
// 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;
}
+33
View File
@@ -106,3 +106,36 @@ bool GetArch(bool & is64Bit)
#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;
}
+4 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include <string>
#include <windows.h>
#include "Version.h"
const int MAX_KEY_LENGTH = 255;
const int MAX_VALUE_NAME = 16383;
@@ -20,4 +21,6 @@ bool MyCreateProcess(const std::wstring &command);
// Check if file lpPath exists.
bool FindFirstFileExists(LPCWSTR lpPath, DWORD dwFilter);
bool GetArch(bool &is64Bit);
bool GetArch(bool &is64Bit);
bool MyGetFileVersionInfo(const std::wstring &filePath, Version &version);
BIN
View File
Binary file not shown.