diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java index 5b779dbfb..5b8da0995 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java @@ -208,6 +208,24 @@ public final class Profile implements Observable { Platform.runLater(observableHelper::invalidate); } + public static class ProfileVersion { + private final Profile profile; + private final String version; + + public ProfileVersion(Profile profile, String version) { + this.profile = profile; + this.version = version; + } + + public Profile getProfile() { + return profile; + } + + public String getVersion() { + return version; + } + } + public static final class Serializer implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 000cb482f..ba6e15534 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -29,6 +29,7 @@ import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.java.JavaRepository; import org.jackhuang.hmcl.setting.EnumCommonDirectory; +import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage; @@ -40,13 +41,16 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.construct.PromptDialogPane; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; import org.jackhuang.hmcl.ui.decorator.DecoratorController; +import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.main.RootPage; +import org.jackhuang.hmcl.ui.versions.GameListPage; import org.jackhuang.hmcl.ui.versions.VersionPage; import org.jackhuang.hmcl.util.FutureCallback; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.JavaVersion; +import java.io.File; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -62,6 +66,7 @@ public final class Controllers { private static Scene scene; private static Stage stage; private static VersionPage versionPage = null; + private static GameListPage gameListPage = null; private static AuthlibInjectorServersPage serversPage = null; private static RootPage rootPage; private static DecoratorController decorator; @@ -84,6 +89,20 @@ public final class Controllers { return versionPage; } + // FXThread + public static GameListPage getGameListPage() { + if (gameListPage == null) { + gameListPage = new GameListPage(); + gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty()); + gameListPage.profilesProperty().bindContent(Profiles.profilesProperty()); + FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { + File modpack = modpacks.get(0); + Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack")); + }); + } + return gameListPage; + } + // FXThread public static RootPage getRootPage() { if (rootPage == null) @@ -167,7 +186,11 @@ public final class Controllers { } public static void confirm(String text, String title, Runnable onAccept, Runnable onCancel) { - dialog(new MessageDialogPane(text, title, onAccept, onCancel)); + confirm(text, title, MessageType.QUESTION, onAccept, onCancel); + } + + public static void confirm(String text, String title, MessageType type, Runnable onAccept, Runnable onCancel) { + dialog(new MessageDialogPane(text, title, type, onAccept, onCancel)); } public static CompletableFuture prompt(String title, FutureCallback onResult) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 7af072c83..c72c89e78 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -194,4 +194,20 @@ public final class SVG { public static Node hanger(ObjectBinding fill, double width, double height) { return createSVGPath("M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z", fill, width, height); } + + public static Node puzzle(ObjectBinding fill, double width, double height) { + return createSVGPath("M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z", fill, width, height); + } + + public static Node cube(ObjectBinding fill, double width, double height) { + return createSVGPath("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z", fill, width, height); + } + + public static Node pack(ObjectBinding fill, double width, double height) { + return createSVGPath("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z", fill, width, height); + } + + public static Node gamepad(ObjectBinding fill, double width, double height) { + return createSVGPath("M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z", fill, width, height); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java index b39bba5c2..2183228d9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java @@ -18,25 +18,28 @@ package org.jackhuang.hmcl.ui.account; import javafx.beans.binding.Bindings; -import javafx.beans.property.*; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; +import javafx.scene.Node; import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.setting.Accounts; -import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.AdvancedListItem; +import org.jackhuang.hmcl.util.Pair; import static org.jackhuang.hmcl.ui.FXUtils.newImage; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class AccountAdvancedListItem extends AdvancedListItem { private final Tooltip tooltip; + private final ImageView imageView; private ObjectProperty account = new SimpleObjectProperty() { @@ -47,23 +50,26 @@ public class AccountAdvancedListItem extends AdvancedListItem { titleProperty().unbind(); setTitle(i18n("account.missing")); setSubtitle(i18n("account.missing.add")); - imageProperty().unbind(); - setImage(newImage("/assets/img/craft_table.png")); + imageView.imageProperty().unbind(); + imageView.setImage(newImage("/assets/img/craft_table.png")); tooltip.setText(""); } else { titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account)); setSubtitle(accountSubtitle(account)); - imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32)); + imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32)); tooltip.setText(account.getCharacter() + " " + accountTooltip(account)); } } }; public AccountAdvancedListItem() { - setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1)); tooltip = new Tooltip(); FXUtils.installFastTooltip(this, tooltip); + Pair view = createImageView(null); + setLeftGraphic(view.getKey()); + imageView = view.getValue(); + setOnScroll(event -> { Account current = account.get(); if (current == null) return; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java index d02f11fa4..e16e6a5bb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java @@ -37,7 +37,6 @@ public class AdvancedListBox extends ScrollPane { setFitToWidth(true); setHbarPolicy(ScrollBarPolicy.NEVER); - container.setSpacing(5); container.getStyleClass().add("advanced-list-box-content"); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItem.java index 28c0f5a4d..4d4a40945 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItem.java @@ -24,10 +24,16 @@ import javafx.scene.Node; import javafx.scene.control.Control; import javafx.scene.control.Skin; import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.util.Pair; + +import static org.jackhuang.hmcl.util.Pair.pair; public class AdvancedListItem extends Control { - private final ObjectProperty image = new SimpleObjectProperty<>(this, "image"); + private final ObjectProperty leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic"); private final ObjectProperty rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic"); private final StringProperty title = new SimpleStringProperty(this, "title"); private final BooleanProperty active = new SimpleBooleanProperty(this, "active"); @@ -39,16 +45,16 @@ public class AdvancedListItem extends Control { addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent())); } - public Image getImage() { - return image.get(); + public Node getLeftGraphic() { + return leftGraphic.get(); } - public ObjectProperty imageProperty() { - return image; + public ObjectProperty leftGraphicProperty() { + return leftGraphic; } - public void setImage(Image image) { - this.image.set(image); + public void setLeftGraphic(Node leftGraphic) { + this.leftGraphic.set(leftGraphic); } public Node getRightGraphic() { @@ -134,4 +140,17 @@ public class AdvancedListItem extends Control { protected Skin createDefaultSkin() { return new AdvancedListItemSkin(this); } + + public static Pair createImageView(Image image) { + StackPane imageViewContainer = new StackPane(); + FXUtils.setLimitWidth(imageViewContainer, 32); + FXUtils.setLimitHeight(imageViewContainer, 32); + + ImageView imageView = new ImageView(); + FXUtils.limitSize(imageView, 32, 32); + imageView.setPreserveRatio(true); + imageView.setImage(image); + imageViewContainer.getChildren().setAll(imageView); + return pair(imageViewContainer, imageView); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItemSkin.java index e15d5bac5..0509e05bb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListItemSkin.java @@ -18,16 +18,13 @@ package org.jackhuang.hmcl.ui.construct; import javafx.css.PseudoClass; -import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.SkinBase; -import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; -import javafx.scene.text.TextAlignment; import org.jackhuang.hmcl.ui.FXUtils; public class AdvancedListItemSkin extends SkinBase { @@ -48,35 +45,22 @@ public class AdvancedListItemSkin extends SkinBase { root.setPickOnBounds(false); HBox left = new HBox(); - left.setAlignment(Pos.CENTER); + left.setAlignment(Pos.CENTER_LEFT); left.setMouseTransparent(true); - StackPane imageViewContainer = new StackPane(); - FXUtils.setLimitWidth(imageViewContainer, 32); - FXUtils.setLimitHeight(imageViewContainer, 32); - - ImageView imageView = new ImageView(); - FXUtils.limitSize(imageView, 32, 32); - imageView.setPreserveRatio(true); - imageView.imageProperty().bind(skinnable.imageProperty()); - imageViewContainer.getChildren().setAll(imageView); - VBox vbox = new VBox(); + root.setCenter(vbox); + vbox.setMouseTransparent(true); vbox.setAlignment(Pos.CENTER_LEFT); - vbox.setPadding(new Insets(0, 0, 0, 10)); Label title = new Label(); title.textProperty().bind(skinnable.titleProperty()); - title.setMaxWidth(90); - title.setStyle("-fx-font-size: 15;"); - title.setTextAlignment(TextAlignment.JUSTIFY); + title.getStyleClass().add("title"); vbox.getChildren().add(title); Label subtitle = new Label(); subtitle.textProperty().bind(skinnable.subtitleProperty()); - subtitle.setMaxWidth(90); - subtitle.setStyle("-fx-font-size: 10;"); - subtitle.setTextAlignment(TextAlignment.JUSTIFY); + subtitle.getStyleClass().add("subtitle"); vbox.getChildren().add(subtitle); FXUtils.onChangeAndOperate(skinnable.subtitleProperty(), subtitleString -> { @@ -84,7 +68,14 @@ public class AdvancedListItemSkin extends SkinBase { else vbox.getChildren().setAll(title, subtitle); }); - left.getChildren().setAll(imageViewContainer, vbox); + FXUtils.onChangeAndOperate(skinnable.leftGraphicProperty(), + newGraphic -> { + if (newGraphic == null) { + left.getChildren().clear(); + } else { + left.getChildren().setAll(newGraphic); + } + }); root.setLeft(left); HBox right = new HBox(); @@ -105,8 +96,6 @@ public class AdvancedListItemSkin extends SkinBase { FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(), visible -> root.setRight(visible ? right : null)); - stackPane.setStyle("-fx-padding: 10 16 10 16;"); - stackPane.getStyleClass().add("transparent"); stackPane.setPickOnBounds(false); stackPane.getChildren().setAll(root); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MessageDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MessageDialogPane.java index 27000be17..513fb29ea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MessageDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MessageDialogPane.java @@ -88,8 +88,8 @@ public final class MessageDialogPane extends StackPane { } } - public MessageDialogPane(String text, String title, Runnable onAccept, Runnable onCancel) { - this(text, title, MessageType.QUESTION, onAccept); + public MessageDialogPane(String text, String title, MessageType type, Runnable onAccept, Runnable onCancel) { + this(text, title, type, onAccept); cancelButton.setVisible(true); cancelButton.setOnMouseClicked(e -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java index 708de2fd3..87528564b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java @@ -49,6 +49,7 @@ public class TabHeader extends Control implements TabControl { } private ObservableList tabs = FXCollections.observableArrayList(); + private ObjectProperty side = new SimpleObjectProperty<>(Side.TOP); @Override public ObservableList getTabs() { @@ -69,6 +70,27 @@ public class TabHeader extends Control implements TabControl { this.selectionModel.set(selectionModel); } + /** + * The position to place the tabs. + */ + public Side getSide() { + return side.get(); + } + + /** + * The position of the tabs. + */ + public ObjectProperty sideProperty() { + return side; + } + + /** + * The position the place the tabs in this TabHeader. + */ + public void setSide(Side side) { + this.side.set(side); + } + @Override protected Skin createDefaultSkin() { return new TabHeaderSkin(this); @@ -115,7 +137,7 @@ public class TabHeader extends Control implements TabControl { protected class HeaderContainer extends StackPane { private Timeline timeline; private StackPane selectedTabLine; - private StackPane headersRegion; + private HeadersRegion headersRegion; private Scale scale = new Scale(1, 1, 0, 0); private Rotate rotate = new Rotate(0, 0, 1); private double selectedTabLineOffset; @@ -125,9 +147,115 @@ public class TabHeader extends Control implements TabControl { getStyleClass().add("tab-header-area"); setPickOnBounds(false); - headersRegion = new StackPane() { + headersRegion = new HeadersRegion(); + headersRegion.sideProperty().bind(getSkinnable().sideProperty()); + + selectedTabLine = new StackPane(); + selectedTabLine.setManaged(false); + selectedTabLine.getTransforms().addAll(scale, rotate); + selectedTabLine.setCache(true); + selectedTabLine.getStyleClass().addAll("tab-selected-line"); + selectedTabLine.setPrefHeight(2); + selectedTabLine.setPrefWidth(2); + selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY))); + getChildren().setAll(headersRegion, selectedTabLine); + headersRegion.setPickOnBounds(false); + headersRegion.prefHeightProperty().bind(heightProperty()); + prefWidthProperty().bind(headersRegion.widthProperty()); + rotate.pivotXProperty().bind(Bindings.createDoubleBinding(() -> getSkinnable().getSide().isHorizontal() ? 0.0 : 1, getSkinnable().sideProperty())); + rotate.pivotYProperty().bind(Bindings.createDoubleBinding(() -> getSkinnable().getSide().isHorizontal() ? 1.0 : 0, getSkinnable().sideProperty())); + + Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> { + TabHeaderContainer container = new TabHeaderContainer(tab); + container.setVisible(true); + return container; + })); + } + + public void setNeedsLayout2(boolean value) { + setNeedsLayout(value); + } + + private boolean isAnimating() { + return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING; + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + + if (isSelectingTab) { + headersRegion.animateSelectionLine(); + isSelectingTab = false; + } + } + + private class HeadersRegion extends StackPane { + private SideAction action; + private final ObjectProperty side = new SimpleObjectProperty() { @Override - protected double computePrefWidth(double height) { + protected void invalidated() { + super.invalidated(); + + switch (get()) { + case TOP: action = new Top(); break; + case BOTTOM: action = new Bottom(); break; + case LEFT: action = new Left(); break; + case RIGHT: action = new Right(); break; + default: throw new InternalError(); + } + } + }; + + public Side getSide() { + return side.get(); + } + + public ObjectProperty sideProperty() { + return side; + } + + public void setSide(Side side) { + this.side.set(side); + } + + @Override + protected double computePrefWidth(double height) { + return action.computePrefWidth(height); + } + + @Override + protected double computePrefHeight(double width) { + return action.computePrefHeight(width); + } + + @Override + protected void layoutChildren() { + action.layoutChildren(); + } + + protected void animateSelectionLine() { + action.animateSelectionLine(); + } + + private abstract class SideAction { + abstract double computePrefWidth(double height); + + abstract double computePrefHeight(double width); + + void layoutChildren() { + if (isSelectingTab) { + animateSelectionLine(); + isSelectingTab = false; + } + } + + abstract void animateSelectionLine(); + } + + private abstract class Horizontal extends SideAction { + @Override + public double computePrefWidth(double height) { double width = 0; for (Node child : getChildren()) { if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue; @@ -137,7 +265,7 @@ public class TabHeader extends Control implements TabControl { } @Override - protected double computePrefHeight(double width) { + public double computePrefHeight(double width) { double height = 0; for (Node child : getChildren()) { if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue; @@ -146,13 +274,81 @@ public class TabHeader extends Control implements TabControl { return snapSize(height) + snappedTopInset() + snappedBottomInset(); } - @Override - protected void layoutChildren() { - if (isSelectingTab) { - animateSelectionLine(); - isSelectingTab = false; + private void runTimeline(double newTransX, double newWidth) { + double lineWidth = selectedTabLine.prefWidth(-1.0D); + if (isAnimating()) { + timeline.stop(); + double tempScaleX = scale.getX(); + if (rotate.getAngle() != 0.0D) { + rotate.setAngle(0.0D); + double tempWidth = tempScaleX * lineWidth; + selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() - tempWidth); + } } + double oldScaleX = scale.getX(); + double oldWidth = lineWidth * oldScaleX; + double oldTransX = selectedTabLine.getTranslateX(); + double newScaleX = newWidth * oldScaleX / oldWidth; + selectedTabLineOffset = newTransX; + // newTransX += offsetStart * (double)this.direction; + double transDiff = newTransX - oldTransX; + if (transDiff < 0.0D) { + selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() + oldWidth); + newTransX += newWidth; + rotate.setAngle(180.0D); + } + + timeline = new Timeline( + new KeyFrame( + Duration.ZERO, + new KeyValue(selectedTabLine.translateXProperty(), selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH) + ), + new KeyFrame( + Duration.seconds(0.24D), + new KeyValue(scale.xProperty(), newScaleX, Interpolator.EASE_BOTH), + new KeyValue(selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH) + ) + ); + timeline.setOnFinished((finish) -> { + if (rotate.getAngle() != 0.0D) { + rotate.setAngle(0.0D); + selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() - newWidth); + } + + }); + timeline.play(); + } + + @Override + public void animateSelectionLine() { + double offset = 0.0D; + double selectedTabOffset = 0.0D; + double selectedTabWidth = 0.0D; + + for (Node node : headersRegion.getChildren()) { + if (node instanceof TabHeaderContainer) { + TabHeaderContainer tabHeader = (TabHeaderContainer) node; + double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1.0D)); + if (selectedTab != null && selectedTab.equals(tabHeader.tab)) { + selectedTabOffset = offset; + selectedTabWidth = tabHeaderPrefWidth; + break; + } + + offset += tabHeaderPrefWidth; + } + } + + this.runTimeline(selectedTabOffset, selectedTabWidth); + } + } + + private class Top extends Horizontal { + @Override + public void layoutChildren() { + super.layoutChildren(); + double headerHeight = snapSize(prefHeight(-1)); double tabStartX = 0; for (Node node : getChildren()) { @@ -171,115 +367,171 @@ public class TabHeader extends Control implements TabControl { snapSize(selectedTabLine.prefWidth(-1)), snapSize(selectedTabLine.prefHeight(-1))); } - }; - - selectedTabLine = new StackPane(); - selectedTabLine.setManaged(false); - selectedTabLine.getTransforms().addAll(scale, rotate); - selectedTabLine.setCache(true); - selectedTabLine.getStyleClass().addAll("tab-selected-line"); - selectedTabLine.setPrefHeight(2); - selectedTabLine.setPrefWidth(1); - selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY))); - getChildren().setAll(headersRegion, selectedTabLine); - headersRegion.setPickOnBounds(false); - headersRegion.prefHeightProperty().bind(heightProperty()); - prefWidthProperty().bind(headersRegion.widthProperty()); - - Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> { - TabHeaderContainer container = new TabHeaderContainer(tab); - container.setVisible(true); - return container; - })); - } - - public void setNeedsLayout2(boolean value) { - setNeedsLayout(value); - } - - private void runTimeline(double newTransX, double newWidth) { - double tempScaleX = 0.0D; - double tempWidth = 0.0D; - double lineWidth = this.selectedTabLine.prefWidth(-1.0D); - if (this.isAnimating()) { - this.timeline.stop(); - tempScaleX = this.scale.getX(); - if (this.rotate.getAngle() != 0.0D) { - this.rotate.setAngle(0.0D); - tempWidth = tempScaleX * lineWidth; - this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - tempWidth); - } } - double oldScaleX = this.scale.getX(); - double oldWidth = lineWidth * oldScaleX; - double oldTransX = this.selectedTabLine.getTranslateX(); - double newScaleX = newWidth * oldScaleX / oldWidth; - this.selectedTabLineOffset = newTransX; - // newTransX += offsetStart * (double)this.direction; - double transDiff = newTransX - oldTransX; - if (transDiff < 0.0D) { - this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() + oldWidth); - newTransX += newWidth; - this.rotate.setAngle(180.0D); - } + private class Bottom extends Horizontal { + @Override + public void layoutChildren() { + super.layoutChildren(); - this.timeline = new Timeline( - new KeyFrame( - Duration.ZERO, - new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH) - ), - new KeyFrame( - Duration.seconds(0.24D), - new KeyValue(this.scale.xProperty(), newScaleX, Interpolator.EASE_BOTH), - new KeyValue(this.selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH) - ) - ); - this.timeline.setOnFinished((finish) -> { - if (this.rotate.getAngle() != 0.0D) { - this.rotate.setAngle(0.0D); - this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - newWidth); - } + double headerHeight = snapSize(prefHeight(-1)); + double tabStartX = 0; + for (Node node : getChildren()) { + if (!(node instanceof TabHeaderContainer)) continue; + TabHeaderContainer child = (TabHeaderContainer) node; + double w = snapSize(child.prefWidth(-1)); + double h = snapSize(child.prefHeight(-1)); + child.resize(w, h); - }); - this.timeline.play(); - } - - private boolean isAnimating() { - return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING; - } - - @Override - protected void layoutChildren() { - super.layoutChildren(); - - if (isSelectingTab) { - animateSelectionLine(); - isSelectingTab = false; - } - } - - private void animateSelectionLine() { - double offset = 0.0D; - double selectedTabOffset = 0.0D; - double selectedTabWidth = 0.0D; - Side side = Side.TOP; - - for (Node node : headersRegion.getChildren()) { - if (node instanceof TabHeaderContainer) { - TabHeaderContainer tabHeader = (TabHeaderContainer)node; - double tabHeaderPrefWidth = this.snapSize(tabHeader.prefWidth(-1.0D)); - if (selectedTab != null && selectedTab.equals(tabHeader.tab)) { - selectedTabOffset = side != Side.LEFT && side != Side.BOTTOM ? offset : -offset - tabHeaderPrefWidth; - selectedTabWidth = tabHeaderPrefWidth; - break; + child.relocate(tabStartX, snappedTopInset()); + tabStartX += w; } - offset += tabHeaderPrefWidth; + selectedTabLine.resizeRelocate(0, 0, + snapSize(selectedTabLine.prefWidth(-1)), + snapSize(selectedTabLine.prefHeight(-1))); + } + } + + private abstract class Vertical extends SideAction { + @Override + public double computePrefWidth(double height) { + double width = 0; + for (Node child : getChildren()) { + if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue; + width = Math.max(width, child.prefWidth(height)); + } + return snapSize(width) + snappedLeftInset() + snappedRightInset(); + } + + @Override + public double computePrefHeight(double width) { + double height = 0; + for (Node child : getChildren()) { + if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue; + height += child.prefHeight(width); + } + return snapSize(height) + snappedTopInset() + snappedBottomInset(); + } + + private void runTimeline(double newTransY, double newHeight) { + double lineHeight = selectedTabLine.prefHeight(-1.0D); + if (isAnimating()) { + timeline.stop(); + double tempScaleY = scale.getY(); + if (rotate.getAngle() != 0.0D) { + rotate.setAngle(0.0D); + double tempHeight = tempScaleY * lineHeight; + selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() - tempHeight); + } + } + + double oldScaleY = scale.getY(); + double oldHeight = lineHeight * oldScaleY; + double oldTransY = selectedTabLine.getTranslateY(); + double newScaleY = newHeight * oldScaleY / oldHeight; + selectedTabLineOffset = newTransY; + // newTransY += offsetStart * (double)this.direction; + double transDiff = newTransY - oldTransY; + if (transDiff < 0.0D) { + selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() + oldHeight); + newTransY += newHeight; + rotate.setAngle(180.0D); + } + + timeline = new Timeline( + new KeyFrame( + Duration.ZERO, + new KeyValue(selectedTabLine.translateYProperty(), selectedTabLine.getTranslateY(), Interpolator.EASE_BOTH) + ), + new KeyFrame( + Duration.seconds(1.24D), + new KeyValue(scale.yProperty(), newScaleY, Interpolator.EASE_BOTH), + new KeyValue(selectedTabLine.translateYProperty(), newTransY, Interpolator.EASE_BOTH) + ) + ); + timeline.setOnFinished((finish) -> { + if (rotate.getAngle() != 0.0D) { + rotate.setAngle(0.0D); + selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() - newHeight); + } + + }); + timeline.play(); + } + + @Override + public void animateSelectionLine() { + double offset = 0.0D; + double selectedTabOffset = 0.0D; + double selectedTabHeight = 0.0D; + + for (Node node : headersRegion.getChildren()) { + if (node instanceof TabHeaderContainer) { + TabHeaderContainer tabHeader = (TabHeaderContainer) node; + double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1.0D)); + if (selectedTab != null && selectedTab.equals(tabHeader.tab)) { + selectedTabOffset = offset; + selectedTabHeight = tabHeaderPrefHeight; + break; + } + + offset += tabHeaderPrefHeight; + } + } + + this.runTimeline(selectedTabOffset, selectedTabHeight); + } + } + + private class Left extends Vertical { + @Override + public void layoutChildren() { + super.layoutChildren(); + + double headerWidth = snapSize(prefWidth(-1)); + double tabStartY = 0; + for (Node node : getChildren()) { + if (!(node instanceof TabHeaderContainer)) continue; + TabHeaderContainer child = (TabHeaderContainer) node; + double w = snapSize(child.prefWidth(-1)); + double h = snapSize(child.prefHeight(-1)); + child.resize(w, h); + + child.relocate(headerWidth - w - snappedRightInset(), tabStartY); + tabStartY += h; + } + + selectedTabLine.resizeRelocate(headerWidth - selectedTabLine.prefWidth(-1), 0, + snapSize(selectedTabLine.prefWidth(-1)), + snapSize(selectedTabLine.prefHeight(-1))); + } + } + + private class Right extends Vertical { + @Override + public void layoutChildren() { + super.layoutChildren(); + + double headerWidth = snapSize(prefWidth(-1)); + double tabStartY = 0; + for (Node node : getChildren()) { + if (!(node instanceof TabHeaderContainer)) continue; + TabHeaderContainer child = (TabHeaderContainer) node; + double w = snapSize(child.prefWidth(-1)); + double h = snapSize(child.prefHeight(-1)); + child.resize(w, h); + + child.relocate(snappedLeftInset(), tabStartY); + tabStartY += h; + } + + selectedTabLine.resizeRelocate(0, 0, + snapSize(selectedTabLine.prefWidth(-1)), + snapSize(selectedTabLine.prefHeight(-1))); } } - this.runTimeline(selectedTabOffset, selectedTabWidth); } } @@ -314,4 +566,5 @@ public class TabHeader extends Control implements TabControl { } } } + } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index f4f6e7bc8..4a25dd25b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -41,10 +41,7 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListItem; import org.jackhuang.hmcl.ui.construct.TabHeader; import org.jackhuang.hmcl.ui.decorator.DecoratorTabPage; import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; -import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem; -import org.jackhuang.hmcl.ui.profile.ProfileList; import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem; -import org.jackhuang.hmcl.ui.versions.GameList; import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.util.io.CompressingUtils; @@ -65,15 +62,11 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class RootPage extends DecoratorTabPage { private MainPage mainPage = null; private SettingsPage settingsPage = null; - private GameList gameListPage = null; private AccountList accountListPage = null; - private ProfileList profileListPage = null; private final TabHeader.Tab mainTab = new TabHeader.Tab("main"); private final TabHeader.Tab settingsTab = new TabHeader.Tab("settings"); - private final TabHeader.Tab gameTab = new TabHeader.Tab("game"); private final TabHeader.Tab accountTab = new TabHeader.Tab("account"); - private final TabHeader.Tab profileTab = new TabHeader.Tab("profile"); public RootPage() { setLeftPaneWidth(200); @@ -86,10 +79,8 @@ public class RootPage extends DecoratorTabPage { mainTab.setNodeSupplier(this::getMainPage); settingsTab.setNodeSupplier(this::getSettingsPage); - gameTab.setNodeSupplier(this::getGameListPage); accountTab.setNodeSupplier(this::getAccountListPage); - profileTab.setNodeSupplier(this::getProfileListPage); - getTabs().setAll(mainTab, settingsTab, gameTab, accountTab, profileTab); + getTabs().setAll(mainTab, settingsTab, accountTab); } @Override @@ -151,17 +142,6 @@ public class RootPage extends DecoratorTabPage { return settingsPage; } - private GameList getGameListPage() { - if (gameListPage == null) { - gameListPage = new GameList(); - FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { - File modpack = modpacks.get(0); - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack")); - }); - } - return gameListPage; - } - private AccountList getAccountListPage() { if (accountListPage == null) { accountListPage = new AccountList(); @@ -171,15 +151,6 @@ public class RootPage extends DecoratorTabPage { return accountListPage; } - private ProfileList getProfileListPage() { - if (profileListPage == null) { - profileListPage = new ProfileList(); - profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty()); - profileListPage.profilesProperty().bindContent(Profiles.profilesProperty()); - } - return profileListPage; - } - public Tab getMainTab() { return mainTab; } @@ -188,18 +159,10 @@ public class RootPage extends DecoratorTabPage { return settingsTab; } - public Tab getGameTab() { - return gameTab; - } - public Tab getAccountTab() { return accountTab; } - public Tab getProfileTab() { - return profileTab; - } - private void selectPage(Tab tab) { if (getSelectionModel().getSelectedItem() == tab) { getSelectionModel().select(getMainTab()); @@ -226,7 +189,7 @@ public class RootPage extends DecoratorTabPage { Profile profile = Profiles.getSelectedProfile(); String version = Profiles.getSelectedVersion(); if (version == null) { - control.selectPage(control.gameTab); + Controllers.navigate(Controllers.getGameListPage()); } else { Versions.modifyGameSettings(profile, version); } @@ -234,21 +197,14 @@ public class RootPage extends DecoratorTabPage { // third item in left sidebar AdvancedListItem gameItem = new AdvancedListItem(); - gameItem.activeProperty().bind(control.gameTab.selectedProperty()); - gameItem.setImage(newImage("/assets/img/bookshelf.png")); + gameItem.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/bookshelf.png")).getKey()); gameItem.setTitle(i18n("version.manage")); - gameItem.setOnAction(e -> control.selectPage(control.gameTab)); - - // forth item in left sidebar - ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem(); - profileListItem.activeProperty().bind(control.profileTab.selectedProperty()); - profileListItem.setOnAction(e -> control.selectPage(control.profileTab)); - profileListItem.profileProperty().bind(Profiles.selectedProfileProperty()); + gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage())); // fifth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); launcherSettingsItem.activeProperty().bind(control.settingsTab.selectedProperty()); - launcherSettingsItem.setImage(newImage("/assets/img/command.png")); + launcherSettingsItem.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey()); launcherSettingsItem.setTitle(i18n("settings.launcher")); launcherSettingsItem.setOnAction(e -> control.selectPage(control.settingsTab)); @@ -259,8 +215,6 @@ public class RootPage extends DecoratorTabPage { .startCategory(i18n("version").toUpperCase()) .add(gameListItem) .add(gameItem) - .startCategory(i18n("profile.title").toUpperCase()) - .add(profileListItem) .startCategory(i18n("launcher").toUpperCase()) .add(launcherSettingsItem); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileAdvancedListItem.java index 10f94ef77..811e42053 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileAdvancedListItem.java @@ -42,7 +42,7 @@ public class ProfileAdvancedListItem extends AdvancedListItem { }; public ProfileAdvancedListItem() { - setImage(newImage("/assets/img/craft_table.png")); + setLeftGraphic(createImageView(newImage("/assets/img/craft_table.png")).getKey()); setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java deleted file mode 100644 index 4e936054d..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileList.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui 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 . - */ -package org.jackhuang.hmcl.ui.profile; - -import javafx.beans.property.*; -import javafx.collections.FXCollections; -import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.ListPage; -import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.util.javafx.MappedObservableList; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor; - -public class ProfileList extends ListPage implements DecoratorPage { - private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("profile.manage"))); - private final ListProperty profiles = new SimpleListProperty<>(FXCollections.observableArrayList()); - private ObjectProperty selectedProfile; - - public ProfileList() { - setItems(MappedObservableList.create(profilesProperty(), ProfileListItem::new)); - selectedProfile = createSelectedItemPropertyFor(getItems(), Profile.class); - } - - public ObjectProperty selectedProfileProperty() { - return selectedProfile; - } - - public ListProperty profilesProperty() { - return profiles; - } - - @Override - public void add() { - Controllers.navigate(new ProfilePage(null)); - } - - @Override - public ReadOnlyObjectProperty stateProperty() { - return state.getReadOnlyProperty(); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItem.java index 6f15c462b..05ede3d91 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItem.java @@ -32,7 +32,7 @@ public class ProfileListItem extends RadioButton { public ProfileListItem(Profile profile) { this.profile = profile; - getStyleClass().clear(); + getStyleClass().setAll("navigation-drawer-item"); setUserData(profile); title.set(Profiles.getProfileDisplayName(profile)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java index 60b2d34c2..c792492eb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java @@ -18,20 +18,14 @@ package org.jackhuang.hmcl.ui.profile; import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXRadioButton; -import com.jfoenix.effects.JFXDepthManager; import javafx.geometry.Pos; import javafx.scene.control.SkinBase; -import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; 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 static org.jackhuang.hmcl.ui.FXUtils.newImage; - public class ProfileListItemSkin extends SkinBase { public ProfileListItemSkin(ProfileListItem skinnable) { @@ -39,28 +33,9 @@ public class ProfileListItemSkin extends SkinBase { BorderPane root = new BorderPane(); - JFXRadioButton chkSelected = new JFXRadioButton() { - @Override - public void fire() { - skinnable.fire(); - } - }; - BorderPane.setAlignment(chkSelected, Pos.CENTER); - chkSelected.selectedProperty().bind(skinnable.selectedProperty()); - root.setLeft(chkSelected); - - HBox center = new HBox(); - center.setSpacing(8); - center.setAlignment(Pos.CENTER_LEFT); - - ImageView imageView = new ImageView(); - FXUtils.limitSize(imageView, 32, 32); - imageView.imageProperty().set(newImage("/assets/img/craft_table.png")); - TwoLineListItem item = new TwoLineListItem(); BorderPane.setAlignment(item, Pos.CENTER); - center.getChildren().setAll(imageView, item); - root.setCenter(center); + root.setCenter(item); HBox right = new HBox(); right.setAlignment(Pos.CENTER_RIGHT); @@ -73,9 +48,6 @@ public class ProfileListItemSkin extends SkinBase { right.getChildren().add(btnRemove); root.setRight(right); - root.getStyleClass().add("card"); - root.setStyle("-fx-padding: 8 8 8 0"); - JFXDepthManager.setDepth(root, 1); item.titleProperty().bind(skinnable.titleProperty()); item.subtitleProperty().bind(skinnable.subtitleProperty()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java index 000e85156..c83d32b7f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java @@ -17,35 +17,43 @@ */ package org.jackhuang.hmcl.ui.versions; +import javafx.scene.Node; import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.AdvancedListItem; +import org.jackhuang.hmcl.util.Pair; import static org.jackhuang.hmcl.ui.FXUtils.newImage; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class GameAdvancedListItem extends AdvancedListItem { private final Tooltip tooltip; + private final ImageView imageView; public GameAdvancedListItem() { tooltip = new Tooltip(); + Pair view = createImageView(null); + setLeftGraphic(view.getKey()); + imageView = view.getValue(); + FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> { if (version != null && Profiles.getSelectedProfile() != null && Profiles.getSelectedProfile().getRepository().hasVersion(version)) { FXUtils.installFastTooltip(this, tooltip); setTitle(version); setSubtitle(null); - setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version)); + imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version)); tooltip.setText(version); } else { Tooltip.uninstall(this,tooltip); setTitle(i18n("version.empty")); setSubtitle(i18n("version.empty.add")); - setImage(newImage("/assets/img/grass.png")); + imageView.setImage(newImage("/assets/img/grass.png")); tooltip.setText(""); } }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java deleted file mode 100644 index 4064b85cf..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui 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 . - */ -package org.jackhuang.hmcl.ui.versions; - -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.scene.Node; -import javafx.scene.control.ToggleGroup; -import javafx.scene.layout.HBox; -import org.jackhuang.hmcl.event.EventBus; -import org.jackhuang.hmcl.event.RefreshingVersionsEvent; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.Version; -import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.setting.Profiles; -import org.jackhuang.hmcl.ui.*; -import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; -import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider; -import org.jackhuang.hmcl.util.versioning.VersionNumber; - -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.List; -import java.util.stream.Collectors; - -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class GameList extends ListPageBase implements DecoratorPage { - private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage"))); - - private ToggleGroup toggleGroup; - - public GameList() { - EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> { - if (event.getSource() == Profiles.getSelectedProfile().getRepository()) - runInFX(() -> setLoading(true)); - }); - - Profiles.registerVersionsListener(this::loadVersions); - } - - private void loadVersions(Profile profile) { - HMCLGameRepository repository = profile.getRepository(); - toggleGroup = new ToggleGroup(); - WeakListenerHolder listenerHolder = new WeakListenerHolder(); - toggleGroup.getProperties().put("ReferenceHolder", listenerHolder); - List children = repository.getVersions().parallelStream() - .filter(version -> !version.isHidden()) - .sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime()) - .thenComparing(a -> VersionNumber.asVersion(a.getId()))) - .map(version -> new GameListItem(toggleGroup, profile, version.getId())) - .collect(Collectors.toList()); - runInFX(() -> { - if (profile == Profiles.getSelectedProfile()) { - setLoading(false); - itemsProperty().setAll(children); - children.forEach(GameListItem::checkSelection); - - profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { - FXUtils.checkFxUserThread(); - children.forEach(it -> it.selectedProperty().set(false)); - children.stream() - .filter(it -> it.getVersion().equals(newValue)) - .findFirst() - .ifPresent(it -> it.selectedProperty().set(true)); - })); - } - toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> { - if (toggle == null) return; - GameListItem model = (GameListItem) toggle.getUserData(); - model.getProfile().setSelectedVersion(model.getVersion()); - }); - }); - } - - @Override - protected GameListSkin createDefaultSkin() { - return new GameListSkin(); - } - - public static void addNewGame() { - Profile profile = Profiles.getSelectedProfile(); - if (profile.getRepository().isLoaded()) { - Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(profile), i18n("install.new_game")); - } - } - - public static void importModpack() { - Profile profile = Profiles.getSelectedProfile(); - if (profile.getRepository().isLoaded()) { - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile), i18n("install.modpack")); - } - } - - public static void refreshList() { - Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start(); - } - - public void modifyGlobalGameSettings() { - Versions.modifyGlobalSettings(Profiles.getSelectedProfile()); - } - - @Override - public ReadOnlyObjectProperty stateProperty() { - return state.getReadOnlyProperty(); - } - - private class GameListSkin extends ToolbarListPageSkin { - - public GameListSkin() { - super(GameList.this); - - HBox hbox = new HBox( - createToolbarButton(i18n("install.new_game"), SVG::plus, GameList::addNewGame), - createToolbarButton(i18n("install.modpack"), SVG::importIcon, GameList::importModpack), - createToolbarButton(i18n("button.refresh"), SVG::refresh, GameList::refreshList), - createToolbarButton(i18n("settings.type.global.manage"), SVG::gear, GameList.this::modifyGlobalGameSettings) - ); - hbox.setPickOnBounds(false); - - state.set(new State(i18n("version.manage"), hbox, true, false, true)); - } - - @Override - protected List initializeToolbar(GameList skinnable) { - return Collections.emptyList(); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java new file mode 100644 index 000000000..832dffd18 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -0,0 +1,244 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.versions; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SkinBase; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.event.EventBus; +import org.jackhuang.hmcl.event.RefreshingVersionsEvent; +import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.construct.AdvancedListBox; +import org.jackhuang.hmcl.ui.construct.AdvancedListItem; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.ui.profile.ProfileListItem; +import org.jackhuang.hmcl.ui.profile.ProfilePage; +import org.jackhuang.hmcl.util.javafx.MappedObservableList; +import org.jackhuang.hmcl.util.versioning.VersionNumber; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.ui.FXUtils.runInFX; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor; + +public class GameListPage extends ListPageBase implements DecoratorPage { + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage"))); + private final ListProperty profiles = new SimpleListProperty<>(FXCollections.observableArrayList()); + @SuppressWarnings("FieldCanBeLocal") + private final ObservableList profileListItems; + private final ObjectProperty selectedProfile; + + private ToggleGroup toggleGroup; + + public GameListPage() { + EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> { + if (event.getSource() == Profiles.getSelectedProfile().getRepository()) + runInFX(() -> setLoading(true)); + }); + profileListItems = MappedObservableList.create(profilesProperty(), ProfileListItem::new); + selectedProfile = createSelectedItemPropertyFor(profileListItems, Profile.class); + } + + public ObjectProperty selectedProfileProperty() { + return selectedProfile; + } + + public ObservableList getProfiles() { + return profiles.get(); + } + + public ListProperty profilesProperty() { + return profiles; + } + + public void setProfiles(ObservableList profiles) { + this.profiles.set(profiles); + } + + @Override + protected GameListPageSkin createDefaultSkin() { + return new GameListPageSkin(); + } + + public void modifyGlobalGameSettings() { + Versions.modifyGlobalSettings(Profiles.getSelectedProfile()); + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); + } + + private class GameListPageSkin extends SkinBase { + + protected GameListPageSkin() { + super(GameListPage.this); + + + BorderPane root = new BorderPane(); + GameList gameList = new GameList(); + + { + BorderPane left = new BorderPane(); + FXUtils.setLimitWidth(left, 300); + root.setLeft(left); + + { + AdvancedListItem addProfileItem = new AdvancedListItem(); + addProfileItem.getStyleClass().add("navigation-drawer-item"); + addProfileItem.setTitle(i18n("profile.new")); + addProfileItem.setActionButtonVisible(false); + addProfileItem.setLeftGraphic(VersionPage.wrap(SVG.plus(Theme.blackFillBinding(), -1, -1))); + addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null))); + + ScrollPane pane = new ScrollPane(); + VBox wrapper = new VBox(); + VBox box = new VBox(); + Bindings.bindContent(box.getChildren(), profileListItems); + wrapper.getChildren().setAll(box, addProfileItem); + pane.setContent(wrapper); + left.setCenter(pane); + } + + { + AdvancedListItem installNewGameItem = new AdvancedListItem(); + installNewGameItem.getStyleClass().add("navigation-drawer-item"); + installNewGameItem.setTitle(i18n("install.new_game")); + installNewGameItem.setActionButtonVisible(false); + installNewGameItem.setLeftGraphic(VersionPage.wrap(SVG.plus(Theme.blackFillBinding(), -1, -1))); + installNewGameItem.setOnAction(e -> Versions.addNewGame()); + + AdvancedListItem installModpackItem = new AdvancedListItem(); + installModpackItem.getStyleClass().add("navigation-drawer-item"); + installModpackItem.setTitle(i18n("install.modpack")); + installModpackItem.setActionButtonVisible(false); + installModpackItem.setLeftGraphic(VersionPage.wrap(SVG.importIcon(Theme.blackFillBinding(), -1, -1))); + installModpackItem.setOnAction(e -> Versions.importModpack()); + + AdvancedListItem refreshItem = new AdvancedListItem(); + refreshItem.getStyleClass().add("navigation-drawer-item"); + refreshItem.setTitle(i18n("button.refresh")); + refreshItem.setActionButtonVisible(false); + refreshItem.setLeftGraphic(VersionPage.wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1))); + refreshItem.setOnAction(e -> gameList.refreshList()); + + AdvancedListItem globalManageItem = new AdvancedListItem(); + globalManageItem.getStyleClass().add("navigation-drawer-item"); + globalManageItem.setTitle(i18n("settings.type.global.manage")); + globalManageItem.setActionButtonVisible(false); + globalManageItem.setLeftGraphic(VersionPage.wrap(SVG.gear(Theme.blackFillBinding(), -1, -1))); + globalManageItem.setOnAction(e -> modifyGlobalGameSettings()); + + AdvancedListBox bottomLeftCornerList = new AdvancedListBox() + .add(installNewGameItem) + .add(installModpackItem) + .add(refreshItem) + .add(globalManageItem); + FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 4 + 18); + left.setBottom(bottomLeftCornerList); + } + } + + root.setCenter(gameList); + getChildren().setAll(root); + } + } + + private class GameList extends ListPageBase { + public GameList() { + super(); + + Profiles.registerVersionsListener(this::loadVersions); + } + + private void loadVersions(Profile profile) { + HMCLGameRepository repository = profile.getRepository(); + toggleGroup = new ToggleGroup(); + WeakListenerHolder listenerHolder = new WeakListenerHolder(); + toggleGroup.getProperties().put("ReferenceHolder", listenerHolder); + List children = repository.getVersions().parallelStream() + .filter(version -> !version.isHidden()) + .sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime()) + .thenComparing(a -> VersionNumber.asVersion(a.getId()))) + .map(version -> new GameListItem(toggleGroup, profile, version.getId())) + .collect(Collectors.toList()); + runInFX(() -> { + if (profile == Profiles.getSelectedProfile()) { + setLoading(false); + itemsProperty().setAll(children); + children.forEach(GameListItem::checkSelection); + + profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { + FXUtils.checkFxUserThread(); + children.forEach(it -> it.selectedProperty().set(false)); + children.stream() + .filter(it -> it.getVersion().equals(newValue)) + .findFirst() + .ifPresent(it -> it.selectedProperty().set(true)); + })); + } + toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> { + if (toggle == null) return; + GameListItem model = (GameListItem) toggle.getUserData(); + model.getProfile().setSelectedVersion(model.getVersion()); + }); + }); + } + + public void refreshList() { + Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start(); + } + + @Override + protected GameListSkin createDefaultSkin() { + return new GameListSkin(); + } + + private class GameListSkin extends ToolbarListPageSkin { + + public GameListSkin() { + super(GameList.this); + + state.set(new State(i18n("version.manage"), null, true, false, true, false, 300)); + } + + @Override + protected List initializeToolbar(GameList skinnable) { + return Collections.emptyList(); + } + } + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index 008bd8674..a927cfdd5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -17,48 +17,35 @@ */ package org.jackhuang.hmcl.ui.versions; -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXListCell; -import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXPopup; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.beans.property.*; -import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.input.MouseButton; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.SkinBase; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.util.Callback; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.Version; +import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.ToolbarListPageSkin; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.versioning.VersionNumber; import java.io.File; -import java.util.Comparator; -import java.util.Date; -import java.util.List; -import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class VersionPage extends Control implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final BooleanProperty loading = new SimpleBooleanProperty(); - private final JFXListView listView = new JFXListView<>(); private final TabHeader.Tab versionSettingsTab = new TabHeader.Tab("versionSettingsTab"); private final VersionSettingsPage versionSettingsPage = new VersionSettingsPage(); private final TabHeader.Tab modListTab = new TabHeader.Tab("modListTab"); @@ -70,71 +57,39 @@ public class VersionPage extends Control implements DecoratorPage { private final TransitionPane transitionPane = new TransitionPane(); private final ObjectProperty selectedTab = new SimpleObjectProperty<>(); private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty(); - - private Profile profile; - private String version; + private final ObjectProperty version = new SimpleObjectProperty<>(); private String preferredVersionName = null; { - Profiles.registerVersionsListener(this::loadVersions); - - listView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null && !Objects.equals(oldValue, newValue)) - loadVersion(newValue, profile); - if (newValue == null && !Objects.equals(oldValue, newValue)) { - if (listView.getItems().contains(preferredVersionName)) { - loadVersion(preferredVersionName, profile); - } else if (!listView.getItems().isEmpty()) { - loadVersion(listView.getItems().get(0), profile); - } - } - }); - versionSettingsTab.setNode(versionSettingsPage); modListTab.setNode(modListPage); installerListTab.setNode(installerListPage); worldListTab.setNode(worldListPage); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); - } - private void loadVersions(Profile profile) { - HMCLGameRepository repository = profile.getRepository(); - List children = repository.getVersions().parallelStream() - .filter(version -> !version.isHidden()) - .sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime()) - .thenComparing(a -> VersionNumber.asVersion(a.getId()))) - .map(Version::getId) - .collect(Collectors.toList()); - runInFX(() -> { - if (profile == Profiles.getSelectedProfile()) { - this.profile = profile; - loading.set(false); - listView.getItems().setAll(children); - } + selectedTab.set(versionSettingsTab); + FXUtils.onChangeAndOperate(selectedTab, newValue -> { + transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer()); }); } public void setVersion(String version, Profile profile) { - this.version = version; - this.profile = profile; - - profile.setSelectedVersion(version); + this.version.set(new Profile.ProfileVersion(profile, version)); } public void loadVersion(String version, Profile profile) { // If we jumped to game list page and deleted this version // and back to this page, we should return to main page. - if (!this.profile.getRepository().isLoaded() || - !this.profile.getRepository().hasVersion(version)) { + if (this.version.get() != null && (!getProfile().getRepository().isLoaded() || + !getProfile().getRepository().hasVersion(version))) { Platform.runLater(() -> fireEvent(new PageCloseEvent())); return; } setVersion(version, profile); preferredVersionName = version; - listView.getSelectionModel().select(version); versionSettingsPage.loadVersion(profile, version); currentVersionUpgradable.set(profile.getRepository().isModpack(version)); @@ -146,63 +101,71 @@ public class VersionPage extends Control implements DecoratorPage { } private void onNavigated(Navigator.NavigationEvent event) { - if (this.version == null || this.profile == null) + if (this.version.get() == null) throw new IllegalStateException(); // If we jumped to game list page and deleted this version // and back to this page, we should return to main page. - if (!this.profile.getRepository().isLoaded() || - !this.profile.getRepository().hasVersion(version)) { + if (!getProfile().getRepository().isLoaded() || + !getProfile().getRepository().hasVersion(getVersion())) { Platform.runLater(() -> fireEvent(new PageCloseEvent())); return; } - loadVersion(this.version, this.profile); + loadVersion(getVersion(), getProfile()); } private void onBrowse(String sub) { - FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub)); + FXUtils.openFolder(new File(getProfile().getRepository().getRunDirectory(getVersion()), sub)); } private void redownloadAssetIndex() { - Versions.updateGameAssets(profile, version); + Versions.updateGameAssets(getProfile(), getVersion()); } private void clearLibraries() { - FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries")); + FileUtils.deleteDirectoryQuietly(new File(getProfile().getRepository().getBaseDirectory(), "libraries")); } private void clearJunkFiles() { - Versions.cleanVersion(profile, version); + Versions.cleanVersion(getProfile(), getVersion()); } private void testGame() { - Versions.testGame(profile, version); + Versions.testGame(getProfile(), getVersion()); } private void updateGame() { - Versions.updateVersion(profile, version); + Versions.updateVersion(getProfile(), getVersion()); } private void generateLaunchScript() { - Versions.generateLaunchScript(profile, version); + Versions.generateLaunchScript(getProfile(), getVersion()); } private void export() { - Versions.exportVersion(profile, version); + Versions.exportVersion(getProfile(), getVersion()); } private void rename() { - Versions.renameVersion(profile, version) - .thenApply(newVersionName -> this.preferredVersionName = newVersionName); + Versions.renameVersion(getProfile(), getVersion()) + .thenApply(newVersionName -> this.preferredVersionName = newVersionName); } private void remove() { - Versions.deleteVersion(profile, version); + Versions.deleteVersion(getProfile(), getVersion()); } private void duplicate() { - Versions.duplicateVersion(profile, version); + Versions.duplicateVersion(getProfile(), getVersion()); + } + + public Profile getProfile() { + return Optional.ofNullable(version.get()).map(Profile.ProfileVersion::getProfile).orElse(null); + } + + public String getVersion() { + return Optional.ofNullable(version.get()).map(Profile.ProfileVersion::getVersion).orElse(null); } @Override @@ -217,7 +180,6 @@ public class VersionPage extends Control implements DecoratorPage { public static class Skin extends SkinBase { - String currentVersion; private JFXPopup listViewItemPopup; /** @@ -232,46 +194,30 @@ public class VersionPage extends Control implements DecoratorPage { listViewItemPopup = new JFXPopup(menu); menu.getContent().setAll( new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(() -> { - Versions.testGame(getSkinnable().profile, currentVersion); + Versions.testGame(getSkinnable().getProfile(), getSkinnable().getVersion()); }, listViewItemPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(() -> { - Versions.generateLaunchScript(getSkinnable().profile, currentVersion); + Versions.generateLaunchScript(getSkinnable().getProfile(), getSkinnable().getVersion()); }, listViewItemPopup)), new MenuSeparator(), new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> { - Versions.renameVersion(getSkinnable().profile, currentVersion).thenApply(name -> getSkinnable().preferredVersionName = name); + Versions.renameVersion(getSkinnable().getProfile(), getSkinnable().getVersion()).thenApply(name -> getSkinnable().preferredVersionName = name); }, listViewItemPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(() -> { - Versions.duplicateVersion(getSkinnable().profile, currentVersion); + Versions.duplicateVersion(getSkinnable().getProfile(), getSkinnable().getVersion()); }, listViewItemPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> { - Versions.deleteVersion(getSkinnable().profile, currentVersion); + Versions.deleteVersion(getSkinnable().getProfile(), getSkinnable().getVersion()); }, listViewItemPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(() -> { - Versions.exportVersion(getSkinnable().profile, currentVersion); + Versions.exportVersion(getSkinnable().getProfile(), getSkinnable().getVersion()); }, listViewItemPopup)), new MenuSeparator(), new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> { - Versions.openFolder(getSkinnable().profile, currentVersion); + Versions.openFolder(getSkinnable().getProfile(), getSkinnable().getVersion()); }, listViewItemPopup)) ); - control.listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); - control.listView.setCellFactory(new Callback, ListCell>() { - @Override - public ListCell call(ListView param) { - JFXListCell cell = new JFXListCell<>(); - cell.setOnMouseClicked(e -> { - if (cell.getItem() == null) return; - currentVersion = cell.getItem(); - if (e.getButton() == MouseButton.SECONDARY) { - listViewItemPopup.show(cell, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY()); - } - }); - return cell; - } - }); - SpinnerPane spinnerPane = new SpinnerPane(); spinnerPane.getStyleClass().add("large-spinner-pane"); @@ -281,45 +227,48 @@ public class VersionPage extends Control implements DecoratorPage { { BorderPane left = new BorderPane(); FXUtils.setLimitWidth(left, 200); - left.setCenter(control.listView); root.setLeft(left); - JFXButton btnAddGame = ToolbarListPageSkin.createToolbarButton(null, SVG::plus, GameList::addNewGame); - FXUtils.installFastTooltip(btnAddGame, new Tooltip(i18n("install.new_game"))); + AdvancedListItem versionSettingsItem = new AdvancedListItem(); + versionSettingsItem.getStyleClass().add("navigation-drawer-item"); + versionSettingsItem.setTitle(i18n("settings")); + versionSettingsItem.setLeftGraphic(wrap(SVG.gear(Theme.blackFillBinding(), 20, 20))); + versionSettingsItem.setActionButtonVisible(false); + versionSettingsItem.activeProperty().bind(control.selectedTab.isEqualTo(control.versionSettingsTab)); + versionSettingsItem.setOnAction(e -> control.selectedTab.set(control.versionSettingsTab)); - JFXButton btnImportModpack = ToolbarListPageSkin.createToolbarButton(null, SVG::importIcon, GameList::importModpack); - FXUtils.installFastTooltip(btnImportModpack, new Tooltip(i18n("install.modpack"))); + AdvancedListItem modListItem = new AdvancedListItem(); + modListItem.getStyleClass().add("navigation-drawer-item"); + modListItem.setTitle(i18n("mods")); + modListItem.setLeftGraphic(wrap(SVG.puzzle(Theme.blackFillBinding(), 20, 20))); + modListItem.setActionButtonVisible(false); + modListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.modListTab)); + modListItem.setOnAction(e -> control.selectedTab.set(control.modListTab)); - JFXButton btnRefresh = ToolbarListPageSkin.createToolbarButton(null, SVG::refresh, GameList::refreshList); - FXUtils.installFastTooltip(btnRefresh, new Tooltip(i18n("button.refresh"))); + AdvancedListItem installerListItem = new AdvancedListItem(); + installerListItem.getStyleClass().add("navigation-drawer-item"); + installerListItem.setTitle(i18n("settings.tabs.installers")); + installerListItem.setLeftGraphic(wrap(SVG.cube(Theme.blackFillBinding(), 20, 20))); + installerListItem.setActionButtonVisible(false); + installerListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.installerListTab)); + installerListItem.setOnAction(e -> control.selectedTab.set(control.installerListTab)); - HBox toolbar = new HBox(); - toolbar.setPickOnBounds(false); - toolbar.getChildren().setAll(btnAddGame, btnImportModpack, btnRefresh); - left.setTop(toolbar); - } + AdvancedListItem worldListItem = new AdvancedListItem(); + worldListItem.getStyleClass().add("navigation-drawer-item"); + worldListItem.setTitle(i18n("world")); + worldListItem.setLeftGraphic(wrap(SVG.gamepad(Theme.blackFillBinding(), 20, 20))); + worldListItem.setActionButtonVisible(false); + worldListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.worldListTab)); + worldListItem.setOnAction(e -> control.selectedTab.set(control.worldListTab)); + + AdvancedListBox sideBar = new AdvancedListBox() + .add(versionSettingsItem) + .add(modListItem) + .add(installerListItem) + .add(worldListItem); + left.setCenter(sideBar); - TabHeader tabPane = new TabHeader(); - tabPane.setPickOnBounds(false); - tabPane.getStyleClass().add("jfx-decorator-tab"); - control.versionSettingsTab.setText(i18n("settings")); - control.modListTab.setText(i18n("mods")); - control.installerListTab.setText(i18n("settings.tabs.installers")); - control.worldListTab.setText(i18n("world")); - tabPane.getTabs().setAll( - control.versionSettingsTab, - control.modListTab, - control.installerListTab, - control.worldListTab); - control.selectedTab.bind(tabPane.getSelectionModel().selectedItemProperty()); - FXUtils.onChangeAndOperate(tabPane.getSelectionModel().selectedItemProperty(), newValue -> { - control.transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer()); - }); - HBox toolBar = new HBox(); - toolBar.setAlignment(Pos.TOP_RIGHT); - toolBar.setPickOnBounds(false); - { PopupMenu browseList = new PopupMenu(); JFXPopup browsePopup = new JFXPopup(browseList); browseList.getContent().setAll( @@ -347,44 +296,49 @@ public class VersionPage extends Control implements DecoratorPage { new IconedMenuItem(null, i18n("version.manage.clean"), FXUtils.withJFXPopupClosing(control::clearJunkFiles, managementPopup)).addTooltip(i18n("version.manage.clean.tooltip")) ); - JFXButton upgradeButton = new JFXButton(); - upgradeButton.setGraphic(SVG.update(null, 20, 20)); - upgradeButton.getStyleClass().add("jfx-decorator-button"); - upgradeButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); - upgradeButton.setOnAction(event -> control.updateGame()); - upgradeButton.visibleProperty().bind(control.currentVersionUpgradable); - FXUtils.installFastTooltip(upgradeButton, i18n("version.update")); + AdvancedListItem upgradeItem = new AdvancedListItem(); + upgradeItem.getStyleClass().add("navigation-drawer-item"); + upgradeItem.setTitle(i18n("version.update")); + upgradeItem.setLeftGraphic(wrap(SVG.update(Theme.blackFillBinding(), 20, 20))); + upgradeItem.setActionButtonVisible(false); + upgradeItem.visibleProperty().bind(control.currentVersionUpgradable); + upgradeItem.setOnAction(e -> control.updateGame()); - JFXButton testGameButton = new JFXButton(); - testGameButton.setGraphic(SVG.launch(null, 20, 20)); - testGameButton.getStyleClass().add("jfx-decorator-button"); - testGameButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); - testGameButton.setOnAction(event -> control.testGame()); - FXUtils.installFastTooltip(testGameButton, i18n("version.launch.test")); + AdvancedListItem testGameItem = new AdvancedListItem(); + testGameItem.getStyleClass().add("navigation-drawer-item"); + testGameItem.setTitle(i18n("version.launch.test")); + testGameItem.setLeftGraphic(wrap(SVG.launch(Theme.blackFillBinding(), 20, 20))); + testGameItem.setActionButtonVisible(false); + testGameItem.setOnAction(e -> control.testGame()); - JFXButton browseMenuButton = new JFXButton(); - browseMenuButton.setGraphic(SVG.folderOpen(null, 20, 20)); - browseMenuButton.getStyleClass().add("jfx-decorator-button"); - browseMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); - browseMenuButton.setOnAction(event -> browsePopup.show(browseMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, browseMenuButton.getHeight())); - FXUtils.installFastTooltip(browseMenuButton, i18n("settings.game.exploration")); + AdvancedListItem browseMenuItem = new AdvancedListItem(); + browseMenuItem.getStyleClass().add("navigation-drawer-item"); + browseMenuItem.setTitle(i18n("settings.game.exploration")); + browseMenuItem.setLeftGraphic(wrap(SVG.folderOpen(Theme.blackFillBinding(), 20, 20))); + browseMenuItem.setActionButtonVisible(false); + browseMenuItem.setOnAction(e -> browsePopup.show(browseMenuItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, browseMenuItem.getWidth(), 0)); - JFXButton managementMenuButton = new JFXButton(); - FXUtils.setLimitWidth(managementMenuButton, 40); - FXUtils.setLimitHeight(managementMenuButton, 40); - managementMenuButton.setGraphic(SVG.wrench(null, 20, 20)); - managementMenuButton.getStyleClass().add("jfx-decorator-button"); - managementMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding()); - managementMenuButton.setOnAction(event -> managementPopup.show(managementMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, managementMenuButton.getHeight())); - FXUtils.installFastTooltip(managementMenuButton, i18n("settings.game.management")); + AdvancedListItem managementItem = new AdvancedListItem(); + managementItem.getStyleClass().add("navigation-drawer-item"); + managementItem.setTitle(i18n("settings.game.management")); + managementItem.setLeftGraphic(wrap(SVG.wrench(Theme.blackFillBinding(), 20, 20))); + managementItem.setActionButtonVisible(false); + managementItem.setOnAction(e -> managementPopup.show(managementItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, managementItem.getWidth(), 0)); - toolBar.getChildren().setAll(upgradeButton, testGameButton, browseMenuButton, managementMenuButton); + + AdvancedListBox toolbar = new AdvancedListBox() + .add(upgradeItem) + .add(testGameItem) + .add(browseMenuItem) + .add(managementItem); + toolbar.getStyleClass().add("advanced-list-box-clear-padding"); + FXUtils.setLimitHeight(toolbar, 40 * 4 + 18); + left.setBottom(toolbar); } - BorderPane titleBar = new BorderPane(); - titleBar.setLeft(tabPane); - titleBar.setRight(toolBar); - control.state.set(new State(i18n("version.manage.manage"), titleBar, true, false, true, false, 200)); + control.state.bind(Bindings.createObjectBinding(() -> + new State(i18n("version.manage.manage.title", getSkinnable().getVersion()), null, true, false, true, false, 200), + getSkinnable().version)); //control.transitionPane.getStyleClass().add("gray-background"); //FXUtils.setOverflowHidden(control.transitionPane, 8); @@ -395,4 +349,14 @@ public class VersionPage extends Control implements DecoratorPage { getChildren().setAll(spinnerPane); } } + + public static Node wrap(Node node) { + StackPane stackPane = new StackPane(); + FXUtils.setLimitWidth(stackPane, 30); + FXUtils.setLimitHeight(stackPane, 20); + stackPane.setPadding(new Insets(0, 10, 0, 0)); + stackPane.getChildren().setAll(node); + return stackPane; + } + } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index e66668af6..3c7307d52 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.game.LauncherHelper; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; @@ -33,6 +34,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.PromptDialogPane; import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; +import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider; import org.jackhuang.hmcl.ui.export.ExportWizardProvider; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.StringUtils; @@ -50,13 +52,27 @@ public final class Versions { private Versions() { } + public static void addNewGame() { + Profile profile = Profiles.getSelectedProfile(); + if (profile.getRepository().isLoaded()) { + Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(profile), i18n("install.new_game")); + } + } + + public static void importModpack() { + Profile profile = Profiles.getSelectedProfile(); + if (profile.getRepository().isLoaded()) { + Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile), i18n("install.modpack")); + } + } + public static void deleteVersion(Profile profile, String version) { boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == GameDirectoryType.VERSION_FOLDER; boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported(); String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) : isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") : i18n("version.manage.remove.confirm", version); - Controllers.confirm(message, i18n("message.confirm"), () -> { + Controllers.confirm(message, i18n("message.warning"), MessageDialogPane.MessageType.WARNING, () -> { profile.getRepository().removeVersionFromDisk(version); }, null); } @@ -158,7 +174,7 @@ public final class Versions { Controllers.getRootPage().checkAccount(); else if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id)) Controllers.dialog(i18n("version.empty.launch"), i18n("launch.failed"), MessageDialogPane.MessageType.ERROR, () -> { - Controllers.getRootPage().getSelectionModel().select(Controllers.getRootPage().getGameTab()); + Controllers.navigate(Controllers.getGameListPage()); }); else return true; diff --git a/HMCL/src/main/resources/assets/css/custom.css b/HMCL/src/main/resources/assets/css/custom.css index fea9fbeba..48f66bf78 100644 --- a/HMCL/src/main/resources/assets/css/custom.css +++ b/HMCL/src/main/resources/assets/css/custom.css @@ -19,7 +19,7 @@ -fx-base-color: %base-color%; -fx-base-darker-color: derive(-fx-base-color, -10%); -fx-base-check-color: derive(-fx-base-color, 30%); - -fx-base-rippler-color: %base-rippler-color%; + -fx-base-rippler-color: derive(%base-rippler-color%, 100%); -fx-base-disabled-text-fill: %disabled-font-color%; -fx-base-text-fill: %font-color%; diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index e4c8ebe67..8279c189e 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -49,17 +49,54 @@ } .rippler-container { - -jfx-rippler-fill: -fx-base-check-color; + -jfx-rippler-fill: derive(-fx-base-color, 100%); } .rippler-container:selected .label { -fx-text-fill: -fx-base-text-fill; } +.advanced-list-item .container { + -fx-padding: 10 16 10 16; + -fx-background-color: null; +} + +.advanced-list-item .container VBox { + -fx-padding: 0 0 0 10; +} + +.advanced-list-item .container .image-container { + -fx-max-width: 32; + -fx-min-width: 32; + -fx-max-height: 32; + -fx-min-height: 32; +} + +.advanced-list-item .container .title { + -fx-max-width: 90; + -fx-font-size: 13; + -fx-text-alignment: justify; +} + +.advanced-list-item .container .subtitle { + -fx-max-width: 90; + -fx-font-size: 10; + -fx-text-alignment: justify; +} + .advanced-list-item:selected .container { -fx-background-color: -fx-base-rippler-color; } +.advanced-list-item:selected .container .title { + -fx-text-fill: -fx-base-color; + -fx-font-weight: bold; +} + +.navigation-drawer-item .container VBox { + -fx-padding: 0 0 0 0; +} + .notice-pane > .label { -fx-text-fill: #0079FF; -fx-font-size: 20; @@ -69,15 +106,15 @@ .class-title { -fx-font-size: 12px; - -fx-padding: 0 16 0 16; + -fx-padding: 16 16 8 16; } .advanced-list-box-item { - -fx-padding: 0 16 0 16; } .advanced-list-box-content { - -fx-padding: 20 0 20 0; + -fx-padding: 18 0 0 0; + -fx-spacing: 0; } .iconed-item { diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 8154e75a2..0d04c75f0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -461,6 +461,7 @@ version.manage.duplicate.duplicate_save=複製存檔 version.manage.duplicate.prompt=請輸入新遊戲實例名稱 version.manage.duplicate.confirm=將鎖定複製產生的新遊戲實例:強制版本隔離、遊戲設置獨立。 version.manage.manage=遊戲管理 +version.manage.manage.title=遊戲管理 - %1s version.manage.redownload_assets_index=更新遊戲資源檔案 version.manage.remove=刪除該版本 version.manage.remove.confirm=真的要刪除版本 %s 嗎? 你將無法找回被刪除的檔案! diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index b48429912..21aa60e24 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -22,7 +22,7 @@ about.copyright.statement=版权所有 (c) 2021 huangyuhui. about.author=作者 about.author.statement=huanghongxun (hmcl@huangyuhui.net) about.thanks_to=鸣谢 -about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默认背景图)\n所有通过 Issues、Pull Requests 等方式参与本项目的贡献者 +about.thanks_to.statement=yushijinhun (authlib-injector 相关支持)\nbangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默认背景图)\n所有通过 Issues、Pull Requests 等方式参与本项目的贡献者 about.dependency=依赖 # 由于篇幅限制,仅列出第一作者 about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)\nConstant Pool Scanner (jenkins-ci, CDDL, GPL 2)\nOpenNBT (Steveice10, BSD 3-Clause) @@ -347,7 +347,7 @@ profile.instance_directory=游戏路径 profile.instance_directory.choose=选择游戏路径 profile.manage=游戏目录列表 profile.name=名称 -profile.new=新建配置 +profile.new=添加游戏目录 profile.title=游戏目录 profile.selected=已选中 profile.use_relative_path=若可能,游戏目录使用相对路径 @@ -468,6 +468,7 @@ version.manage.duplicate.duplicate_save=复制存档 version.manage.duplicate.prompt=请输入新游戏实例名称 version.manage.duplicate.confirm=将锁定复制产生的新游戏实例:强制版本隔离、游戏设置独立。 version.manage.manage=游戏管理 +version.manage.manage.title=游戏管理 - %1s version.manage.redownload_assets_index=更新游戏资源文件 version.manage.remove=删除该版本 version.manage.remove.confirm=真的要删除版本 %s 吗?你将无法找回被删除的文件!