Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
311cf64
feat: 初步添加数据包下载功能
Mine-diamond Dec 17, 2025
4875a2d
feat: 为curseforge datapack下载添加分类
Mine-diamond Dec 17, 2025
eb7dfb1
fix: 修复SVG错误,修改datapack本地化
Mine-diamond Dec 17, 2025
d33e796
feat: 修改download.hint
Mine-diamond Dec 17, 2025
78ee487
feat: 添加国际化
Mine-diamond Dec 17, 2025
db3691c
feat:开始做datapack详细信息界面
Mine-diamond Dec 18, 2025
305c8c0
feat:datapack详细信息界面
Mine-diamond Dec 18, 2025
04f2379
feat:更改右键触发逻辑
Mine-diamond Dec 18, 2025
ef8cd12
feat:将DatapackInfoDialog分离为单独的类
Mine-diamond Dec 18, 2025
e784ac3
feat:在数据包界面添加下载按钮
Mine-diamond Dec 18, 2025
dd112cb
fix style
Mine-diamond Dec 18, 2025
a390e52
feat: 限制数据包列表描述只显示一行
Mine-diamond Dec 18, 2025
5877ce4
feat: 优化数据包详细信息页面的显示
Mine-diamond Dec 18, 2025
0444c4b
feat: update
Mine-diamond Dec 18, 2025
6a60bc1
feat: 过滤modrinth返回数据包版本信息的非数据包版本
Mine-diamond Dec 19, 2025
1e95f5e
feat: 移除datapack标签
Mine-diamond Dec 19, 2025
0382032
feat: 添加数据包直接下载到具体世界功能
Mine-diamond Dec 19, 2025
db1089d
fix style
Mine-diamond Dec 19, 2025
45bce08
chore: 修改名称 datapack -> dataPack
Mine-diamond Dec 19, 2025
35c725f
chore: 修改ModLoaderType PACK -> DATA_PACK
Mine-diamond Dec 19, 2025
62f9d5b
Merge branch 'main' into datapack_download
Mine-diamond Dec 21, 2025
ce4c9c0
feat: 修改下载数据包向导
Mine-diamond Dec 21, 2025
ec2d942
fix: 修复数据包下载弹窗显示模组下载
Mine-diamond Dec 21, 2025
c660142
fix: 修复数据包下载弹窗显示模组下载
Mine-diamond Dec 21, 2025
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
2 changes: 2 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public enum SVG {
CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"),
CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"),
CODE_BLOCKS("M9.6 15.6 11 14.175 8.825 12 11 9.825 9.6 8.4 6 12l3.6 3.6Zm4.8 0L18 12l-3.6-3.6-1.4 1.425L15.175 12 13.0 14.175l1.4 1.425ZM5 21q-0.825 0-1.413-0.587Q3 19.825 3 19V5q0-0.825 0.587-1.413Q4.175 3 5 3h14q0.825 0 1.413 0.587Q21 4.175 21 5v14q0 0.825-0.587 1.413Q19.825 21 19 21H5Zm0-2h14V5H5v14Z"),
CODE_BLOCKS_FILL("M9.6 15.6 11 14.175 8.825 12 11 9.825 9.6 8.4 6 12l3.6 3.6Zm4.8 0L18 12 14.4 8.4 13 9.825 15.175 12 13 14.175 14.4 15.6ZM5 21q-0.825 0-1.413-0.587T3 19V5q0-0.825 0.587-1.413T5 3h14q0.825 0 1.413 0.587T21 5v14q0 0.825-0.587 1.413T19 21H5Z"),
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
Expand All @@ -39,6 +42,7 @@
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
Expand Down Expand Up @@ -70,6 +74,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
private final TabHeader.Tab<DownloadListPage> modpackTab = new TabHeader.Tab<>("modpackTab");
private final TabHeader.Tab<DownloadListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TabHeader.Tab<DownloadListPage> shaderTab = new TabHeader.Tab<>("shaderTab");
private final TabHeader.Tab<DownloadListPage> dataPackTab = new TabHeader.Tab<>("dataPackTab");
private final TabHeader.Tab<DownloadListPage> worldTab = new TabHeader.Tab<>("worldTab");
private final TransitionPane transitionPane = new TransitionPane();
private final DownloadNavigator versionPageNavigator = new DownloadNavigator();
Expand Down Expand Up @@ -97,8 +102,9 @@ public DownloadPage(String uploadVersion) {
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, file) -> download(profile, version, file, "mods"), true)));
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
shaderTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(ModrinthRemoteModRepository.SHADER_PACKS, (profile, version, file) -> download(profile, version, file, "shaderpacks"), true)));
dataPackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofDataPack((profile, version, file) -> downloadForDataPack(profile, version, file, "datapacks"), true)));
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, dataPackTab, worldTab);

Profiles.registerVersionsListener(this::loadVersions);

Expand All @@ -112,6 +118,7 @@ public DownloadPage(String uploadVersion) {
.addNavigationDrawerTab(tab, modTab, i18n("mods"), SVG.EXTENSION, SVG.EXTENSION_FILL)
.addNavigationDrawerTab(tab, resourcePackTab, i18n("resourcepack"), SVG.TEXTURE)
.addNavigationDrawerTab(tab, shaderTab, i18n("download.shader"), SVG.WB_SUNNY, SVG.WB_SUNNY_FILL)
.addNavigationDrawerTab(tab, dataPackTab, i18n("datapack"), SVG.CODE_BLOCKS, SVG.CODE_BLOCKS_FILL)
.addNavigationDrawerTab(tab, worldTab, i18n("world"), SVG.PUBLIC);
FXUtils.setLimitWidth(sideBar, 200);
setLeft(sideBar);
Expand Down Expand Up @@ -162,6 +169,53 @@ public static void download(Profile profile, @Nullable String version, RemoteMod

}

public static void downloadForDataPack(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
if (version == null) version = profile.getSelectedVersion();

Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();

ObservableList<World> worlds = FXCollections.observableArrayList(World.getWorlds(runDirectory.resolve("saves")).toList());

Controllers.prompt(
new PromptDialogPane.Builder("安装选项", (result, resolve, reject) -> {
String fileName = ((PromptDialogPane.Builder.StringQuestion) result.get(0)).getValue();
Integer selectWorld = ((PromptDialogPane.Builder.CandidatesQuestion) result.get(1)).getValue();

if (selectWorld == null) {
reject.accept(i18n("mods.install.select_world.not_select.hint"));
return;
}

if (!FileUtils.isNameValid(fileName)) {
reject.accept(i18n("install.new_game.malformed"));
return;
}

String selectedWorldFolderName = worlds.get(selectWorld).getFileName();
Path dest = runDirectory.resolve("saves").resolve(selectedWorldFolderName).resolve(subdirectoryName).resolve(fileName);

Controllers.taskDialog(Task.composeAsync(() -> {
var task = new FileDownloadTask(file.getFile().getUrl(), dest);
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
}
} else {
Controllers.showToast(i18n("install.success"));
}
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
resolve.run();
})
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("archive.file.name"), file.getFile().getFilename()))
.addQuestion(new PromptDialogPane.Builder.CandidatesQuestion(i18n("mods.install.select_world.title"), worlds.stream().map(World::getWorldName).toArray(String[]::new)))
);
}

private void loadVersions(Profile profile) {
listenerHolder = new WeakListenerHolder();
runInFX(() -> {
Expand All @@ -179,6 +233,9 @@ private void loadVersions(Profile profile) {
if (shaderTab.isInitialized()) {
shaderTab.getNode().loadVersion(profile, null);
}
if (dataPackTab.isInitialized()) {
dataPackTab.getNode().loadVersion(profile, null);
}
if (worldTab.isInitialized()) {
worldTab.getNode().loadVersion(profile, null);
}
Expand Down Expand Up @@ -213,6 +270,10 @@ public void showWorldDownloads() {
tab.select(worldTab, false);
}

public void showDatapackDownloads() {
tab.select(dataPackTab, false);
}

private static final class DownloadNavigator implements Navigation {
private final SettingsMap settings = new SettingsMap();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <[email protected]> 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.versions;

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.io.NetworkUtils;

import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;

final class DataPackInfoDialog extends JFXDialogLayout {
public DataPackInfoDialog(DataPackListPageSkin.DataPackInfoObject dataPackInfoObject, Profile profile, String versionID) {

Stage stage = Controllers.getStage();
{
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
}

//heading area
HBox titleContainer = new HBox();
{
titleContainer.setSpacing(8);
setHeading(titleContainer);
}
TwoLineListItem titleItem;
{
titleItem = new TwoLineListItem();
{
titleItem.setTitle(dataPackInfoObject.getTitle());
}

ImageView imageView = new ImageView();
{
FXUtils.limitSize(imageView, 40, 40);
dataPackInfoObject.loadIcon(imageView, null);
}

titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), titleItem);
}

//body area
Label description = new Label(dataPackInfoObject.getSubtitle());
{
description.setWrapText(true);
FXUtils.copyOnDoubleClick(description);
}
ScrollPane descriptionPane = new ScrollPane(description);
{
FXUtils.smoothScrolling(descriptionPane);
descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
descriptionPane.setFitToWidth(true);
description.heightProperty().addListener((obs, oldVal, newVal) -> {
double maxHeight = stage.getHeight() * 0.5;
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
descriptionPane.setPrefViewportHeight(targetHeight);
});

setBody(descriptionPane);
}

//action area
JFXHyperlink openInMcModButton = new JFXHyperlink(i18n("mods.mcmod.search"));
{
openInMcModButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
FXUtils.openLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf(
pair("key", dataPackInfoObject.getTitle()),
pair("site", "all"),
pair("filter", "0")
)));
});
}

AtomicBoolean initWhatNeedRemoteModInfo = new AtomicBoolean(false);
for (Pair<String, ? extends RemoteModRepository> item : Arrays.asList(
pair("mods.curseforge", CurseForgeRemoteModRepository.MODS),
pair("mods.modrinth", ModrinthRemoteModRepository.MODS)
)) {
RemoteModRepository repository = item.getValue();
JFXHyperlink button = new JFXHyperlink(i18n(item.getKey()));
Task.runAsync(() -> {
Optional<RemoteMod.Version> versionOptional = repository.getRemoteVersionByLocalFile(null, dataPackInfoObject.getPackInfo().getPath());
versionOptional.ifPresent(version -> {
RemoteMod remoteMod;
try {
remoteMod = repository.getModById(version.getModid());
} catch (IOException e) {
LOG.warning("Cannot get remote mod of " + version.getModid(), e);
return;
}

FXUtils.runInFX(() -> {
button.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
Controllers.navigate(new DownloadPage(
HMCLLocalizedDownloadListPage.ofDataPack(null, false),
remoteMod,
new Profile.ProfileVersion(profile, versionID),
null
));
});
button.setDisable(false);

if (!initWhatNeedRemoteModInfo.getAndSet(true)) {
ModTranslations.Mod modToOpenInMcMod = ModTranslations.getTranslationsByRepositoryType(repository.getType()).getModByCurseForgeId(remoteMod.getSlug());
if (modToOpenInMcMod != null) {
openInMcModButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modToOpenInMcMod));
});
openInMcModButton.setText(i18n("mods.mcmod.page"));
} else {
openInMcModButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
FXUtils.openLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf(
pair("key", remoteMod.getTitle()),
pair("site", "all"),
pair("filter", "0")
)));
});
}

StringJoiner joiner = new StringJoiner(" | ");
joiner.add(remoteMod.getTitle());
joiner.add(version.getVersion());
titleItem.setSubtitle(joiner.toString());
}
});
});
}).start();
button.setDisable(true);
getActions().add(button);
}

JFXButton okButton = new JFXButton();
{
okButton.getStyleClass().add("dialog-accept");
okButton.setText(i18n("button.ok"));
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
}

getActions().addAll(openInMcModButton, okButton);

onEscPressed(this, okButton::fire);
}
}
Loading