feat: display mod description and website url. Closes #945.
This commit is contained in:
@@ -258,4 +258,8 @@ public final class SVG {
|
||||
public static Node releaseCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node informationOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z", fill, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
@@ -30,7 +32,7 @@ public abstract class FloatListCell<T> extends ListCell<T> {
|
||||
|
||||
protected final StackPane pane = new StackPane();
|
||||
|
||||
{
|
||||
public FloatListCell(JFXListView<T> listView) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
|
||||
@@ -42,6 +44,14 @@ public abstract class FloatListCell<T> extends ListCell<T> {
|
||||
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
|
||||
pane.pseudoClassStateChanged(SELECTED, selected);
|
||||
});
|
||||
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
setPrefWidth(0);
|
||||
if (clippedContainer != null) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
minWidthProperty().bind(clippedContainer.widthProperty());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -119,7 +119,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
chkSnapshot.selectedProperty().addListener(listener);
|
||||
chkOld.selectedProperty().addListener(listener);
|
||||
|
||||
list.setCellFactory(listView -> new FloatListCell<RemoteVersion>() {
|
||||
list.setCellFactory(listView -> new FloatListCell<RemoteVersion>(list) {
|
||||
ImageView imageView = new ImageView();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new FloatListCell<DatapackInfoObject>() {
|
||||
listView.setCellFactory(x -> new FloatListCell<DatapackInfoObject>(listView) {
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
@@ -250,7 +250,7 @@ public class ModDownloadListPage extends Control implements DecoratorPage, Versi
|
||||
CurseAddon selectedItem = listView.getSelectionModel().getSelectedItem();
|
||||
Controllers.navigate(new ModDownloadPage(selectedItem, getSkinnable().version.get(), getSkinnable().callback));
|
||||
});
|
||||
listView.setCellFactory(x -> new FloatListCell<CurseAddon>() {
|
||||
listView.setCellFactory(x -> new FloatListCell<CurseAddon>(listView) {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
ImageView imageView = new ImageView();
|
||||
|
||||
|
||||
@@ -191,22 +191,14 @@ public class ModDownloadPage extends Control implements DecoratorPage {
|
||||
JFXListView<CurseAddon.LatestFile> listView = new JFXListView<>();
|
||||
spinnerPane.setContent(listView);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().items);
|
||||
listView.setCellFactory(x -> new FloatListCell<CurseAddon.LatestFile>() {
|
||||
listView.setCellFactory(x -> new FloatListCell<CurseAddon.LatestFile>(listView) {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
StackPane graphicPane = new StackPane();
|
||||
|
||||
{
|
||||
Region clippedContainer = (Region)listView.lookup(".clipped-container");
|
||||
setPrefWidth(0);
|
||||
HBox container = new HBox(8);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
pane.getChildren().add(container);
|
||||
if (clippedContainer != null) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
minWidthProperty().bind(clippedContainer.widthProperty());
|
||||
}
|
||||
|
||||
container.getChildren().setAll(graphicPane, content);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
@@ -27,20 +29,38 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.mod.ModInfo;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.FloatListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -80,37 +100,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new FloatListCell<ModInfoObject>() {
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
{
|
||||
Region clippedContainer = (Region)listView.lookup(".clipped-container");
|
||||
setPrefWidth(0);
|
||||
HBox container = new HBox(8);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
pane.getChildren().add(container);
|
||||
if (clippedContainer != null) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
minWidthProperty().bind(clippedContainer.widthProperty());
|
||||
}
|
||||
|
||||
container.getChildren().setAll(checkBox, content);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
||||
if (empty) return;
|
||||
content.setTitle(dataItem.getTitle());
|
||||
content.setSubtitle(dataItem.getSubtitle());
|
||||
if (booleanProperty != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
||||
}
|
||||
});
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
|
||||
@@ -164,4 +154,111 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
return modInfo.getFileName().toLowerCase().compareTo(o.modInfo.getFileName().toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
static class ModInfoDialog extends JFXDialogLayout {
|
||||
|
||||
ModInfoDialog(ModInfoObject modInfo) {
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getLogoPath())) {
|
||||
Task.supplyAsync(() -> {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
|
||||
Path iconPath = fs.getPath(modInfo.getModInfo().getLogoPath());
|
||||
if (Files.exists(iconPath)) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
Files.copy(iconPath, stream);
|
||||
return new ByteArrayInputStream(stream.toByteArray());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}).whenComplete(Schedulers.javafx(), (stream, exception) -> {
|
||||
if (stream != null) {
|
||||
imageView.setImage(new Image(stream, 40, 40, true, true));
|
||||
} else {
|
||||
imageView.setImage(new Image("/assets/img/command.png", 40, 40, true, true));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
||||
title.getTags().setAll(modInfo.getModInfo().getVersion());
|
||||
}
|
||||
title.setSubtitle(FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
|
||||
titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title);
|
||||
setHeading(titleContainer);
|
||||
|
||||
Label description = new Label(modInfo.getModInfo().getDescription().toString());
|
||||
setBody(description);
|
||||
|
||||
JFXButton okButton = new JFXButton();
|
||||
okButton.getStyleClass().add("dialog-accept");
|
||||
okButton.setText(i18n("button.ok"));
|
||||
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
|
||||
JFXButton searchButton = new JFXButton();
|
||||
searchButton.getStyleClass().add("dialog-cancel");
|
||||
searchButton.setText(i18n("mods.mcmod.search"));
|
||||
searchButton.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
FXUtils.openLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf(
|
||||
pair("key", modInfo.getModInfo().getName()),
|
||||
pair("site", "all"),
|
||||
pair("filter", "0")
|
||||
)));
|
||||
});
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getUrl())) {
|
||||
JFXButton officialPageButton = new JFXButton();
|
||||
officialPageButton.getStyleClass().add("dialog-cancel");
|
||||
officialPageButton.setText(i18n("mods.url"));
|
||||
officialPageButton.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
FXUtils.openLink(modInfo.getModInfo().getUrl());
|
||||
});
|
||||
|
||||
setActions(okButton, officialPageButton, searchButton);
|
||||
} else {
|
||||
setActions(okButton, searchButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ModInfoListCell extends FloatListCell<ModInfoObject> {
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
JFXButton infoButton = new JFXButton();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView) {
|
||||
super(listView);
|
||||
HBox container = new HBox(8);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
pane.getChildren().add(container);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
|
||||
infoButton.getStyleClass().add("toggle-icon4");
|
||||
infoButton.setGraphic(FXUtils.limitingSize(SVG.informationOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
|
||||
container.getChildren().setAll(checkBox, content, infoButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
||||
if (empty) return;
|
||||
content.setTitle(dataItem.getTitle());
|
||||
content.setSubtitle(dataItem.getSubtitle());
|
||||
if (booleanProperty != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
||||
infoButton.setOnMouseClicked(e -> {
|
||||
Controllers.dialog(new ModInfoDialog(dataItem));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +188,16 @@
|
||||
|
||||
.two-line-list-item > HBox > .subtitle {
|
||||
-fx-text-fill: rgba(0, 0, 0, 0.5);
|
||||
-fx-font-weight: normal;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.two-line-list-item > HBox > HBox > .tag {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-background-color: -fx-base-rippler-color;
|
||||
-fx-padding: 2;
|
||||
-fx-font-weight: normal;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
|
||||
@@ -377,11 +377,15 @@ mods.add=Install mods
|
||||
mods.add.failed=Failed to install mods %s.
|
||||
mods.add.success=Successfully installed mods %s.
|
||||
mods.choose_mod=Choose your mods
|
||||
mods.download=Mod Downloads
|
||||
mods.enable=Enable
|
||||
mods.disable=Disable
|
||||
mods.download=Mod Downloads
|
||||
mods.download.title=Mod Downloads - %1s
|
||||
mods.enable=Enable
|
||||
mods.mcmod.page=MCWiki
|
||||
mods.mcmod.search=Search in MCWiki
|
||||
mods.name=Name
|
||||
mods.not_modded=You should install a modloader first (Fabric, Forge or LiteLoader)
|
||||
mods.url=Official Page
|
||||
|
||||
datapack=Datapacks
|
||||
datapack.add=Install datapack
|
||||
|
||||
@@ -376,14 +376,17 @@ mods=模组管理
|
||||
mods.add=添加模组
|
||||
mods.add.failed=添加模组 %s 失败。
|
||||
mods.add.success=成功添加模组 %s。
|
||||
mods.category=类别
|
||||
mods.choose_mod=选择模组
|
||||
mods.disable=禁用
|
||||
mods.download=模组下载
|
||||
mods.download.title=模组下载 - %1s
|
||||
mods.enable=启用
|
||||
mods.disable=禁用
|
||||
mods.mcmod.page=MC 百科页面
|
||||
mods.mcmod.search=MC 百科搜索
|
||||
mods.name=名称
|
||||
mods.category=类别
|
||||
mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。
|
||||
mods.url=官方页面
|
||||
|
||||
datapack=数据包
|
||||
datapack.add=添加数据包
|
||||
|
||||
Reference in New Issue
Block a user