Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,29 @@
import javafx.scene.control.TreeItem;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.GameInstanceID;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.modpack.ModAdviser;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Lang.mapOf;
Expand All @@ -59,43 +64,75 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
private final WizardController controller;
private final GameInstanceID instanceId;
private final ModAdviser adviser;
private final ModpackFileTreeItem rootNode;
private @Nullable ModpackFileTreeItem rootNode;

public ModpackFileSelectionPage(WizardController controller, HMCLGameRepository repository, GameInstanceID instanceId, ModAdviser adviser) {
this.controller = controller;
this.instanceId = instanceId;
this.adviser = adviser;

JFXTreeView<String> treeView = new JFXTreeView<>();
rootNode = getTreeItem(repository.getRunDirectory(instanceId), "minecraft", 0);
treeView.setRoot(rootNode);
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
onEscPressed(treeView, () -> controller.onPrev(true));
setMargin(treeView, new Insets(10, 10, 5, 10));
this.setCenter(treeView);

Label placeholder = new Label(i18n("modpack.files.empty"));
StackPane placeholderPane = new StackPane(placeholder);
placeholderPane.getStyleClass().add("notice-pane");
placeholderPane.setVisible(false);

SpinnerPane spinnerPane = new SpinnerPane();
StackPane center = new StackPane(treeView, placeholderPane);
spinnerPane.setContent(center);

setMargin(spinnerPane, new Insets(10, 10, 5, 10));
this.setCenter(spinnerPane);


HBox nextPane = new HBox();
nextPane.setPadding(new Insets(16, 16, 16, 0));
nextPane.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton btnNext = FXUtils.newRaisedButton(i18n("wizard.next"));
btnNext.setPrefSize(100, 40);
btnNext.setOnAction(e -> onNext());

nextPane.getChildren().setAll(btnNext);
}
JFXButton btnNext = FXUtils.newRaisedButton(i18n("wizard.next"));
btnNext.setPrefSize(100, 40);
btnNext.setOnAction(e -> onNext());
nextPane.getChildren().setAll(btnNext);

loadRoot(repository, treeView, placeholderPane, spinnerPane, btnNext);
spinnerPane.setOnFailedAction((__) -> loadRoot(repository, treeView, placeholderPane, spinnerPane, btnNext));

this.setBottom(nextPane);
}

private void loadRoot(HMCLGameRepository repository, JFXTreeView<String> treeView, StackPane placeholderPane, SpinnerPane spinnerPane, JFXButton btnNext) {
spinnerPane.setLoading(true);
btnNext.setDisable(true);
CompletableFuture
.supplyAsync(() -> getTreeItem(repository.getRunDirectory(instanceId), "minecraft", 0), Schedulers.io())
.whenCompleteAsync((root, throwable) -> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Annotate the nullable completion parameters

The newly introduced root and throwable lambda parameters are both nullable—this branch explicitly tests each for null—but neither is annotated, and the nullable return contract of getTreeItem remains implicit. Explicitly type and annotate these parameters and the method return so nullability analysis accurately models the asynchronous result.

AGENTS.md reference: AGENTS.md:L8-L9

Useful? React with 👍 / 👎.

if (throwable == null) {
if (root != null) {
treeView.setRoot(rootNode = root);
} else {
placeholderPane.setVisible(true);
Comment on lines +115 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface root-directory I/O failures instead of empty state

When Files.list fails on the run directory, such as from permissions or a transient I/O error, getTreeItem catches the IOException and then returns null because the root has no children. This new branch consequently reports “No files to export,” clears the failure state, and enables Next, so the added retry UI is never offered and the user can produce an empty export. Propagate or otherwise distinguish the top-level listing failure before selecting the empty-state path.

Useful? React with 👍 / 👎.

}
spinnerPane.setFailedReason(null);
btnNext.setDisable(false);
} else {
LOG.warning("Failed to load modpack file tree", throwable);
spinnerPane.setFailedReason(i18n("modpack.files.load_failed"));
}
spinnerPane.setLoading(false);
}, Schedulers.javafx());
}

private ModpackFileTreeItem getTreeItem(Path file, String basePath, int level) {
if (Files.notExists(file))
return null;

boolean isDirectory = Files.isDirectory(file);

ModAdviser.ModSuggestion state = ModAdviser.ModSuggestion.SUGGESTED;
if (basePath.length() > "minecraft/".length()) {
if (level > 0) {
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (isDirectory ? "/" : ""), isDirectory);

String fileName = FileUtils.getName(file);
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1038,8 +1038,10 @@ modpack.file_api=Modpack URL Prefix
modpack.files.blueprints=BuildCraft Blueprints
modpack.files.config=Mod Configuration Files
modpack.files.dumps=NEI Debug Output Files
modpack.files.empty=No files to export
modpack.files.hmclversion_cfg=Launcher Configuration File
modpack.files.liteconfig=LiteLoader Related Files
modpack.files.load_failed=Failed to load modpack file tree. Please click here to retry.
modpack.files.mods=Mods
modpack.files.mods.voxelmods=VoxelMods Options
modpack.files.options_txt=Minecraft Option File
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -847,8 +847,10 @@ modpack.file_api=模組包下載連結前綴
modpack.files.blueprints=BuildCraft 藍圖
modpack.files.config=模組設定檔案
modpack.files.dumps=NEI 除錯輸出檔案
modpack.files.empty=沒有需要匯出的檔案
modpack.files.hmclversion_cfg=啟動器設定檔案
modpack.files.liteconfig=LiteLoader 相關檔案
modpack.files.load_failed=載入整合包檔案樹失敗,點選此處重試
modpack.files.mods=模組
modpack.files.mods.voxelmods=VoxelMods 設定,如小地圖
modpack.files.options_txt=遊戲設定
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -853,8 +853,10 @@ modpack.file_api=整合包下载链接前缀
modpack.files.blueprints=BuildCraft 蓝图
modpack.files.config=模组配置文件
modpack.files.dumps=NEI 调试输出文件
modpack.files.empty=没有需要导出的文件
modpack.files.hmclversion_cfg=启动器配置文件
modpack.files.liteconfig=LiteLoader 相关文件
modpack.files.load_failed=加载整合包文件树失败,点击此处重试
modpack.files.mods=模组
modpack.files.mods.voxelmods=VoxelMods 配置,如小地图
modpack.files.options_txt=游戏设置
Expand Down