Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
73b01ce
下载模组时展示更新日志
Calboot Nov 19, 2025
cffd3e0
添加模组更新界面的更新日志
Calboot Nov 19, 2025
5a39c98
代码格式
Calboot Nov 20, 2025
3d514fe
Merge branch 'HMCL-dev:main' into mod-changelog
Calboot Nov 20, 2025
5b3a705
中文支持&界面优化
Calboot Nov 20, 2025
404548d
多语言支持
Calboot Nov 21, 2025
d351523
update
Calboot Nov 23, 2025
8fed3b7
Merge branch 'HMCL-dev:main' into mod-changelog
Calboot Nov 23, 2025
36c6661
Merge remote-tracking branch 'upstream/main' into mod-changelog
Calboot Nov 30, 2025
254fa86
Merge remote-tracking branch 'upstream/main' into mod-changelog
Calboot Nov 30, 2025
cb3fc84
update
Calboot Nov 30, 2025
998c6d1
update
Calboot Nov 30, 2025
c9c7ea5
update
Calboot Dec 1, 2025
f501b79
Merge remote-tracking branch 'upstream/main' into mod-changelog
Calboot Dec 12, 2025
71b0aae
Merge remote-tracking branch 'upstream/main' into mod-changelog
Calboot Dec 12, 2025
0973c76
update
Calboot Dec 12, 2025
50a2aeb
HTML support & changelog cache
Calboot Dec 13, 2025
e3f05fb
Markdown
Calboot Dec 13, 2025
b35897b
update
Calboot Dec 19, 2025
1915cfd
Add deps
Calboot Dec 20, 2025
212d8be
update
Calboot Dec 20, 2025
e9cca3e
Merge branch 'HMCL-dev:main' into mod-changelog
Calboot Dec 20, 2025
853ef1f
Merge branch 'HMCL-dev:main' into mod-changelog
Calboot Dec 24, 2025
ce4d12a
Merge branch 'HMCL-dev:main' into mod-changelog
Calboot Jan 1, 2026
1005fcd
Merge branch 'main' into mod-changelog
Calboot Jan 2, 2026
9fd3efc
update
Calboot Jan 2, 2026
5f81a39
update
Calboot Jan 2, 2026
bdf6ef4
update
Calboot Jan 3, 2026
8ad29d1
update
Calboot Jan 3, 2026
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 @@ -128,4 +128,9 @@ public RemoteMod.File getModFile(String modId, String fileId) throws IOException
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(id);
}

@Override
public String getModChangelog(String modId, String fileId) throws IOException {
return getBackedRemoteModRepository().getModChangelog(modId, fileId);
}
}
8 changes: 8 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.Nullable;
import org.jsoup.Jsoup;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
Expand Down Expand Up @@ -1570,4 +1571,11 @@ public static JFXPopup.PopupVPosition determineOptimalPopupPosition(Node root, J
? JFXPopup.PopupVPosition.BOTTOM // Show menu below the button, expanding downward
: JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward
}

public static TextFlow renderModChangelog(String changelogHTML) {
HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser();
renderer.appendNode(Jsoup.parse(changelogHTML));
renderer.mergeLineBreaks();
return renderer.render();
}
}
10 changes: 10 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
import java.util.List;
import java.util.function.Consumer;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;

/**
* @author Glavo
*/
public final class HTMLRenderer {

private static URI resolveLink(Node linkNode) {
String href = linkNode.absUrl("href");
if (href.isEmpty())
Expand All @@ -49,6 +51,14 @@ private static URI resolveLink(Node linkNode) {
}
}

public static HTMLRenderer openHyperlinkInBrowser() {
return new HTMLRenderer(uri -> {
Controllers.confirm(i18n("web.open_in_browser", uri), i18n("message.confirm"), () -> {
FXUtils.openLink(uri.toString());
}, null);
});
}

private final List<javafx.scene.Node> children = new ArrayList<>();
private final List<Node> stack = new ArrayList<>();

Expand Down
6 changes: 1 addition & 5 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/WebPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,7 @@ public WebPage(String title, String content) {

Task.supplyAsync(() -> {
Document document = Jsoup.parseBodyFragment(content);
HTMLRenderer renderer = new HTMLRenderer(uri -> {
Controllers.confirm(i18n("web.open_in_browser", uri), i18n("message.confirm"), () -> {
FXUtils.openLink(uri.toString());
}, null);
});
HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser();
renderer.appendNode(document);
renderer.mergeLineBreaks();
return renderer.render();
Expand Down
99 changes: 84 additions & 15 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;

public class DownloadPage extends Control implements DecoratorPage {
private static final WeakHashMap<RemoteMod.Version, String> changelogCache = new WeakHashMap<>();

private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final BooleanProperty loaded = new SimpleBooleanProperty(false);
private final BooleanProperty loading = new SimpleBooleanProperty(false);
Expand Down Expand Up @@ -299,7 +301,7 @@ protected ModDownloadPageSkin(DownloadPage control) {

for (String gameVersion : control.versions.keys().stream()
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
.collect(Collectors.toList())) {
.toList()) {
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
if (versions == null || versions.isEmpty()) {
continue;
Expand Down Expand Up @@ -373,6 +375,10 @@ private static final class DependencyModItem extends StackPane {

private static final class ModItem extends StackPane {

ModItem(RemoteMod.Version dataItem) {
this(dataItem, null);
}

ModItem(RemoteMod.Version dataItem, DownloadPage selfPage) {
VBox pane = new VBox(8);
pane.setPadding(new Insets(8, 0, 8, 0));
Expand Down Expand Up @@ -435,7 +441,9 @@ private static final class ModItem extends StackPane {
}

RipplerContainer container = new RipplerContainer(pane);
FXUtils.onClicked(container, () -> Controllers.dialog(new ModVersion(dataItem, selfPage)));
if (selfPage != null) {
FXUtils.onClicked(container, () -> Controllers.dialog(new ModVersion(dataItem, selfPage)));
}
getChildren().setAll(container);

// Workaround for https://github.com/HMCL-dev/HMCL/issues/2129
Expand All @@ -444,6 +452,8 @@ private static final class ModItem extends StackPane {
}

private static final class ModVersion extends JFXDialogLayout {
private final String title;

public ModVersion(RemoteMod.Version version, DownloadPage selfPage) {
RemoteModRepository.Type type = selfPage.repository.getType();

Expand All @@ -454,24 +464,26 @@ public ModVersion(RemoteMod.Version version, DownloadPage selfPage) {
case SHADER_PACK -> "shaderpack.download.title";
default -> "mods.download.title";
};
this.setHeading(new HBox(new Label(i18n(title, version.getName()))));
this.title = i18n(title, version.getName());
this.setHeading(new HBox(new Label(this.title)));

VBox box = new VBox(8);
box.setPadding(new Insets(8));
ModItem modItem = new ModItem(version, selfPage);
modItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again
box.getChildren().setAll(modItem);
box.getChildren().setAll(new ModItem(version));

Button changelogButton = new JFXButton(i18n("mods.show_detail"));
changelogButton.getStyleClass().add("dialog-accept");
SpinnerPane spinnerPane = new SpinnerPane();
ScrollPane scrollPane = new ScrollPane();
ComponentList dependenciesList = new ComponentList(Lang::immutableListOf);
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
loadChangelogAndDependencies(version, selfPage, spinnerPane, dependenciesList, changelogButton);
spinnerPane.setOnFailedAction(e -> loadChangelogAndDependencies(version, selfPage, spinnerPane, dependenciesList, changelogButton));

scrollPane.setContent(dependenciesList);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
spinnerPane.setContent(scrollPane);
box.getChildren().add(spinnerPane);
box.getChildren().addAll(spinnerPane);
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);

this.setBody(box);
Expand Down Expand Up @@ -502,9 +514,9 @@ public ModVersion(RemoteMod.Version version, DownloadPage selfPage) {
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));

if (downloadButton == null) {
this.setActions(saveAsButton, cancelButton);
this.setActions(changelogButton, saveAsButton, cancelButton);
} else {
this.setActions(downloadButton, saveAsButton, cancelButton);
this.setActions(changelogButton, downloadButton, saveAsButton, cancelButton);
}

this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7));
Expand All @@ -513,9 +525,23 @@ public ModVersion(RemoteMod.Version version, DownloadPage selfPage) {
onEscPressed(this, cancelButton::fire);
}

private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList) {
private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList, Button changelogButton) {
spinnerPane.setLoading(true);
changelogButton.setDisable(true);
Task.supplyAsync(() -> {
Optional<String> changelog;
if (changelogCache.containsKey(version)) {
changelog = Optional.ofNullable(changelogCache.get(version));
} else if (version.getChangelog() != null) {
changelog = StringUtils.nullIfBlank(version.getChangelog());
} else {
try {
changelog = StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId()));
} catch (UnsupportedOperationException e) {
changelog = Optional.empty();
}
}

EnumMap<RemoteMod.DependencyType, List<Node>> dependencies = new EnumMap<>(RemoteMod.DependencyType.class);
for (RemoteMod.Dependency dependency : version.getDependencies()) {
if (dependency.getType() == RemoteMod.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteMod.DependencyType.BROKEN) {
Expand All @@ -533,20 +559,63 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage,
dependencies.get(dependency.getType()).add(dependencyModItem);
}

return dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
return new Pair<>(changelog, dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList()));
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
spinnerPane.setLoading(false);
if (exception == null) {
dependenciesList.getContent().setAll(result);
if (result.getKey().isPresent()) {
String s = StringUtils.markdownToHTML(result.getKey().get());
changelogCache.put(version, s);
changelogButton.setDisable(false);
changelogButton.setOnAction(e -> Controllers.dialog(new ModChangelog(ModVersion.this.title, s)));
} else {
changelogCache.put(version, null);
changelogButton.setOnAction(null);
}
dependenciesList.getContent().setAll(result.getValue());
spinnerPane.setFailedReason(null);
} else {
changelogButton.setOnAction(null);
dependenciesList.getContent().setAll();
spinnerPane.setFailedReason(i18n("download.failed.refresh"));
}
spinnerPane.setLoading(false);
}).start();
}
}

private static final class ModChangelog extends JFXDialogLayout {

public ModChangelog(String title, String changelog) {
setHeading(new HBox(new Label(title)));

VBox box = new VBox(8);
box.setPadding(new Insets(8));

SpinnerPane spinnerPane = new SpinnerPane();
ScrollPane scrollPane = new ScrollPane();
scrollPane.getStyleClass().add("mod-changelog");
scrollPane.setFitToWidth(true);
scrollPane.setContent(FXUtils.renderModChangelog(changelog));

spinnerPane.setContent(scrollPane);
box.getChildren().add(spinnerPane);
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);

this.setBody(box);

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

setActions(closeButton);

this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7));
this.prefHeightProperty().bind(BindingMapping.of(Controllers.getStage().heightProperty()).map(w -> w.doubleValue() * 0.7));

onEscPressed(this, closeButton::fire);
}
}

public interface DownloadCallback {
void download(Profile profile, @Nullable String version, RemoteMod.Version file);
}
Expand Down
Loading