feat: refine mod list ui.
This commit is contained in:
@@ -59,6 +59,7 @@ import java.io.*;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BooleanSupplier;
|
||||
@@ -363,6 +364,21 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void showFileInExplorer(Path file) {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select", e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Currently unsupported.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String[] linuxBrowsers = {
|
||||
"xdg-open",
|
||||
"google-chrome",
|
||||
|
||||
@@ -99,6 +99,14 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton2(String text, SVG.SVGIcon creator, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(creator.createIcon(Theme.blackFillBinding(), -1, -1)));
|
||||
ret.setText(text);
|
||||
ret.setOnMouseClicked(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-decorator-button");
|
||||
|
||||
@@ -133,8 +133,8 @@ public class ComponentList extends Control {
|
||||
list = MappedObservableList.create(control.getContent(), node -> {
|
||||
ComponentListCell cell = new ComponentListCell(node);
|
||||
cell.getStyleClass().add("options-list-item");
|
||||
if (node.getProperties().containsKey("vgrow")) {
|
||||
VBox.setVgrow(cell, Priority.ALWAYS);
|
||||
if (node.getProperties().containsKey("ComponentList.vgrow")) {
|
||||
VBox.setVgrow(cell, (Priority) node.getProperties().get("ComponentList.vgrow"));
|
||||
}
|
||||
return cell;
|
||||
});
|
||||
@@ -176,4 +176,8 @@ public class ComponentList extends Control {
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
public static void setVgrow(Node node, Priority priority) {
|
||||
node.getProperties().put("ComponentList.vgrow", priority);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public abstract class MDListCell<T> extends ListCell<T> {
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
|
||||
public MDListCell() {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(container);
|
||||
root.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
updateControl(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(root);
|
||||
}
|
||||
}
|
||||
|
||||
protected StackPane getContainer() {
|
||||
return container;
|
||||
}
|
||||
|
||||
protected void setSelectable() {
|
||||
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
|
||||
root.pseudoClassStateChanged(SELECTED, selected);
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void updateControl(T item, boolean empty);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import javafx.scene.control.ListCell;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
@@ -105,7 +106,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
} else {
|
||||
centrePane.getContent().setAll(list);
|
||||
}
|
||||
list.getProperties().put("vgrow", true);
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
InvalidationListener listener = o -> list.getItems().setAll(loadVersions());
|
||||
chkRelease.selectedProperty().addListener(listener);
|
||||
|
||||
@@ -54,6 +54,8 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
|
||||
private ModManager modManager;
|
||||
private LibraryAnalyzer libraryAnalyzer;
|
||||
private Profile profile;
|
||||
private String versionId;
|
||||
|
||||
public ModListPage() {
|
||||
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
|
||||
@@ -79,6 +81,9 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
|
||||
@Override
|
||||
public void loadVersion(Profile profile, String id) {
|
||||
this.profile = profile;
|
||||
this.versionId = versionId;
|
||||
|
||||
libraryAnalyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id));
|
||||
modded.set(libraryAnalyzer.hasModLoader());
|
||||
loadMods(profile.getRepository().getModManager(id));
|
||||
@@ -167,6 +172,10 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
.forEach(info -> info.setActive(false));
|
||||
}
|
||||
|
||||
public void openModFolder() {
|
||||
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(versionId), "mods"));
|
||||
}
|
||||
|
||||
public boolean isModded() {
|
||||
return modded.get();
|
||||
}
|
||||
|
||||
@@ -22,16 +22,15 @@ 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;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
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.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -42,10 +41,7 @@ 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.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -59,7 +55,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
|
||||
@@ -71,42 +67,44 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
super(skinnable);
|
||||
|
||||
StackPane pane = new StackPane();
|
||||
pane.setPadding(new Insets(10));
|
||||
pane.getStyleClass().addAll("notice-pane");
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
JFXListView<ModInfoObject> listView = new JFXListView<>();
|
||||
|
||||
{
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.getStyleClass().add("jfx-tool-bar-second");
|
||||
JFXDepthManager.setDepth(toolbar, 1);
|
||||
toolbar.setPickOnBounds(false);
|
||||
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("mods.add"), SVG::plus, skinnable::add));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("button.remove"), SVG::delete, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
}));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG::check, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG::close, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())));
|
||||
root.setTop(toolbar);
|
||||
toolbar.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
|
||||
createToolbarButton2(i18n("mods.add"), SVG::plus, skinnable::add),
|
||||
createToolbarButton2(i18n("button.remove"), SVG::delete, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
}),
|
||||
createToolbarButton2(i18n("mods.enable"), SVG::check, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("mods.disable"), SVG::close, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, () ->
|
||||
skinnable.openModFolder()));
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView));
|
||||
listView.setCellFactory(x -> new ModInfoListCell());
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
|
||||
center.setContent(listView);
|
||||
root.setCenter(center);
|
||||
root.getContent().add(center);
|
||||
}
|
||||
|
||||
Label label = new Label(i18n("mods.not_modded"));
|
||||
@@ -231,23 +229,31 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
static class ModInfoListCell extends FloatListCell<ModInfoObject> {
|
||||
static class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
JFXButton infoButton = new JFXButton();
|
||||
JFXButton revealButton = new JFXButton();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView) {
|
||||
super(listView);
|
||||
ModInfoListCell() {
|
||||
HBox container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
pane.getChildren().add(container);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
|
||||
infoButton.getStyleClass().add("toggle-icon4");
|
||||
infoButton.setGraphic(FXUtils.limitingSize(SVG.informationOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
|
||||
container.getChildren().setAll(checkBox, content, infoButton);
|
||||
container.getChildren().setAll(checkBox, content, revealButton, infoButton);
|
||||
|
||||
StackPane.setMargin(container, new Insets(10, 16, 10, 16));
|
||||
getContainer().getChildren().setAll(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -259,6 +265,9 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
||||
revealButton.setOnMouseClicked(e -> {
|
||||
FXUtils.showFileInExplorer(dataItem.getModInfo().getFile());
|
||||
});
|
||||
infoButton.setOnMouseClicked(e -> {
|
||||
Controllers.dialog(new ModInfoDialog(dataItem));
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -793,6 +793,10 @@
|
||||
-fx-border-width: 0 0 1 0;
|
||||
}
|
||||
|
||||
.md-list-cell:selected {
|
||||
-fx-background-color: derive(-fx-base-color, 60%);
|
||||
}
|
||||
|
||||
.options-sublist {
|
||||
-fx-background-color: white;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ account.methods.microsoft=Microsoft Account
|
||||
account.methods.microsoft.close_page=Microsoft account authorization has been finished. There are some remaining logging-in steps to be finished later. You can close this page right now.
|
||||
account.methods.microsoft.error.add_family=Since you are not yet 18 years old, an adult must add you to a family in order for you to play Minecraft.
|
||||
account.methods.microsoft.error.missing_xbox_account=Your Microsoft account is not connected to an Xbox account. Please create one before continuing.
|
||||
account.methods.microsoft.error.no_character=Account is missing a Minecraft Java profile. While the Microsoft account is valid, it does not own the game.
|
||||
account.methods.microsoft.error.unknown=Failed to log in. Microsoft respond with error code %d.
|
||||
account.methods.microsoft.logging_in=Logging in...
|
||||
account.methods.microsoft.manual=You should finish authorization in the newly opened browser window. If the browser window failed to show, you can click here to copy the URL, and manually open it in your browser.
|
||||
@@ -505,6 +506,7 @@ selector.custom=Custom
|
||||
settings=Settings
|
||||
|
||||
settings.advanced=Advanced Settings
|
||||
settings.advanced.custom_commands=Custom Commands
|
||||
settings.advanced.dont_check_game_completeness=Do not scan game files
|
||||
settings.advanced.dont_check_jvm_validity=Don't check whether JVM can launch the game or not
|
||||
settings.advanced.game_dir.default=Standard (.minecraft/)
|
||||
|
||||
@@ -61,6 +61,7 @@ account.methods.microsoft=微軟帳戶
|
||||
account.methods.microsoft.close_page=已完成微軟帳號授權,接下來啟動器還需要完成剩餘登錄步驟。你已經可以關閉本頁面了。
|
||||
account.methods.microsoft.error.add_family=由於你未滿 18 歲,你的帳號必須被加入到家庭中才能登錄遊戲。
|
||||
account.methods.microsoft.error.missing_xbox_account=你的微軟帳號尚未關聯 XBox 帳號,你必須先創建 XBox 帳號,才能登錄遊戲。
|
||||
account.methods.microsoft.error.no_character=該帳號沒有包含 Minecraft Java 版購買記錄
|
||||
account.methods.microsoft.error.unknown=登錄失敗,錯誤碼:%d
|
||||
account.methods.microsoft.logging_in=登錄中...
|
||||
account.methods.microsoft.hint=點擊確定以登錄
|
||||
|
||||
@@ -74,6 +74,7 @@ account.methods.microsoft=微软账户
|
||||
account.methods.microsoft.close_page=已完成微软账号授权,接下来启动器还需要完成剩余登录步骤。你已经可以关闭本页面了。
|
||||
account.methods.microsoft.error.add_family=由于你未满 18 岁,你的账号必须被加入到家庭中才能登录游戏。
|
||||
account.methods.microsoft.error.missing_xbox_account=你的微软账号尚未关联 XBox 账号,你必须先创建 XBox 账号,才能登录游戏。
|
||||
account.methods.microsoft.error.no_character=该账号没有包含 Minecraft Java 版购买记录
|
||||
account.methods.microsoft.error.unknown=登录失败,错误码:%d
|
||||
account.methods.microsoft.logging_in=登录中...
|
||||
account.methods.microsoft.hint=点击确定以登录
|
||||
@@ -460,6 +461,16 @@ mods.name=名称
|
||||
mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。
|
||||
mods.url=官方页面
|
||||
|
||||
multiplayer=联机
|
||||
multiplayer.nat=网络检测
|
||||
multiplayer.nat.hint=执行网络检测可以让你更清楚里的网络状况是否符合联机功能的需求。不符合联机功能运行条件的网络状况将可能导致联机失败。
|
||||
multiplayer.nat.latency=延迟
|
||||
multiplayer.nat.not_yet_tested=尚未检测
|
||||
multiplayer.nat.packet_loss_ratio=丢包率
|
||||
multiplayer.nat.testing=检测中
|
||||
multiplayer.nat.type=NAT 类型
|
||||
multiplayer.room=房间
|
||||
|
||||
datapack=数据包
|
||||
datapack.add=添加数据包
|
||||
datapack.choose_datapack=选择要导入的数据包压缩包
|
||||
|
||||
Reference in New Issue
Block a user