使用 LineButton 代替 IconedTwoLineListItem (#5446)

This commit is contained in:
Glavo
2026-02-05 22:15:53 +08:00
committed by GitHub
parent cb413fa40a
commit ab9c45549b
6 changed files with 94 additions and 235 deletions

View File

@@ -1,118 +0,0 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.StringUtils;
public class IconedTwoLineListItem extends HBox {
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObservableList<Label> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final StringProperty externalLink = new SimpleStringProperty(this, "externalLink");
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ImageView imageView = new ImageView();
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
private JFXButton externalLinkButton;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener observer;
public IconedTwoLineListItem() {
setAlignment(Pos.CENTER);
setSpacing(16);
imageView.imageProperty().bind(image);
twoLineListItem.titleProperty().bind(title);
twoLineListItem.subtitleProperty().bind(subtitle);
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
Bindings.bindContent(twoLineListItem.getTags(), tags);
observer = FXUtils.observeWeak(() -> {
getChildren().clear();
if (image.get() != null) getChildren().add(imageView);
getChildren().add(twoLineListItem);
if (StringUtils.isNotBlank(externalLink.get())) getChildren().add(getExternalLinkButton());
}, image, externalLink);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public ObservableList<Label> getTags() {
return tags;
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public String getExternalLink() {
return externalLink.get();
}
public StringProperty externalLinkProperty() {
return externalLink;
}
public void setExternalLink(String externalLink) {
this.externalLink.set(externalLink);
}
public Image getImage() {
return image.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public void setImage(Image image) {
this.image.set(image);
}
public ImageView getImageView() {
return imageView;
}
public JFXButton getExternalLinkButton() {
if (externalLinkButton == null) {
externalLinkButton = new JFXButton();
externalLinkButton.getStyleClass().add("toggle-icon4");
externalLinkButton.setGraphic(SVG.OPEN_IN_NEW.createIcon());
externalLinkButton.setOnAction(e -> FXUtils.openLink(externalLink.get()));
}
return externalLinkButton;
}
}

View File

@@ -23,6 +23,7 @@ import javafx.event.EventHandler;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle; import javafx.scene.control.OverrunStyle;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
/// @author Glavo /// @author Glavo
@@ -38,6 +39,15 @@ public class LineButton extends LineButtonBase {
return button; return button;
} }
public static LineButton createExternalLinkButton(String url) {
var button = new LineButton();
button.setTrailingIcon(SVG.OPEN_IN_NEW);
if (url != null) {
button.setOnAction(event -> FXUtils.openLink(url));
}
return button;
}
public LineButton() { public LineButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS); getStyleClass().add(DEFAULT_STYLE_CLASS);
container.setMouseTransparent(true); container.setMouseTransparent(true);

View File

@@ -17,11 +17,7 @@
*/ */
package org.jackhuang.hmcl.ui.main; package org.jackhuang.hmcl.ui.main;
import com.google.gson.JsonArray; import com.google.gson.*;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image; import javafx.scene.image.Image;
@@ -30,8 +26,10 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.theme.Themes; import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem; import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException; import java.io.IOException;
@@ -42,20 +40,22 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class AboutPage extends StackPane { public final class AboutPage extends StackPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
public AboutPage() { public AboutPage() {
ComponentList about = new ComponentList(); ComponentList about = new ComponentList();
{ {
IconedTwoLineListItem launcher = new IconedTwoLineListItem(); var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
launcher.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png")); launcher.setLargeTitle(true);
launcher.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
launcher.setTitle("Hello Minecraft! Launcher"); launcher.setTitle("Hello Minecraft! Launcher");
launcher.setSubtitle(Metadata.VERSION); launcher.setSubtitle(Metadata.VERSION);
launcher.setExternalLink(Metadata.PUBLISH_URL);
IconedTwoLineListItem author = new IconedTwoLineListItem(); var author = LineButton.createExternalLinkButton("https://space.bilibili.com/1445341");
author.setImage(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png")); author.setLargeTitle(true);
author.setLeading(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png"));
author.setTitle("huanghongxun"); author.setTitle("huanghongxun");
author.setSubtitle(i18n("about.author.statement")); author.setSubtitle(i18n("about.author.statement"));
author.setExternalLink("https://space.bilibili.com/1445341");
about.getContent().setAll(launcher, author); about.getContent().setAll(launcher, author);
} }
@@ -66,20 +66,20 @@ public final class AboutPage extends StackPane {
ComponentList legal = new ComponentList(); ComponentList legal = new ComponentList();
{ {
IconedTwoLineListItem copyright = new IconedTwoLineListItem(); var copyright = LineButton.createExternalLinkButton(Metadata.ABOUT_URL);
copyright.setLargeTitle(true);
copyright.setTitle(i18n("about.copyright")); copyright.setTitle(i18n("about.copyright"));
copyright.setSubtitle(i18n("about.copyright.statement")); copyright.setSubtitle(i18n("about.copyright.statement"));
copyright.setExternalLink(Metadata.ABOUT_URL);
IconedTwoLineListItem claim = new IconedTwoLineListItem(); var claim = LineButton.createExternalLinkButton(Metadata.EULA_URL);
claim.setLargeTitle(true);
claim.setTitle(i18n("about.claim")); claim.setTitle(i18n("about.claim"));
claim.setSubtitle(i18n("about.claim.statement")); claim.setSubtitle(i18n("about.claim.statement"));
claim.setExternalLink(Metadata.EULA_URL);
IconedTwoLineListItem openSource = new IconedTwoLineListItem(); var openSource = LineButton.createExternalLinkButton("https://github.com/HMCL-dev/HMCL");
openSource.setLargeTitle(true);
openSource.setTitle(i18n("about.open_source")); openSource.setTitle(i18n("about.open_source"));
openSource.setSubtitle(i18n("about.open_source.statement")); openSource.setSubtitle(i18n("about.open_source.statement"));
openSource.setExternalLink("https://github.com/HMCL-dev/HMCL");
legal.getContent().setAll(copyright, claim, openSource); legal.getContent().setAll(copyright, claim, openSource);
} }
@@ -113,7 +113,7 @@ public final class AboutPage extends StackPane {
: new Image(url); : new Image(url);
} }
private static ComponentList loadIconedTwoLineList(String path) { private ComponentList loadIconedTwoLineList(String path) {
ComponentList componentList = new ComponentList(); ComponentList componentList = new ComponentList();
InputStream input = FXUtils.class.getResourceAsStream(path); InputStream input = FXUtils.class.getResourceAsStream(path);
@@ -127,36 +127,42 @@ public final class AboutPage extends StackPane {
for (JsonElement element : array) { for (JsonElement element : array) {
JsonObject obj = element.getAsJsonObject(); JsonObject obj = element.getAsJsonObject();
IconedTwoLineListItem item = new IconedTwoLineListItem();
var button = new LineButton();
button.setLargeTitle(true);
if (obj.get("externalLink") instanceof JsonPrimitive externalLink) {
button.setTrailingIcon(SVG.OPEN_IN_NEW);
String link = externalLink.getAsString();
button.setOnAction(event -> FXUtils.openLink(link));
}
if (obj.has("image")) { if (obj.has("image")) {
JsonElement image = obj.get("image"); JsonElement image = obj.get("image");
if (image.isJsonPrimitive()) { if (image.isJsonPrimitive()) {
item.setImage(loadImage(image.getAsString())); button.setLeading(loadImage(image.getAsString()));
} else if (image.isJsonObject()) { } else if (image.isJsonObject()) {
item.imageProperty().bind(Bindings.when(Themes.darkModeProperty()) holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
.then(loadImage(image.getAsJsonObject().get("dark").getAsString())) button.setLeading(darkMode
.otherwise(loadImage(image.getAsJsonObject().get("light").getAsString()))); ? loadImage(image.getAsJsonObject().get("dark").getAsString())
: loadImage(image.getAsJsonObject().get("light").getAsString())
);
}));
} }
} }
if (obj.has("title")) if (obj.get("title") instanceof JsonPrimitive title)
item.setTitle(obj.get("title").getAsString()); button.setTitle(title.getAsString());
else if (obj.has("titleLocalized")) else if (obj.get("titleLocalized") instanceof JsonPrimitive titleLocalized)
item.setTitle(i18n(obj.get("titleLocalized").getAsString())); button.setTitle(i18n(titleLocalized.getAsString()));
if (obj.has("subtitle")) if (obj.get("subtitle") instanceof JsonPrimitive subtitle)
item.setSubtitle(obj.get("subtitle").getAsString()); button.setSubtitle(subtitle.getAsString());
else if (obj.has("subtitleLocalized")) else if (obj.get("subtitleLocalized") instanceof JsonPrimitive subtitleLocalized)
item.setSubtitle(i18n(obj.get("subtitleLocalized").getAsString())); button.setSubtitle(i18n(subtitleLocalized.getAsString()));
if (obj.has("externalLink")) { componentList.getContent().add(button);
String link = obj.get("externalLink").getAsString();
item.setExternalLink(link);
FXUtils.installFastTooltip(item.getExternalLinkButton(), link);
}
componentList.getContent().add(item);
} }
} catch (IOException | JsonParseException e) { } catch (IOException | JsonParseException e) {
LOG.warning("Failed to load list: " + path, e); LOG.warning("Failed to load list: " + path, e);

View File

@@ -17,14 +17,14 @@
*/ */
package org.jackhuang.hmcl.ui.main; package org.jackhuang.hmcl.ui.main;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.theme.Themes; import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem; import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -33,6 +33,8 @@ import org.jackhuang.hmcl.Metadata;
public class FeedbackPage extends SpinnerPane { public class FeedbackPage extends SpinnerPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
public FeedbackPage() { public FeedbackPage() {
VBox content = new VBox(); VBox content = new VBox();
content.setPadding(new Insets(10)); content.setPadding(new Insets(10));
@@ -45,30 +47,33 @@ public class FeedbackPage extends SpinnerPane {
ComponentList groups = new ComponentList(); ComponentList groups = new ComponentList();
{ {
IconedTwoLineListItem users = new IconedTwoLineListItem(); var users = LineButton.createExternalLinkButton(Metadata.GROUPS_URL);
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png")); users.setLargeTitle(true);
users.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
users.setTitle(i18n("contact.chat.qq_group")); users.setTitle(i18n("contact.chat.qq_group"));
users.setSubtitle(i18n("contact.chat.qq_group.statement")); users.setSubtitle(i18n("contact.chat.qq_group.statement"));
users.setExternalLink(Metadata.GROUPS_URL);
IconedTwoLineListItem discord = new IconedTwoLineListItem(); var discord = LineButton.createExternalLinkButton("https://discord.gg/jVvC7HfM6U");
discord.setImage(FXUtils.newBuiltinImage("/assets/img/discord.png")); discord.setLargeTitle(true);
discord.setLeading(FXUtils.newBuiltinImage("/assets/img/discord.png"));
discord.setTitle(i18n("contact.chat.discord")); discord.setTitle(i18n("contact.chat.discord"));
discord.setSubtitle(i18n("contact.chat.discord.statement")); discord.setSubtitle(i18n("contact.chat.discord.statement"));
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
groups.getContent().setAll(users, discord); groups.getContent().setAll(users, discord);
} }
ComponentList feedback = new ComponentList(); ComponentList feedback = new ComponentList();
{ {
IconedTwoLineListItem github = new IconedTwoLineListItem(); var github = LineButton.createExternalLinkButton("https://github.com/HMCL-dev/HMCL/issues/new/choose");
github.imageProperty().bind(Bindings.when(Themes.darkModeProperty()) github.setLargeTitle(true);
.then(FXUtils.newBuiltinImage("/assets/img/github-white.png"))
.otherwise(FXUtils.newBuiltinImage("/assets/img/github.png")));
github.setTitle(i18n("contact.feedback.github")); github.setTitle(i18n("contact.feedback.github"));
github.setSubtitle(i18n("contact.feedback.github.statement")); github.setSubtitle(i18n("contact.feedback.github.statement"));
github.setExternalLink("https://github.com/HMCL-dev/HMCL/issues/new/choose");
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
github.setLeading(darkMode
? FXUtils.newBuiltinImage("/assets/img/github-white.png")
: FXUtils.newBuiltinImage("/assets/img/github.png"));
}));
feedback.getContent().setAll(github); feedback.getContent().setAll(github);
} }

View File

@@ -26,11 +26,11 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem; import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.HttpRequest;
import java.util.Collections;
import java.util.List; import java.util.List;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
@@ -50,10 +50,11 @@ public class HelpPage extends SpinnerPane {
FXUtils.smoothScrolling(scrollPane); FXUtils.smoothScrolling(scrollPane);
setContent(scrollPane); setContent(scrollPane);
IconedTwoLineListItem docPane = new IconedTwoLineListItem(); var docPane = LineButton.createExternalLinkButton(Metadata.DOCS_URL);
docPane.setLargeTitle(true);
docPane.setTitle(i18n("help.doc")); docPane.setTitle(i18n("help.doc"));
docPane.setSubtitle(i18n("help.detail")); docPane.setSubtitle(i18n("help.detail"));
docPane.setExternalLink(Metadata.DOCS_URL);
ComponentList doc = new ComponentList(); ComponentList doc = new ComponentList();
doc.getContent().setAll(docPane); doc.getContent().setAll(docPane);
content.getChildren().add(doc); content.getChildren().add(doc);
@@ -68,77 +69,32 @@ public class HelpPage extends SpinnerPane {
for (HelpCategory category : helpCategories) { for (HelpCategory category : helpCategories) {
ComponentList categoryPane = new ComponentList(); ComponentList categoryPane = new ComponentList();
for (Help help : category.getItems()) { for (Help help : category.items()) {
IconedTwoLineListItem item = new IconedTwoLineListItem(); var item = LineButton.createExternalLinkButton(help.url());
item.setTitle(help.getTitle()); item.setLargeTitle(true);
item.setSubtitle(help.getSubtitle()); item.setTitle(help.title());
item.setExternalLink(help.getUrl()); item.setSubtitle(help.subtitle());
categoryPane.getContent().add(item); categoryPane.getContent().add(item);
} }
content.getChildren().add(ComponentList.createComponentListTitle(category.title)); content.getChildren().add(ComponentList.createComponentListTitle(category.title()));
content.getChildren().add(categoryPane); content.getChildren().add(categoryPane);
} }
hideSpinner(); hideSpinner();
}).start(); }).start();
} }
private static class HelpCategory { @JsonSerializable
@SerializedName("title") private record HelpCategory(
private final String title; @SerializedName("title") String title,
@SerializedName("items") List<Help> items) {
@SerializedName("items")
private final List<Help> items;
public HelpCategory() {
this("", Collections.emptyList());
} }
public HelpCategory(String title, List<Help> items) { @JsonSerializable
this.title = title; private record Help(
this.items = items; @SerializedName("title") String title,
} @SerializedName("subtitle") String subtitle,
@SerializedName("url") String url) {
public String getTitle() {
return title;
}
public List<Help> getItems() {
return items;
}
}
private static class Help {
@SerializedName("title")
private final String title;
@SerializedName("subtitle")
private final String subtitle;
@SerializedName("url")
private final String url;
public Help() {
this("", "", "");
}
public Help(String title, String subtitle, String url) {
this.title = title;
this.subtitle = subtitle;
this.url = url;
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public String getUrl() {
return url;
}
} }
} }

View File

@@ -76,7 +76,7 @@
}, },
{ {
"title": "EasyTier", "title": "EasyTier",
"subtitle": "Copyright 2024-present Easytier Programme within The Commons Conservancy", "subtitle": "Copyright 2024-present Easytier Programme within The Commons Conservancy.\nLicensed under the LGPL 3.0 License.",
"externalLink": "https://github.com/EasyTier/EasyTier" "externalLink": "https://github.com/EasyTier/EasyTier"
}, },
{ {