From 73b01cee815fc4784e3d56ff1f51210be08702a3 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 19 Nov 2025 20:00:03 +0800 Subject: [PATCH 01/77] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E6=A8=A1=E7=BB=84?= =?UTF-8?q?=E6=97=B6=E5=B1=95=E7=A4=BA=E6=9B=B4=E6=96=B0=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/LocalizedRemoteModRepository.java | 5 +++ .../hmcl/ui/versions/DownloadPage.java | 42 +++++++++++++++---- .../org/jackhuang/hmcl/mod/RemoteMod.java | 8 +++- .../hmcl/mod/RemoteModRepository.java | 2 + .../jackhuang/hmcl/mod/curse/CurseAddon.java | 1 + .../curse/CurseForgeRemoteModRepository.java | 7 ++++ .../modrinth/ModrinthRemoteModRepository.java | 6 +++ 7 files changed, 61 insertions(+), 10 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java index 8a15898173..fd051d817a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -128,4 +128,9 @@ public RemoteMod.File getModFile(String modId, String fileId) throws IOException public Stream getRemoteVersionsById(String id) throws IOException { return getBackedRemoteModRepository().getRemoteVersionsById(id); } + + @Override + public String getModChangelog(String modId, String fileId) throws IOException { + return getBackedRemoteModRepository().getModChangelog(modId, fileId); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index c62da41ee1..d757f3324d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -30,6 +30,7 @@ import javafx.scene.control.*; import javafx.scene.image.ImageView; import javafx.scene.layout.*; +import javafx.scene.text.Text; import javafx.stage.FileChooser; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.HMCLGameRepository; @@ -53,6 +54,9 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Safelist; import java.nio.file.Path; import java.util.*; @@ -471,13 +475,14 @@ public ModVersion(RemoteMod.Version version, DownloadPage selfPage) { 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); + 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)); + ComponentList changelogAndDependenciesList = new ComponentList(Lang::immutableListOf); + loadChangelogAndDependencies(version, selfPage, spinnerPane, changelogAndDependenciesList); + spinnerPane.setOnFailedAction(e -> loadChangelogAndDependencies(version, selfPage, spinnerPane, changelogAndDependenciesList)); - scrollPane.setContent(dependenciesList); + scrollPane.setContent(changelogAndDependenciesList); scrollPane.setFitToWidth(true); scrollPane.setFitToHeight(true); spinnerPane.setContent(scrollPane); @@ -523,9 +528,24 @@ 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) { spinnerPane.setLoading(true); Task.supplyAsync(() -> { + String changelogText; + if (version.getChangelog() != null) { + changelogText = version.getChangelog(); + } else { + String changelog = selfPage.repository.getModChangelog(version.getModid(), version.getVersionId()); + Document document = Jsoup.parse(changelog); + Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); + document.outputSettings(outputSettings); + document.select("br").append("\\n"); + document.select("p").prepend("\\n"); + document.select("p").append("\\n"); + String newHtml = document.html().replaceAll("\\\\n", System.lineSeparator()); + changelogText = Jsoup.clean(newHtml, "", Safelist.none(), outputSettings).trim(); + } + EnumMap> dependencies = new EnumMap<>(RemoteMod.DependencyType.class); for (RemoteMod.Dependency dependency : version.getDependencies()) { if (dependency.getType() == RemoteMod.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteMod.DependencyType.BROKEN) { @@ -533,7 +553,7 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, } if (!dependencies.containsKey(dependency.getType())) { - List list = new ArrayList<>(); + List list = new LinkedList<>(); Label title = new Label(i18n(DependencyModItem.I18N_KEY.get(dependency.getType()))); title.setPadding(new Insets(0, 8, 0, 8)); list.add(title); @@ -543,16 +563,20 @@ 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<>(changelogText, dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { - spinnerPane.setLoading(false); if (exception == null) { - dependenciesList.getContent().setAll(result); + List nodes = new LinkedList<>(); + Text changelogText = new Text(result.getKey()); + nodes.add(changelogText); + nodes.addAll(result.getValue()); + dependenciesList.getContent().setAll(nodes); spinnerPane.setFailedReason(null); } else { dependenciesList.getContent().setAll(); spinnerPane.setFailedReason(i18n("download.failed.refresh")); } + spinnerPane.setLoading(false); }).start(); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index a936f887be..d06e564151 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -213,6 +213,7 @@ public interface IVersion { public static class Version { private final IVersion self; + private final String versionId; private final String modid; private final String name; private final String version; @@ -224,7 +225,8 @@ public static class Version { private final List gameVersions; private final List loaders; - public Version(IVersion self, String modid, String name, String version, String changelog, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { + public Version(IVersion self, String versionId, String modid, String name, String version, String changelog, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { + this.versionId = versionId; this.self = self; this.modid = modid; this.name = name; @@ -242,6 +244,10 @@ public IVersion getSelf() { return self; } + public String getVersionId() { + return versionId; + } + public String getModid() { return modid; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index 5f74ba7d49..c0c54d94e2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -95,6 +95,8 @@ SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Null Stream getRemoteVersionsById(String id) throws IOException; + String getModChangelog(String modId, String fileId) throws IOException; + Stream getCategories() throws IOException; class Category { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index e77fb259e1..c2adaeda33 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -571,6 +571,7 @@ public RemoteMod.Version toVersion() { return new RemoteMod.Version( this, + Integer.toString(getId()), Integer.toString(modId), getDisplayName(), getFileName(), diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index f884334c8e..9ea7704a63 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -203,6 +203,13 @@ public Stream getRemoteVersionsById(String id) throws IOExcep return response.getData().stream().map(CurseAddon.LatestFile::toVersion); } + @Override + public String getModChangelog(String modId, String fileId) throws IOException { + Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s/changelog", PREFIX, modId, fileId))) + .getJson(Response.typeOf(String.class)); + return response.getData(); + } + public List getCategoriesImpl() throws IOException { Response> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432"))) .getJson(Response.typeOf(listTypeOf(CurseAddon.Category.class))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 4b60bf3cdd..70a46bdceb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -141,6 +141,11 @@ public Stream getRemoteVersionsById(String id) throws IOExcep return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream); } + @Override + public String getModChangelog(String modId, String fileId) throws IOException { + throw new UnsupportedOperationException(); + } + public List getCategoriesImpl() throws IOException { List categories = HttpRequest.GET(PREFIX + "/v2/tag/category").getJson(listTypeOf(Category.class)); return categories.stream().filter(category -> category.getProjectType().equals(projectType)).collect(Collectors.toList()); @@ -496,6 +501,7 @@ public Optional toVersion() { return Optional.of(new RemoteMod.Version( this, + getId(), projectId, name, versionNumber, From cffd3e0fcdf98032e11a904045eb9c0eedc9e81d Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 19 Nov 2025 22:04:08 +0800 Subject: [PATCH 02/77] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=A8=A1=E7=BB=84?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=95=8C=E9=9D=A2=E7=9A=84=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/DownloadPage.java | 33 ++- .../hmcl/ui/versions/ModUpdatesPage.java | 213 ++++++++++++++++-- .../resources/assets/lang/I18N.properties | 1 + .../org/jackhuang/hmcl/mod/LocalModFile.java | 10 +- 4 files changed, 229 insertions(+), 28 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index d757f3324d..c76c309826 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -31,6 +31,7 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; import javafx.stage.FileChooser; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.HMCLGameRepository; @@ -58,6 +59,7 @@ import org.jsoup.nodes.Document; import org.jsoup.safety.Safelist; +import java.io.IOException; import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; @@ -533,17 +535,22 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag Task.supplyAsync(() -> { String changelogText; if (version.getChangelog() != null) { - changelogText = version.getChangelog(); + changelogText = version.getChangelog().isBlank() ? null : version.getChangelog(); } else { - String changelog = selfPage.repository.getModChangelog(version.getModid(), version.getVersionId()); - Document document = Jsoup.parse(changelog); - Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); - document.outputSettings(outputSettings); - document.select("br").append("\\n"); - document.select("p").prepend("\\n"); - document.select("p").append("\\n"); - String newHtml = document.html().replaceAll("\\\\n", System.lineSeparator()); - changelogText = Jsoup.clean(newHtml, "", Safelist.none(), outputSettings).trim(); + try { + String changelog = selfPage.repository.getModChangelog(version.getModid(), version.getVersionId()); + Document document = Jsoup.parse(changelog); + Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); + document.outputSettings(outputSettings); + document.select("br").append("\\n"); + document.select("p").prepend("\\n"); + document.select("p").append("\\n"); + String newHtml = document.html().replaceAll("\\\\n", System.lineSeparator()); + String plainText = Jsoup.clean(newHtml, "", Safelist.none(), outputSettings).trim(); + changelogText = plainText.isBlank() ? null : plainText; + } catch (UnsupportedOperationException e) { + changelogText = null; + } } EnumMap> dependencies = new EnumMap<>(RemoteMod.DependencyType.class); @@ -567,8 +574,10 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { List nodes = new LinkedList<>(); - Text changelogText = new Text(result.getKey()); - nodes.add(changelogText); + String changelog = result.getKey(); + if (changelog != null) { + nodes.add(new Text(changelog)); + } nodes.addAll(result.getValue()); dependenciesList.getContent().setAll(nodes); spinnerPane.setFailedReason(null); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 8b02ca9f85..544d6dac4a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -18,40 +18,42 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDialogLayout; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.CheckBox; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; +import javafx.scene.control.*; import javafx.scene.control.cell.CheckBoxTableCell; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.ModManager; -import org.jackhuang.hmcl.mod.RemoteMod; +import javafx.scene.layout.*; +import javafx.scene.text.Text; +import org.jackhuang.hmcl.mod.*; +import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.task.FileDownloadTask; 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.construct.MessageDialogPane; -import org.jackhuang.hmcl.ui.construct.PageCloseEvent; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.CSVTable; +import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Safelist; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -95,12 +97,27 @@ public ModUpdatesPage(ModManager modManager, List update TableColumn sourceColumn = new TableColumn<>(i18n("mods.check_updates.source")); setupCellValueFactory(sourceColumn, ModUpdateObject::sourceProperty); + TableColumn detailColumn = new TableColumn<>(); + detailColumn.setCellFactory(param -> { + TableCell cell = (TableCell) TableColumn.DEFAULT_CELL_FACTORY.call(param); + cell.setOnMouseClicked(event -> { + List items = cell.getTableColumn().getTableView().getItems(); + if (cell.getIndex() >= items.size()) { + return; + } + ModUpdateObject object = items.get(cell.getIndex()); + Controllers.dialog(new ModDetail(object.data.getCandidates().get(0), object.data.getRepository())); + }); + return cell; + }); + detailColumn.setCellValueFactory(it -> new SimpleStringProperty(i18n("mods.check_updates.show_detail"))); + objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList())); FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new)); TableView table = new TableView<>(objects); table.setEditable(true); - table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn); + table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn, detailColumn); setCenter(table); @@ -271,6 +288,174 @@ public void setSource(String source) { } } + private static final class ModItem extends StackPane { + + ModItem(RemoteMod.Version dataItem) { + VBox pane = new VBox(8); + pane.setPadding(new Insets(8, 0, 8, 0)); + + { + HBox descPane = new HBox(8); + descPane.setPadding(new Insets(0, 8, 0, 8)); + descPane.setAlignment(Pos.CENTER_LEFT); + descPane.setMouseTransparent(true); + + { + StackPane graphicPane = new StackPane(); + TwoLineListItem content = new TwoLineListItem(); + HBox.setHgrow(content, Priority.ALWAYS); + content.setTitle(dataItem.getName()); + content.setSubtitle(I18n.formatDateTime(dataItem.getDatePublished())); + + switch (dataItem.getVersionType()) { + case Alpha: + content.addTag(i18n("mods.channel.alpha")); + graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE.createIcon(Theme.blackFill(), 24)); + break; + case Beta: + content.addTag(i18n("mods.channel.beta")); + graphicPane.getChildren().setAll(SVG.BETA_CIRCLE.createIcon(Theme.blackFill(), 24)); + break; + case Release: + content.addTag(i18n("mods.channel.release")); + graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE.createIcon(Theme.blackFill(), 24)); + break; + } + + for (ModLoaderType modLoaderType : dataItem.getLoaders()) { + switch (modLoaderType) { + case FORGE: + content.addTag(i18n("install.installer.forge")); + break; + case CLEANROOM: + content.addTag(i18n("install.installer.cleanroom")); + break; + case NEO_FORGED: + content.addTag(i18n("install.installer.neoforge")); + break; + case FABRIC: + content.addTag(i18n("install.installer.fabric")); + break; + case LITE_LOADER: + content.addTag(i18n("install.installer.liteloader")); + break; + case QUILT: + content.addTag(i18n("install.installer.quilt")); + break; + } + } + + descPane.getChildren().setAll(graphicPane, content); + } + + pane.getChildren().add(descPane); + } + + RipplerContainer container = new RipplerContainer(pane); + getChildren().setAll(container); + + // Workaround for https://github.com/HMCL-dev/HMCL/issues/2129 + this.setMinHeight(50); + } + } + + private static final class ModDetail extends JFXDialogLayout { + + private final RemoteModRepository repository; + + public ModDetail(RemoteMod.Version version, RemoteModRepository repository) { + this.repository = repository; + RemoteModRepository.Type type = repository.getType(); + + String title; + switch (type) { + case WORLD: + title = "world.download.title"; + break; + case MODPACK: + title = "modpack.download.title"; + break; + case RESOURCE_PACK: + title = "resourcepack.download.title"; + break; + case MOD: + default: + title = "mods.download.title"; + break; + } + this.setHeading(new HBox(new Label(i18n(title, version.getName())))); + + VBox box = new VBox(8); + box.setPadding(new Insets(8)); + ModItem modItem = new ModItem(version); + modItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again + box.getChildren().setAll(modItem); + + SpinnerPane spinnerPane = new SpinnerPane(); + ScrollPane scrollPane = new ScrollPane(); + ComponentList changelogComponent = new ComponentList(Lang::immutableListOf); + loadChangelog(version, spinnerPane, changelogComponent); + spinnerPane.setOnFailedAction(e -> loadChangelog(version, spinnerPane, changelogComponent)); + + scrollPane.setContent(changelogComponent); + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + 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); + } + + private void loadChangelog(RemoteMod.Version version, SpinnerPane spinnerPane, ComponentList componentList) { + spinnerPane.setLoading(true); + Task.supplyAsync(() -> { + if (version.getChangelog() != null) { + return version.getChangelog().isBlank() ? null : version.getChangelog(); + } else { + try { + String changelog = repository.getModChangelog(version.getModid(), version.getVersionId()); + Document document = Jsoup.parse(changelog); + Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); + document.outputSettings(outputSettings); + document.select("br").append("\\n"); + document.select("p").prepend("\\n"); + document.select("p").append("\\n"); + String newHtml = document.html().replaceAll("\\\\n", System.lineSeparator()); + String plainText = Jsoup.clean(newHtml, "", Safelist.none(), outputSettings).trim(); + return plainText.isBlank() ? null : plainText; + } catch (UnsupportedOperationException e) { + return null; + } + } + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + if (result != null) { + componentList.getContent().setAll(new Text(result)); + } else { + componentList.getContent().setAll(); + } + spinnerPane.setFailedReason(null); + } else { + componentList.getContent().setAll(); + spinnerPane.setFailedReason(i18n("download.failed.refresh")); + } + spinnerPane.setLoading(false); + }).start(); + } + } + public static class ModUpdateTask extends Task { private final Collection> dependents; private final List failedMods = new ArrayList<>(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 32f4abca59..a9dc9e83c4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1065,6 +1065,7 @@ mods.check_updates.empty=All mods are up-to-date mods.check_updates.failed_check=Failed to check for updates. mods.check_updates.failed_download=Failed to download some files. mods.check_updates.file=File +mods.check_updates.show_detail=Show Detail mods.check_updates.source=Source mods.check_updates.target_version=Target Version mods.check_updates.update=Update diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index 5cb1a4403f..8ff86eaac3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -184,7 +184,7 @@ public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository .sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed()) .collect(Collectors.toList()); if (remoteVersions.isEmpty()) return null; - return new ModUpdate(this, currentVersion.get(), remoteVersions); + return new ModUpdate(repository, this, currentVersion.get(), remoteVersions); } @Override @@ -203,16 +203,22 @@ public int hashCode() { } public static class ModUpdate { + private final RemoteModRepository repository; private final LocalModFile localModFile; private final RemoteMod.Version currentVersion; private final List candidates; - public ModUpdate(LocalModFile localModFile, RemoteMod.Version currentVersion, List candidates) { + public ModUpdate(RemoteModRepository repository, LocalModFile localModFile, RemoteMod.Version currentVersion, List candidates) { + this.repository = repository; this.localModFile = localModFile; this.currentVersion = currentVersion; this.candidates = candidates; } + public RemoteModRepository getRepository() { + return repository; + } + public LocalModFile getLocalMod() { return localModFile; } From 5a39c9888cf670babbd6b776479eeb41952d130c Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 20 Nov 2025 18:24:09 +0800 Subject: [PATCH 03/77] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index c76c309826..dfbc0eae01 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -31,7 +31,6 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.scene.text.Text; -import javafx.scene.text.TextAlignment; import javafx.stage.FileChooser; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.HMCLGameRepository; @@ -59,7 +58,6 @@ import org.jsoup.nodes.Document; import org.jsoup.safety.Safelist; -import java.io.IOException; import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; From 5b3a705c6ff5365e372a92ec3180038ed5a3b5b2 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 20 Nov 2025 22:53:41 +0800 Subject: [PATCH 04/77] =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=94=AF=E6=8C=81&?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/DownloadPage.java | 28 +++------ .../hmcl/ui/versions/ModUpdatesPage.java | 59 ++++++------------- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_lzh.properties | 2 + .../resources/assets/lang/I18N_zh.properties | 2 + .../assets/lang/I18N_zh_CN.properties | 2 + .../org/jackhuang/hmcl/util/StringUtils.java | 20 +++++++ 7 files changed, 51 insertions(+), 63 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index dfbc0eae01..04f8281f77 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -54,9 +54,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.safety.Safelist; import java.nio.file.Path; import java.util.*; @@ -531,23 +528,15 @@ public ModVersion(RemoteMod.Version version, DownloadPage selfPage) { private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList) { spinnerPane.setLoading(true); Task.supplyAsync(() -> { - String changelogText; + Optional changelog; if (version.getChangelog() != null) { - changelogText = version.getChangelog().isBlank() ? null : version.getChangelog(); + changelog = Optional.ofNullable(version.getChangelog().isBlank() ? null : version.getChangelog()); } else { try { - String changelog = selfPage.repository.getModChangelog(version.getModid(), version.getVersionId()); - Document document = Jsoup.parse(changelog); - Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); - document.outputSettings(outputSettings); - document.select("br").append("\\n"); - document.select("p").prepend("\\n"); - document.select("p").append("\\n"); - String newHtml = document.html().replaceAll("\\\\n", System.lineSeparator()); - String plainText = Jsoup.clean(newHtml, "", Safelist.none(), outputSettings).trim(); - changelogText = plainText.isBlank() ? null : plainText; + String changelogText = StringUtils.htmlToText(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())); + changelog = changelogText.isBlank() ? Optional.empty() : Optional.of(changelogText); } catch (UnsupportedOperationException e) { - changelogText = null; + changelog = Optional.empty(); } } @@ -568,14 +557,11 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag dependencies.get(dependency.getType()).add(dependencyModItem); } - return new Pair<>(changelogText, 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) -> { if (exception == null) { List nodes = new LinkedList<>(); - String changelog = result.getKey(); - if (changelog != null) { - nodes.add(new Text(changelog)); - } + result.getKey().ifPresent(s -> nodes.add(new HBox(new Text(s)))); nodes.addAll(result.getValue()); dependenciesList.getContent().setAll(nodes); spinnerPane.setFailedReason(null); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 544d6dac4a..c559b8be30 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -41,13 +41,11 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.CSVTable; import org.jackhuang.hmcl.util.javafx.BindingMapping; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.safety.Safelist; import java.nio.file.Path; import java.nio.file.Paths; @@ -106,7 +104,7 @@ public ModUpdatesPage(ModManager modManager, List update return; } ModUpdateObject object = items.get(cell.getIndex()); - Controllers.dialog(new ModDetail(object.data.getCandidates().get(0), object.data.getRepository())); + Controllers.dialog(new ModDetail(object.data.getCandidates().get(0), object.data.getRepository(), object.getSource())); }); return cell; }); @@ -290,7 +288,7 @@ public void setSource(String source) { private static final class ModItem extends StackPane { - ModItem(RemoteMod.Version dataItem) { + ModItem(RemoteMod.Version targetVersion, String source) { VBox pane = new VBox(8); pane.setPadding(new Insets(8, 0, 8, 0)); @@ -304,10 +302,10 @@ private static final class ModItem extends StackPane { StackPane graphicPane = new StackPane(); TwoLineListItem content = new TwoLineListItem(); HBox.setHgrow(content, Priority.ALWAYS); - content.setTitle(dataItem.getName()); - content.setSubtitle(I18n.formatDateTime(dataItem.getDatePublished())); + content.setTitle(targetVersion.getVersion()); + content.setSubtitle(I18n.formatDateTime(targetVersion.getDatePublished())); - switch (dataItem.getVersionType()) { + switch (targetVersion.getVersionType()) { case Alpha: content.addTag(i18n("mods.channel.alpha")); graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE.createIcon(Theme.blackFill(), 24)); @@ -322,7 +320,7 @@ private static final class ModItem extends StackPane { break; } - for (ModLoaderType modLoaderType : dataItem.getLoaders()) { + for (ModLoaderType modLoaderType : targetVersion.getLoaders()) { switch (modLoaderType) { case FORGE: content.addTag(i18n("install.installer.forge")); @@ -345,6 +343,8 @@ private static final class ModItem extends StackPane { } } + content.addTag(source); + descPane.getChildren().setAll(graphicPane, content); } @@ -363,39 +363,22 @@ private static final class ModDetail extends JFXDialogLayout { private final RemoteModRepository repository; - public ModDetail(RemoteMod.Version version, RemoteModRepository repository) { + public ModDetail(RemoteMod.Version targetVersion, RemoteModRepository repository, String source) { this.repository = repository; - RemoteModRepository.Type type = repository.getType(); - String title; - switch (type) { - case WORLD: - title = "world.download.title"; - break; - case MODPACK: - title = "modpack.download.title"; - break; - case RESOURCE_PACK: - title = "resourcepack.download.title"; - break; - case MOD: - default: - title = "mods.download.title"; - break; - } - this.setHeading(new HBox(new Label(i18n(title, version.getName())))); + this.setHeading(new HBox(new Label(i18n("mods.check_updates.update_mod", targetVersion.getName())))); VBox box = new VBox(8); box.setPadding(new Insets(8)); - ModItem modItem = new ModItem(version); + ModItem modItem = new ModItem(targetVersion, source); modItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again box.getChildren().setAll(modItem); SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); ComponentList changelogComponent = new ComponentList(Lang::immutableListOf); - loadChangelog(version, spinnerPane, changelogComponent); - spinnerPane.setOnFailedAction(e -> loadChangelog(version, spinnerPane, changelogComponent)); + loadChangelog(targetVersion, spinnerPane, changelogComponent); + spinnerPane.setOnFailedAction(e -> loadChangelog(targetVersion, spinnerPane, changelogComponent)); scrollPane.setContent(changelogComponent); scrollPane.setFitToWidth(true); @@ -425,16 +408,8 @@ private void loadChangelog(RemoteMod.Version version, SpinnerPane spinnerPane, C return version.getChangelog().isBlank() ? null : version.getChangelog(); } else { try { - String changelog = repository.getModChangelog(version.getModid(), version.getVersionId()); - Document document = Jsoup.parse(changelog); - Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); - document.outputSettings(outputSettings); - document.select("br").append("\\n"); - document.select("p").prepend("\\n"); - document.select("p").append("\\n"); - String newHtml = document.html().replaceAll("\\\\n", System.lineSeparator()); - String plainText = Jsoup.clean(newHtml, "", Safelist.none(), outputSettings).trim(); - return plainText.isBlank() ? null : plainText; + String changelog = StringUtils.htmlToText(repository.getModChangelog(version.getModid(), version.getVersionId())); + return changelog.isBlank() ? null : changelog; } catch (UnsupportedOperationException e) { return null; } @@ -442,7 +417,7 @@ private void loadChangelog(RemoteMod.Version version, SpinnerPane spinnerPane, C }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { if (result != null) { - componentList.getContent().setAll(new Text(result)); + componentList.getContent().setAll(new HBox(new Text(result))); } else { componentList.getContent().setAll(); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a9dc9e83c4..488704e47f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1069,6 +1069,7 @@ mods.check_updates.show_detail=Show Detail mods.check_updates.source=Source mods.check_updates.target_version=Target Version mods.check_updates.update=Update +mods.check_updates.update_mod=Update Mod - %1s mods.choose_mod=Choose mod mods.curseforge=CurseForge mods.dependency.embedded=Built-in Dependencies (Already packaged in the mod file by the author. No need to download separately) diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index dd1ead57ed..e01d171157 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -866,9 +866,11 @@ mods.check_updates.empty=無改囊可迭更 mods.check_updates.failed_check=檢囊迭更未成 mods.check_updates.failed_download=有引案未成 mods.check_updates.file=案 +mods.check_updates.show_detail=示詳 mods.check_updates.source=源 mods.check_updates.target_version=將至之版 mods.check_updates.update=迭更 +mods.check_updates.update_mod=迭更改囊 - %1s mods.choose_mod=擇改囊 mods.curseforge=CurseForge mods.dependency.embedded=既存之相依改囊 (既以內於改囊案,無須他引) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 71882ac121..6487fdd1d9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -862,9 +862,11 @@ mods.check_updates.empty=沒有需要更新的模組 mods.check_updates.failed_check=檢查更新失敗 mods.check_updates.failed_download=部分檔案下載失敗 mods.check_updates.file=檔案 +mods.check_updates.show_detail=顯示詳情 mods.check_updates.source=來源 mods.check_updates.target_version=目標版本 mods.check_updates.update=更新 +mods.check_updates.update_mod=更新模組 - %1s mods.choose_mod=選取模組 mods.curseforge=CurseForge mods.dependency.embedded=內建相依模組 (作者已經打包在模組檔中,無需單獨下載) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index d4a5e9c68c..83405f1f45 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -872,9 +872,11 @@ mods.check_updates.empty=没有需要更新的模组 mods.check_updates.failed_check=检查更新失败 mods.check_updates.failed_download=部分文件下载失败 mods.check_updates.file=文件 +mods.check_updates.show_detail=显示详情 mods.check_updates.source=来源 mods.check_updates.target_version=目标版本 mods.check_updates.update=更新 +mods.check_updates.update_mod=更新模组 - %1s mods.choose_mod=选择模组 mods.curseforge=CurseForge mods.dependency.embedded=内置的前置模组 (已经由作者打包在模组文件中,无需另外下载) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 26456b8f75..5d52c51530 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -17,6 +17,11 @@ */ package org.jackhuang.hmcl.util; +import org.jetbrains.annotations.Contract; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Safelist; + import java.io.PrintWriter; import java.io.StringWriter; import java.util.*; @@ -529,6 +534,21 @@ public static boolean isAlphabeticOrNumber(String str) { return true; } + @Contract(value = "null -> null", pure = true) + public static String htmlToText(String html) { + if (html == null) { + return null; + } + Document document = Jsoup.parse(html); + Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); + document.outputSettings(outputSettings); + document.select("br").append("\\n"); + document.select("p").prepend("\\n"); + document.select("p").append("\\n"); + String newHtml = document.html().replaceAll("\\\\n", System.lineSeparator()); + return Jsoup.clean(newHtml, "", Safelist.none(), outputSettings).trim(); + } + public static class LevCalculator { private int[][] lev; From 404548d5b3d89d8ea577d81dde33a999ea4ecbfb Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 21 Nov 2025 18:43:22 +0800 Subject: [PATCH 05/77] =?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20=E5=8F=AF=E8=83=BD=E4=B8=8D=E5=87=86=E7=A1=AE?= =?UTF-8?q?=E4=B8=8D=E5=9C=B0=E9=81=93=EF=BC=8C=E9=9C=80=E8=A6=81=E7=86=9F?= =?UTF-8?q?=E6=82=89=E8=BF=99=E4=BA=9B=E8=AF=AD=E8=A8=80=E7=9A=84=E4=BA=BA?= =?UTF-8?q?=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N_es.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_ja.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_ru.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_uk.properties | 2 ++ 4 files changed, 8 insertions(+) diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index f4e0bc6d41..3284315d61 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1068,9 +1068,11 @@ mods.check_updates.empty=Todos los mods están actualizados mods.check_updates.failed_check=No se ha podido comprobar si hay actualizaciones. mods.check_updates.failed_download=No se han podido descargar algunos de los archivos. mods.check_updates.file=Archivo +mods.check_updates.show_detail=Ver detalles mods.check_updates.source=Fuente mods.check_updates.target_version=Versión de destino mods.check_updates.update=Actualización +mods.check_updates.update_mod=Actualizar mod - %1s mods.choose_mod=Elige un mod mods.curseforge=CurseForge mods.dependency.embedded=Dependencias incorporadas (Already packaged in the mod file by the author. No need to download separately) diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index a7dd805c6b..46df0a6a55 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -674,9 +674,11 @@ mods.check_updates.current_version=Current mods.check_updates.failed_check=更新のチェックに失敗しました mods.check_updates.failed_download=一部のファイルのダウンロードに失敗しました mods.check_updates.file=ファイル +mods.check_updates.show_detail=詳細を表示 mods.check_updates.source=Source mods.check_updates.target_version=Target mods.check_updates.update=更新 +mods.check_updates.update_mod=Modを更新- %1s mods.choose_mod=modを選択してください mods.curseforge=CurseForge mods.disable=無効にする diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index e83949930f..da0e65465f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1063,9 +1063,11 @@ mods.check_updates.empty=Все моды новейшие mods.check_updates.failed_check=Не удалось проверить обновления. mods.check_updates.failed_download=Не удалось скачать некоторые файлы. mods.check_updates.file=Файл +mods.check_updates.show_detail=Подробнее mods.check_updates.source=Источник mods.check_updates.target_version=Целевая версия mods.check_updates.update=Обновить +mods.check_updates.update_mod=Обновить мод - %1s mods.choose_mod=Выберите мод mods.curseforge=CurseForge mods.dependency.embedded=Встроенные зависимости (Уже упакован в файл мода автором. Нет необходимости скачивать отдельно.) diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index e2684eb773..da226856d7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1005,9 +1005,11 @@ mods.check_updates.empty=Усі моди оновлені mods.check_updates.failed_check=Не вдалося перевірити оновлення. mods.check_updates.failed_download=Не вдалося завантажити деякі файли. mods.check_updates.file=Файл +mods.check_updates.show_detail=Детальніше mods.check_updates.source=Джерело mods.check_updates.target_version=Цільова версія mods.check_updates.update=Оновити +mods.check_updates.update_mod=Оновити мод - %1s mods.choose_mod=Вибрати мод mods.curseforge=CurseForge mods.dependency.embedded=Вбудовані залежності (Вже запаковані в файл мода автором. Не потрібно завантажувати окремо) From d3515232a7692dfd20c150664c81c1864b17cf98 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 23 Nov 2025 11:37:06 +0800 Subject: [PATCH 06/77] update --- .../hmcl/ui/versions/DownloadPage.java | 33 +++++++------------ .../hmcl/ui/versions/ModUpdatesPage.java | 24 ++++---------- .../org/jackhuang/hmcl/util/StringUtils.java | 7 +++- 3 files changed, 25 insertions(+), 39 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 04f8281f77..58905dd46f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -301,7 +301,7 @@ protected ModDownloadPageSkin(DownloadPage control) { for (String gameVersion : control.versions.keys().stream() .sorted(Collections.reverseOrder(GameVersionNumber::compare)) - .collect(Collectors.toList())) { + .toList()) { List versions = control.versions.get(gameVersion); if (versions == null || versions.isEmpty()) { continue; @@ -449,22 +449,12 @@ private static final class ModVersion extends JFXDialogLayout { public ModVersion(RemoteMod.Version version, DownloadPage selfPage) { RemoteModRepository.Type type = selfPage.repository.getType(); - String title; - switch (type) { - case WORLD: - title = "world.download.title"; - break; - case MODPACK: - title = "modpack.download.title"; - break; - case RESOURCE_PACK: - title = "resourcepack.download.title"; - break; - case MOD: - default: - title = "mods.download.title"; - break; - } + String title = switch (type) { + case WORLD -> "world.download.title"; + case MODPACK -> "modpack.download.title"; + case RESOURCE_PACK -> "resourcepack.download.title"; + default -> "mods.download.title"; + }; this.setHeading(new HBox(new Label(i18n(title, version.getName())))); VBox box = new VBox(8); @@ -530,12 +520,13 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag Task.supplyAsync(() -> { Optional changelog; if (version.getChangelog() != null) { - changelog = Optional.ofNullable(version.getChangelog().isBlank() ? null : version.getChangelog()); + changelog = StringUtils.nullIfBlank(version.getChangelog()); } else { try { - String changelogText = StringUtils.htmlToText(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())); - changelog = changelogText.isBlank() ? Optional.empty() : Optional.of(changelogText); - } catch (UnsupportedOperationException e) { + changelog = StringUtils.nullIfBlank( + StringUtils.htmlToText(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())) + ); + } catch (UnsupportedOperationException e) { // Should be impossible changelog = Optional.empty(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index c559b8be30..0f38347b5a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -39,7 +39,6 @@ import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; @@ -351,8 +350,7 @@ private static final class ModItem extends StackPane { pane.getChildren().add(descPane); } - RipplerContainer container = new RipplerContainer(pane); - getChildren().setAll(container); + getChildren().setAll(new RipplerContainer(pane)); // Workaround for https://github.com/HMCL-dev/HMCL/issues/2129 this.setMinHeight(50); @@ -370,13 +368,11 @@ public ModDetail(RemoteMod.Version targetVersion, RemoteModRepository repository VBox box = new VBox(8); box.setPadding(new Insets(8)); - ModItem modItem = new ModItem(targetVersion, source); - modItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again - box.getChildren().setAll(modItem); + box.getChildren().setAll(new ModItem(targetVersion, source)); SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); - ComponentList changelogComponent = new ComponentList(Lang::immutableListOf); + ComponentList changelogComponent = new ComponentList(null); loadChangelog(targetVersion, spinnerPane, changelogComponent); spinnerPane.setOnFailedAction(e -> loadChangelog(targetVersion, spinnerPane, changelogComponent)); @@ -405,25 +401,19 @@ private void loadChangelog(RemoteMod.Version version, SpinnerPane spinnerPane, C spinnerPane.setLoading(true); Task.supplyAsync(() -> { if (version.getChangelog() != null) { - return version.getChangelog().isBlank() ? null : version.getChangelog(); + return StringUtils.nullIfBlank(version.getChangelog()); } else { try { - String changelog = StringUtils.htmlToText(repository.getModChangelog(version.getModid(), version.getVersionId())); - return changelog.isBlank() ? null : changelog; + return StringUtils.nullIfBlank(StringUtils.htmlToText(repository.getModChangelog(version.getModid(), version.getVersionId()))); } catch (UnsupportedOperationException e) { - return null; + return Optional.empty(); } } }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - if (result != null) { - componentList.getContent().setAll(new HBox(new Text(result))); - } else { - componentList.getContent().setAll(); - } + result.ifPresent(s -> componentList.getContent().setAll(new HBox(new Text(s)))); spinnerPane.setFailedReason(null); } else { - componentList.getContent().setAll(); spinnerPane.setFailedReason(i18n("download.failed.refresh")); } spinnerPane.setLoading(false); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 5d52c51530..f66d9df3bc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -534,6 +534,11 @@ public static boolean isAlphabeticOrNumber(String str) { return true; } + @Contract(pure = true) + public static Optional nullIfBlank(String str) { + return Optional.ofNullable(str).map(s -> s.isBlank() ? null : s); + } + @Contract(value = "null -> null", pure = true) public static String htmlToText(String html) { if (html == null) { @@ -546,7 +551,7 @@ public static String htmlToText(String html) { document.select("p").prepend("\\n"); document.select("p").append("\\n"); String newHtml = document.html().replaceAll("\\\\n", System.lineSeparator()); - return Jsoup.clean(newHtml, "", Safelist.none(), outputSettings).trim(); + return Jsoup.clean(newHtml, "", Safelist.none(), outputSettings); } public static class LevCalculator { From cb3fc8420f93f215fa5866047a932f629d44560b Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 30 Nov 2025 18:32:08 +0800 Subject: [PATCH 07/77] update --- .../org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index f830043cdb..f4344050eb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -30,10 +30,10 @@ import javafx.scene.layout.*; import javafx.scene.text.Text; import org.jackhuang.hmcl.mod.*; -import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.theme.Theme; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; @@ -307,15 +307,15 @@ private static final class ModItem extends StackPane { switch (targetVersion.getVersionType()) { case Alpha: content.addTag(i18n("mods.channel.alpha")); - graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE.createIcon(Theme.blackFill(), 24)); + graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE.createIcon(24)); break; case Beta: content.addTag(i18n("mods.channel.beta")); - graphicPane.getChildren().setAll(SVG.BETA_CIRCLE.createIcon(Theme.blackFill(), 24)); + graphicPane.getChildren().setAll(SVG.BETA_CIRCLE.createIcon(24)); break; case Release: content.addTag(i18n("mods.channel.release")); - graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE.createIcon(Theme.blackFill(), 24)); + graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE.createIcon(24)); break; } From 998c6d132d5752cfc1d8b7d2700961ab00611783 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 30 Nov 2025 18:34:12 +0800 Subject: [PATCH 08/77] update --- .../main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index f4344050eb..da9d15ecf9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -33,7 +33,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.theme.Theme; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; From c9c7ea5e641154410b819e5fa1e99690ce403273 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 1 Dec 2025 19:54:32 +0800 Subject: [PATCH 09/77] update --- .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index da9d15ecf9..e6f0df8f21 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -79,15 +79,15 @@ public ModUpdatesPage(ModManager modManager, List update enabledColumn.setMinWidth(40); TableColumn fileNameColumn = new TableColumn<>(i18n("mods.check_updates.file")); - fileNameColumn.setPrefWidth(200); + fileNameColumn.setPrefWidth(180); setupCellValueFactory(fileNameColumn, ModUpdateObject::fileNameProperty); TableColumn currentVersionColumn = new TableColumn<>(i18n("mods.check_updates.current_version")); - currentVersionColumn.setPrefWidth(200); + currentVersionColumn.setPrefWidth(180); setupCellValueFactory(currentVersionColumn, ModUpdateObject::currentVersionProperty); TableColumn targetVersionColumn = new TableColumn<>(i18n("mods.check_updates.target_version")); - targetVersionColumn.setPrefWidth(200); + targetVersionColumn.setPrefWidth(180); setupCellValueFactory(targetVersionColumn, ModUpdateObject::targetVersionProperty); TableColumn sourceColumn = new TableColumn<>(i18n("mods.check_updates.source")); From 0973c76e233027a8a600d39fefc9e484fd980ed9 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 12 Dec 2025 22:28:53 +0800 Subject: [PATCH 10/77] update --- .../hmcl/ui/versions/ModUpdatesPage.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index c4233fcede..a258656b97 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -18,8 +18,8 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXDialogLayout; import com.jfoenix.controls.JFXCheckBox; +import com.jfoenix.controls.JFXDialogLayout; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -27,17 +27,9 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; -import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.layout.*; import javafx.scene.text.Text; import org.jackhuang.hmcl.mod.*; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.ModManager; -import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -45,9 +37,6 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTableCell; -import org.jackhuang.hmcl.ui.construct.MessageDialogPane; -import org.jackhuang.hmcl.ui.construct.PageCloseEvent; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; @@ -60,7 +49,10 @@ import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; From 50a2aeb85954ca29ecb4635bb75c6f88b683110e Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 13 Dec 2025 13:33:40 +0800 Subject: [PATCH 11/77] HTML support & changelog cache --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 17 +++++++ .../java/org/jackhuang/hmcl/ui/WebPage.java | 6 +-- .../hmcl/ui/versions/DownloadPage.java | 34 ++++++++++--- .../hmcl/ui/versions/ModUpdatesPage.java | 50 ++++++++++++++----- HMCL/src/main/resources/assets/css/root.css | 17 +++++++ .../jackhuang/hmcl/ui/HTMLRendererTest.java | 28 +++++++++++ .../org/jackhuang/hmcl/util/StringUtils.java | 18 ------- 7 files changed, 127 insertions(+), 43 deletions(-) create mode 100644 HMCL/src/test/java/org/jackhuang/hmcl/ui/HTMLRendererTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index a7dcc2643d..e45e412a2a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -30,13 +30,17 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import java.util.regex.Pattern; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** * @author Glavo */ public final class HTMLRenderer { + public static final Pattern HTML_PATTERN = Pattern.compile("<[^>]+>"); + private static URI resolveLink(Node linkNode) { String href = linkNode.absUrl("href"); if (href.isEmpty()) @@ -49,6 +53,19 @@ private static URI resolveLink(Node linkNode) { } } + public static boolean isHTML(String str) { + if (str == null) return false; + return HTML_PATTERN.matcher(str).find(); + } + + 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 children = new ArrayList<>(); private final List stack = new ArrayList<>(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebPage.java index 2542d698f0..2b56bc9573 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebPage.java @@ -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(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 28411edb0a..d96cb7b910 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -44,6 +44,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.HTMLRenderer; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -53,6 +54,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; +import org.jsoup.Jsoup; import java.nio.file.Path; import java.util.*; @@ -63,6 +65,8 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DownloadPage extends Control implements DecoratorPage { + private static final WeakHashMap changelogCache = new WeakHashMap<>(); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final BooleanProperty loaded = new SimpleBooleanProperty(false); private final BooleanProperty loading = new SimpleBooleanProperty(false); @@ -518,14 +522,14 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag spinnerPane.setLoading(true); Task.supplyAsync(() -> { Optional changelog; - if (version.getChangelog() != null) { + 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( - StringUtils.htmlToText(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())) - ); - } catch (UnsupportedOperationException e) { // Should be impossible + changelog = StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())); + } catch (UnsupportedOperationException e) { changelog = Optional.empty(); } } @@ -540,7 +544,7 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag List list = new LinkedList<>(); Label title = new Label(i18n(DependencyModItem.I18N_KEY.get(dependency.getType()))); title.setPadding(new Insets(0, 8, 0, 8)); - list.add(title); + list.add(new HBox(title)); dependencies.put(dependency.getType(), list); } DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback); @@ -551,7 +555,23 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { List nodes = new LinkedList<>(); - result.getKey().ifPresent(s -> nodes.add(new HBox(new Text(s)))); + result.getKey().ifPresent(s -> { + changelogCache.put(version, s); + HBox container; + if (HTMLRenderer.isHTML(s)) { + var document = Jsoup.parse(s); + HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); + renderer.appendNode(document); + renderer.mergeLineBreaks(); + var textFlow = renderer.render(); + textFlow.setPrefWidth(Region.USE_COMPUTED_SIZE); + container = new HBox(textFlow); + } else { + container = new HBox(new Text(s)); + } + container.getStyleClass().add("mod-changelog"); + nodes.add(container); + }); nodes.addAll(result.getValue()); dependenciesList.getContent().setAll(nodes); spinnerPane.setFailedReason(null); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index a258656b97..9777e36707 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -35,6 +35,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.HTMLRenderer; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -44,6 +45,7 @@ import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.CSVTable; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jsoup.Jsoup; import java.nio.file.Path; import java.nio.file.Paths; @@ -106,7 +108,7 @@ public ModUpdatesPage(ModManager modManager, List update return; } ModUpdateObject object = items.get(cell.getIndex()); - Controllers.dialog(new ModDetail(object.data.getCandidates().get(0), object.data.getRepository(), object.getSource())); + Controllers.dialog(new ModDetail(object)); }); return cell; }); @@ -212,6 +214,7 @@ private static final class ModUpdateObject { final StringProperty currentVersion = new SimpleStringProperty(); final StringProperty targetVersion = new SimpleStringProperty(); final StringProperty source = new SimpleStringProperty(); + String changelog = null; public ModUpdateObject(LocalModFile.ModUpdate data) { this.data = data; @@ -366,8 +369,10 @@ private static final class ModDetail extends JFXDialogLayout { private final RemoteModRepository repository; - public ModDetail(RemoteMod.Version targetVersion, RemoteModRepository repository, String source) { - this.repository = repository; + public ModDetail(ModUpdateObject object) { + this.repository = object.data.getRepository(); + RemoteMod.Version targetVersion = object.data.getCandidates().get(0); + String source = object.getSource(); this.setHeading(new HBox(new Label(i18n("mods.check_updates.update_mod", targetVersion.getName())))); @@ -378,8 +383,8 @@ public ModDetail(RemoteMod.Version targetVersion, RemoteModRepository repository SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); ComponentList changelogComponent = new ComponentList(null); - loadChangelog(targetVersion, spinnerPane, changelogComponent); - spinnerPane.setOnFailedAction(e -> loadChangelog(targetVersion, spinnerPane, changelogComponent)); + loadChangelog(object, spinnerPane, changelogComponent); + spinnerPane.setOnFailedAction(e -> loadChangelog(object, spinnerPane, changelogComponent)); scrollPane.setContent(changelogComponent); scrollPane.setFitToWidth(true); @@ -402,21 +407,40 @@ public ModDetail(RemoteMod.Version targetVersion, RemoteModRepository repository onEscPressed(this, closeButton::fire); } - private void loadChangelog(RemoteMod.Version version, SpinnerPane spinnerPane, ComponentList componentList) { + private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, ComponentList componentList) { spinnerPane.setLoading(true); Task.supplyAsync(() -> { + if (object.changelog != null) { + return Optional.of(object.changelog); + } + RemoteMod.Version version = object.data.getCandidates().get(0); if (version.getChangelog() != null) { return StringUtils.nullIfBlank(version.getChangelog()); - } else { - try { - return StringUtils.nullIfBlank(StringUtils.htmlToText(repository.getModChangelog(version.getModid(), version.getVersionId()))); - } catch (UnsupportedOperationException e) { - return Optional.empty(); - } + } + try { + return StringUtils.nullIfBlank(repository.getModChangelog(version.getModid(), version.getVersionId())); + } catch (UnsupportedOperationException e) { + return Optional.empty(); } }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - result.ifPresent(s -> componentList.getContent().setAll(new HBox(new Text(s)))); + result.ifPresent(s -> { + object.changelog = s; + HBox container; + if (HTMLRenderer.isHTML(s)) { + var document = Jsoup.parse(s); + HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); + renderer.appendNode(document); + renderer.mergeLineBreaks(); + var textFlow = renderer.render(); + textFlow.setPrefWidth(Region.USE_COMPUTED_SIZE); + container = new HBox(textFlow); + } else { + container = new HBox(new Text(s)); + } + container.getStyleClass().add("mod-changelog"); + componentList.getContent().setAll(container); + }); spinnerPane.setFailedReason(null); } else { spinnerPane.setFailedReason(i18n("download.failed.refresh")); diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 5b0ade9c98..bcc814b39d 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -1749,6 +1749,23 @@ -fx-font-style: italic; } +.mod-changelog .html, +.mod-changelog .text { + -fx-font-size: 12; +} + +.mod-changelog .html-h1 { + -fx-font-size: 16.5; +} + +.mod-changelog .html-h2 { + -fx-font-size: 15; +} + +.mod-changelog .html-h3 { + -fx-font-size: 13.5; +} + /******************************************************************************* * * * Tooltip * diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/ui/HTMLRendererTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/ui/HTMLRendererTest.java new file mode 100644 index 0000000000..5bfe7ee929 --- /dev/null +++ b/HMCL/src/test/java/org/jackhuang/hmcl/ui/HTMLRendererTest.java @@ -0,0 +1,28 @@ +package org.jackhuang.hmcl.ui; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HTMLRendererTest { + + @Test + public void isHTMLTest() { + assertTrue(HTMLRenderer.isHTML("Bold")); + assertTrue(HTMLRenderer.isHTML("Some text with link.")); + assertTrue(HTMLRenderer.isHTML(""" + A DIV +
+ \t

+ \t\tParagraph + \t

+
""")); + assertTrue(HTMLRenderer.isHTML("\"Image\"")); + assertFalse(HTMLRenderer.isHTML(null)); + assertFalse(HTMLRenderer.isHTML("")); + assertFalse(HTMLRenderer.isHTML("<>")); + assertFalse(HTMLRenderer.isHTML("Just a plain text.")); + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index f66d9df3bc..aef1c3e65d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -18,9 +18,6 @@ package org.jackhuang.hmcl.util; import org.jetbrains.annotations.Contract; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.safety.Safelist; import java.io.PrintWriter; import java.io.StringWriter; @@ -539,21 +536,6 @@ public static Optional nullIfBlank(String str) { return Optional.ofNullable(str).map(s -> s.isBlank() ? null : s); } - @Contract(value = "null -> null", pure = true) - public static String htmlToText(String html) { - if (html == null) { - return null; - } - Document document = Jsoup.parse(html); - Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); - document.outputSettings(outputSettings); - document.select("br").append("\\n"); - document.select("p").prepend("\\n"); - document.select("p").append("\\n"); - String newHtml = document.html().replaceAll("\\\\n", System.lineSeparator()); - return Jsoup.clean(newHtml, "", Safelist.none(), outputSettings); - } - public static class LevCalculator { private int[][] lev; From e3f05fb2181db44fbbf6e678f1aea530dc06a564 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 13 Dec 2025 15:03:26 +0800 Subject: [PATCH 12/77] Markdown --- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 14 ++++++++++ .../hmcl/ui/versions/DownloadPage.java | 20 +++---------- .../hmcl/ui/versions/ModUpdatesPage.java | 20 +++---------- .../jackhuang/hmcl/ui/HTMLRendererTest.java | 28 ------------------- HMCLCore/build.gradle.kts | 1 + .../org/jackhuang/hmcl/util/StringUtils.java | 8 ++++++ gradle/libs.versions.toml | 2 ++ 7 files changed, 33 insertions(+), 60 deletions(-) delete mode 100644 HMCL/src/test/java/org/jackhuang/hmcl/ui/HTMLRendererTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index c92acf202c..6a4d70b5bf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -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; @@ -1576,4 +1577,17 @@ 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 HBox renderModChangelog(String changelogHTML) { + HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); + renderer.appendNode(Jsoup.parse(changelogHTML)); + renderer.mergeLineBreaks(); + + var textFlow = renderer.render(); + textFlow.setPrefWidth(Region.USE_COMPUTED_SIZE); + + HBox container = new HBox(textFlow); + container.getStyleClass().add("mod-changelog"); + return container; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index d96cb7b910..9ab0ec1300 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -30,7 +30,6 @@ import javafx.scene.control.*; import javafx.scene.image.ImageView; import javafx.scene.layout.*; -import javafx.scene.text.Text; import javafx.stage.FileChooser; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.HMCLGameRepository; @@ -54,7 +53,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; -import org.jsoup.Jsoup; import java.nio.file.Path; import java.util.*; @@ -556,21 +554,11 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag if (exception == null) { List nodes = new LinkedList<>(); result.getKey().ifPresent(s -> { - changelogCache.put(version, s); - HBox container; - if (HTMLRenderer.isHTML(s)) { - var document = Jsoup.parse(s); - HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); - renderer.appendNode(document); - renderer.mergeLineBreaks(); - var textFlow = renderer.render(); - textFlow.setPrefWidth(Region.USE_COMPUTED_SIZE); - container = new HBox(textFlow); - } else { - container = new HBox(new Text(s)); + if (!HTMLRenderer.isHTML(s)) { + s = StringUtils.markdownToHTML(s); } - container.getStyleClass().add("mod-changelog"); - nodes.add(container); + changelogCache.put(version, s); + nodes.add(FXUtils.renderModChangelog(s)); }); nodes.addAll(result.getValue()); dependenciesList.getContent().setAll(nodes); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 9777e36707..802c1e15ec 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -28,7 +28,6 @@ import javafx.geometry.Pos; import javafx.scene.control.*; import javafx.scene.layout.*; -import javafx.scene.text.Text; import org.jackhuang.hmcl.mod.*; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; @@ -45,7 +44,6 @@ import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.CSVTable; import org.jackhuang.hmcl.util.javafx.BindingMapping; -import org.jsoup.Jsoup; import java.nio.file.Path; import java.nio.file.Paths; @@ -425,21 +423,11 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Comp }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { result.ifPresent(s -> { - object.changelog = s; - HBox container; - if (HTMLRenderer.isHTML(s)) { - var document = Jsoup.parse(s); - HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); - renderer.appendNode(document); - renderer.mergeLineBreaks(); - var textFlow = renderer.render(); - textFlow.setPrefWidth(Region.USE_COMPUTED_SIZE); - container = new HBox(textFlow); - } else { - container = new HBox(new Text(s)); + if (!HTMLRenderer.isHTML(s)) { + s = StringUtils.markdownToHTML(s); } - container.getStyleClass().add("mod-changelog"); - componentList.getContent().setAll(container); + object.changelog = s; + componentList.getContent().setAll(FXUtils.renderModChangelog(s)); }); spinnerPane.setFailedReason(null); } else { diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/ui/HTMLRendererTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/ui/HTMLRendererTest.java deleted file mode 100644 index 5bfe7ee929..0000000000 --- a/HMCL/src/test/java/org/jackhuang/hmcl/ui/HTMLRendererTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.jackhuang.hmcl.ui; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class HTMLRendererTest { - - @Test - public void isHTMLTest() { - assertTrue(HTMLRenderer.isHTML("Bold")); - assertTrue(HTMLRenderer.isHTML("Some text with link.")); - assertTrue(HTMLRenderer.isHTML(""" - A DIV -
- \t

- \t\tParagraph - \t

-
""")); - assertTrue(HTMLRenderer.isHTML("\"Image\"")); - assertFalse(HTMLRenderer.isHTML(null)); - assertFalse(HTMLRenderer.isHTML("")); - assertFalse(HTMLRenderer.isHTML("<>")); - assertFalse(HTMLRenderer.isHTML("Just a plain text.")); - } - -} diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index 86ca2bde92..d99f994f24 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { api(libs.chardet) api(libs.jna) api(libs.pci.ids) + api(libs.commonmark) compileOnlyApi(libs.jetbrains.annotations) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index aef1c3e65d..4e266edbe2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.util; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; import org.jetbrains.annotations.Contract; import java.io.PrintWriter; @@ -536,6 +538,12 @@ public static Optional nullIfBlank(String str) { return Optional.ofNullable(str).map(s -> s.isBlank() ? null : s); } + @Contract(pure = true, value = "null -> null") + public static String markdownToHTML(String md) { + if (md == null) return null; + return HtmlRenderer.builder().build().render(Parser.builder().build().parse(md)); + } + public static class LevCalculator { private int[][] lev; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7724d2e4cb..826870e266 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ pci-ids = "0.4.0" java-info = "1.0" authlib-injector = "1.2.6" monet-fx = "0.4.0" +commonmark = "0.27.0" # testing junit = "6.0.1" @@ -48,6 +49,7 @@ pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" } java-info = { module = "org.glavo:java-info", version.ref = "java-info" } authlib-injector = { module = "org.glavo.hmcl:authlib-injector", version.ref = "authlib-injector" } monet-fx = { module = "org.glavo:MonetFX", version.ref = "monet-fx" } +commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" } # testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } From b35897bc850411265c88e3caa5a4e82489d29368 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 19 Dec 2025 23:00:13 +0800 Subject: [PATCH 13/77] =?UTF-8?q?update=20=E6=94=AF=E6=8C=81=E8=A3=B8URL?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E6=9B=B4=E6=96=B0=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=BF=87=E9=95=BF=E6=97=B6=E6=98=BE=E7=A4=BA=E4=B8=8D=E5=85=A8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java | 7 ++++--- HMCLCore/build.gradle.kts | 1 + .../src/main/java/org/jackhuang/hmcl/util/StringUtils.java | 4 +++- gradle/libs.versions.toml | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 6a4d70b5bf..418f227efb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -1578,16 +1578,17 @@ public static JFXPopup.PopupVPosition determineOptimalPopupPosition(Node root, J : JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward } - public static HBox renderModChangelog(String changelogHTML) { + public static ScrollPane renderModChangelog(String changelogHTML) { HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); renderer.appendNode(Jsoup.parse(changelogHTML)); renderer.mergeLineBreaks(); var textFlow = renderer.render(); - textFlow.setPrefWidth(Region.USE_COMPUTED_SIZE); + textFlow.getChildren().add(new Text(System.lineSeparator())); // Add a newline at the end to prevent cutoff - HBox container = new HBox(textFlow); + var container = new ScrollPane(textFlow); container.getStyleClass().add("mod-changelog"); + container.setMinHeight(Region.USE_PREF_SIZE); return container; } } diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index d99f994f24..659865d1b6 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { api(libs.jna) api(libs.pci.ids) api(libs.commonmark) + api(libs.commonmark.autolink) compileOnlyApi(libs.jetbrains.annotations) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 4e266edbe2..f1274db16f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.util; +import org.commonmark.ext.autolink.AutolinkExtension; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.jetbrains.annotations.Contract; @@ -541,7 +542,8 @@ public static Optional nullIfBlank(String str) { @Contract(pure = true, value = "null -> null") public static String markdownToHTML(String md) { if (md == null) return null; - return HtmlRenderer.builder().build().render(Parser.builder().build().parse(md)); + var extensions = List.of(AutolinkExtension.create()); + return HtmlRenderer.builder().build().render(Parser.builder().extensions(extensions).build().parse(md)); } public static class LevCalculator { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 826870e266..8894461e99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,7 @@ java-info = { module = "org.glavo:java-info", version.ref = "java-info" } authlib-injector = { module = "org.glavo.hmcl:authlib-injector", version.ref = "authlib-injector" } monet-fx = { module = "org.glavo:MonetFX", version.ref = "monet-fx" } commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" } +commonmark-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" } # testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } From 1915cfd4fea6e045bf992b24b7a3f205209bded8 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 20 Dec 2025 13:31:51 +0800 Subject: [PATCH 14/77] Add deps --- HMCL/src/main/resources/assets/about/deps.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HMCL/src/main/resources/assets/about/deps.json b/HMCL/src/main/resources/assets/about/deps.json index d72871ec0c..22f4fc1fcc 100644 --- a/HMCL/src/main/resources/assets/about/deps.json +++ b/HMCL/src/main/resources/assets/about/deps.json @@ -83,5 +83,10 @@ "title": "MonetFX", "subtitle": "Copyright © 2025 Glavo.\nLicensed under the Apache 2.0 License.", "externalLink": "https://github.com/Glavo/MonetFX" + }, + { + "title": "CommonMark", + "subtitle": "Copyright (c) 2015, Robin Stocker\nAll rights reserved.", + "externalLink": "https://github.com/commonmark/commonmark-java" } ] \ No newline at end of file From 212d8be947a10974d6cf44cfe992b7460ce7ca39 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 20 Dec 2025 13:37:09 +0800 Subject: [PATCH 15/77] update --- HMCL/src/main/resources/assets/about/deps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/resources/assets/about/deps.json b/HMCL/src/main/resources/assets/about/deps.json index 22f4fc1fcc..26a67597f5 100644 --- a/HMCL/src/main/resources/assets/about/deps.json +++ b/HMCL/src/main/resources/assets/about/deps.json @@ -86,7 +86,7 @@ }, { "title": "CommonMark", - "subtitle": "Copyright (c) 2015, Robin Stocker\nAll rights reserved.", + "subtitle": "Copyright (c) 2015 Robin Stocker.\nAll rights reserved.", "externalLink": "https://github.com/commonmark/commonmark-java" } ] \ No newline at end of file From 9fd3efc866d6d24e1cb22708478bf529e224beab Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 2 Jan 2026 22:26:28 +0800 Subject: [PATCH 16/77] update --- .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 08ccf973b3..2bb5b7c103 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -543,7 +543,7 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag List list = new ArrayList<>(); Label title = new Label(i18n(DependencyModItem.I18N_KEY.get(dependency.getType()))); title.setPadding(new Insets(0, 8, 0, 8)); - list.add(new HBox(title)); + list.add(new HBox(title)); // So that it stays on the left dependencies.put(dependency.getType(), list); } DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback); From 5f81a392df7b99742d6189dd31d02054f5495ff5 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 2 Jan 2026 22:28:03 +0800 Subject: [PATCH 17/77] update --- .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 2bb5b7c103..ff3ec44fbd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -543,7 +543,7 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag List list = new ArrayList<>(); Label title = new Label(i18n(DependencyModItem.I18N_KEY.get(dependency.getType()))); title.setPadding(new Insets(0, 8, 0, 8)); - list.add(new HBox(title)); // So that it stays on the left + list.add(title); dependencies.put(dependency.getType(), list); } DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback); From bdf6ef43f072420e1715802298bc6dd4b6e631ae Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 3 Jan 2026 13:37:17 +0800 Subject: [PATCH 18/77] update --- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 11 +-- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 7 -- .../hmcl/ui/versions/DownloadPage.java | 89 ++++++++++++++----- .../hmcl/ui/versions/ModUpdatesPage.java | 23 ++--- HMCL/src/main/resources/assets/css/root.css | 7 +- .../resources/assets/lang/I18N.properties | 2 +- .../resources/assets/lang/I18N_es.properties | 2 +- .../resources/assets/lang/I18N_ja.properties | 2 +- .../resources/assets/lang/I18N_ru.properties | 2 +- .../resources/assets/lang/I18N_uk.properties | 2 +- .../resources/assets/lang/I18N_zh.properties | 2 +- .../assets/lang/I18N_zh_CN.properties | 2 +- 12 files changed, 89 insertions(+), 62 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 3c714b1c1b..4781fc6251 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -1572,17 +1572,10 @@ public static JFXPopup.PopupVPosition determineOptimalPopupPosition(Node root, J : JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward } - public static ScrollPane renderModChangelog(String changelogHTML) { + public static TextFlow renderModChangelog(String changelogHTML) { HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); renderer.appendNode(Jsoup.parse(changelogHTML)); renderer.mergeLineBreaks(); - - var textFlow = renderer.render(); - textFlow.getChildren().add(new Text(System.lineSeparator())); // Add a newline at the end to prevent cutoff - - var container = new ScrollPane(textFlow); - container.getStyleClass().add("mod-changelog"); - container.setMinHeight(Region.USE_PREF_SIZE); - return container; + return renderer.render(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index e45e412a2a..f3165dd4f4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -30,7 +30,6 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; -import java.util.regex.Pattern; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -39,7 +38,6 @@ * @author Glavo */ public final class HTMLRenderer { - public static final Pattern HTML_PATTERN = Pattern.compile("<[^>]+>"); private static URI resolveLink(Node linkNode) { String href = linkNode.absUrl("href"); @@ -53,11 +51,6 @@ private static URI resolveLink(Node linkNode) { } } - public static boolean isHTML(String str) { - if (str == null) return false; - return HTML_PATTERN.matcher(str).find(); - } - public static HTMLRenderer openHyperlinkInBrowser() { return new HTMLRenderer(uri -> { Controllers.confirm(i18n("web.open_in_browser", uri), i18n("message.confirm"), () -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index ff3ec44fbd..2f064f12ff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -43,7 +43,6 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.HTMLRenderer; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -376,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)); @@ -438,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 @@ -447,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(); @@ -457,25 +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 changelogAndDependenciesList = new ComponentList(Lang::immutableListOf); - loadChangelogAndDependencies(version, selfPage, spinnerPane, changelogAndDependenciesList); - spinnerPane.setOnFailedAction(e -> loadChangelogAndDependencies(version, selfPage, spinnerPane, changelogAndDependenciesList)); + ComponentList dependenciesList = new ComponentList(Lang::immutableListOf); + loadChangelogAndDependencies(version, selfPage, spinnerPane, dependenciesList, changelogButton); + spinnerPane.setOnFailedAction(e -> loadChangelogAndDependencies(version, selfPage, spinnerPane, dependenciesList, changelogButton)); - scrollPane.setContent(changelogAndDependenciesList); + 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); @@ -506,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)); @@ -517,8 +525,9 @@ public ModVersion(RemoteMod.Version version, DownloadPage selfPage) { onEscPressed(this, cancelButton::fire); } - private void loadChangelogAndDependencies(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 changelog; if (changelogCache.containsKey(version)) { @@ -553,18 +562,19 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag return new Pair<>(changelog, dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - List nodes = new LinkedList<>(); - result.getKey().ifPresent(s -> { - if (!HTMLRenderer.isHTML(s)) { - s = StringUtils.markdownToHTML(s); - } + if (result.getKey().isPresent()) { + String s = StringUtils.markdownToHTML(result.getKey().get()); changelogCache.put(version, s); - nodes.add(FXUtils.renderModChangelog(s)); - }); - nodes.addAll(result.getValue()); - dependenciesList.getContent().setAll(nodes); + 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")); } @@ -573,6 +583,39 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag } } + 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); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 802c1e15ec..7a3fb25c9d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -34,7 +34,6 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.HTMLRenderer; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -110,7 +109,7 @@ public ModUpdatesPage(ModManager modManager, List update }); return cell; }); - detailColumn.setCellValueFactory(it -> new SimpleStringProperty(i18n("mods.check_updates.show_detail"))); + detailColumn.setCellValueFactory(it -> new SimpleStringProperty(i18n("mods.show_detail"))); objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList())); FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new)); @@ -380,13 +379,12 @@ public ModDetail(ModUpdateObject object) { SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); - ComponentList changelogComponent = new ComponentList(null); - loadChangelog(object, spinnerPane, changelogComponent); - spinnerPane.setOnFailedAction(e -> loadChangelog(object, spinnerPane, changelogComponent)); - - scrollPane.setContent(changelogComponent); + scrollPane.getStyleClass().add("mod-changelog"); scrollPane.setFitToWidth(true); - scrollPane.setFitToHeight(true); + + loadChangelog(object, spinnerPane, scrollPane); + spinnerPane.setOnFailedAction(e -> loadChangelog(object, spinnerPane, scrollPane)); + spinnerPane.setContent(scrollPane); box.getChildren().add(spinnerPane); VBox.setVgrow(spinnerPane, Priority.SOMETIMES); @@ -405,7 +403,7 @@ public ModDetail(ModUpdateObject object) { onEscPressed(this, closeButton::fire); } - private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, ComponentList componentList) { + private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) { spinnerPane.setLoading(true); Task.supplyAsync(() -> { if (object.changelog != null) { @@ -422,12 +420,9 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Comp } }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - result.ifPresent(s -> { - if (!HTMLRenderer.isHTML(s)) { - s = StringUtils.markdownToHTML(s); - } + result.map(StringUtils::markdownToHTML).ifPresent(s -> { object.changelog = s; - componentList.getContent().setAll(FXUtils.renderModChangelog(s)); + scrollPane.setContent(FXUtils.renderModChangelog(s)); }); spinnerPane.setFailedReason(null); } else { diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 157fbbc1e1..87c096f666 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -1833,9 +1833,12 @@ -fx-font-style: italic; } -.mod-changelog .html, -.mod-changelog .text { +.mod-changelog .html { + -fx-background-color: -monet-surface; + -fx-background-radius: 4; + -fx-padding: 10; -fx-font-size: 12; + -fx-text-fill: -monet-on-surface; } .mod-changelog .html-h1 { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index f0fdcefc9f..f8bf1dd833 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1069,7 +1069,6 @@ mods.check_updates.empty=All mods are up-to-date mods.check_updates.failed_check=Failed to check for updates. mods.check_updates.failed_download=Failed to download some files. mods.check_updates.file=File -mods.check_updates.show_detail=Show Detail mods.check_updates.source=Source mods.check_updates.target_version=Target Version mods.check_updates.update_mod=Update Mod - %1s @@ -1102,6 +1101,7 @@ mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparab mods.warning.loader_mismatch=Mod loader mismatch mods.install=Install mods.save_as=Save As +mods.show_detail=Show Detail mods.unknown=Unknown Mod nbt.entries=%s entries diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 88c7f72ecb..3ba922175e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1069,7 +1069,6 @@ mods.check_updates.empty=Todos los mods están actualizados mods.check_updates.failed_check=No se ha podido comprobar si hay actualizaciones. mods.check_updates.failed_download=No se han podido descargar algunos de los archivos. mods.check_updates.file=Archivo -mods.check_updates.show_detail=Ver detalles mods.check_updates.source=Fuente mods.check_updates.target_version=Versión de destino mods.check_updates.update_mod=Actualizar mod - %1s @@ -1101,6 +1100,7 @@ mods.url=Página oficial mods.update_modpack_mod.warning=Actualizar mods en un modpack puede generar resultados irreparables, posiblemente corrompiendo el modpack para que no pueda iniciarse. ¿Seguro que quieres actualizar? mods.install=Instalar mods.save_as=Guardar como +mods.show_detail=Ver detalles nbt.entries=%s entradas nbt.open.failed=No se ha podido abrir el archivo diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 4c04e702c2..1131f7a37d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -674,7 +674,6 @@ mods.check_updates.current_version=Current mods.check_updates.failed_check=更新のチェックに失敗しました mods.check_updates.failed_download=一部のファイルのダウンロードに失敗しました mods.check_updates.file=ファイル -mods.check_updates.show_detail=詳細を表示 mods.check_updates.source=Source mods.check_updates.target_version=Target mods.check_updates.update_mod=Modを更新- %1s @@ -695,6 +694,7 @@ mods.not_modded=最初にmodloaderをインストールする必要がありま mods.restore=Restore mods.url=公式ページ mods.update_modpack_mod.warning=modpack 内の mod を更新すると、修復不可能な結果につながる可能性があり、modpack が破損して起動できなくなる可能性があります。更新してもよろしいですか? +mods.show_detail=詳細を表示 datapack=Datapacks datapack.add=データパックをインストールします diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 00dfe6f159..239762c9b6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1064,7 +1064,6 @@ mods.check_updates.empty=Все моды новейшие mods.check_updates.failed_check=Не удалось проверить обновления. mods.check_updates.failed_download=Не удалось скачать некоторые файлы. mods.check_updates.file=Файл -mods.check_updates.show_detail=Подробнее mods.check_updates.source=Источник mods.check_updates.target_version=Целевая версия mods.check_updates.update_mod=Обновить мод - %1s @@ -1096,6 +1095,7 @@ mods.url=Официальная страница mods.update_modpack_mod.warning=Обновление модов в пакете модов может привести к непоправимым результатам, возможно, повредить пакет модов так, что он не сможет запуститься. Вы уверены, что хотите обновить? mods.install=Установить mods.save_as=Сохранить как +mods.show_detail=Подробнее nbt.entries=%s записи nbt.open.failed=Не удалось открыть файл diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 9c66eea457..8667100134 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1006,7 +1006,6 @@ mods.check_updates.empty=Усі моди оновлені mods.check_updates.failed_check=Не вдалося перевірити оновлення. mods.check_updates.failed_download=Не вдалося завантажити деякі файли. mods.check_updates.file=Файл -mods.check_updates.show_detail=Детальніше mods.check_updates.source=Джерело mods.check_updates.target_version=Цільова версія mods.check_updates.update_mod=Оновити мод - %1s @@ -1038,6 +1037,7 @@ mods.url=Офіційна сторінка mods.update_modpack_mod.warning=Оновлення модів у модпаку може призвести до непоправних наслідків, можливо, пошкодивши модпак так, що він не зможе запуститися. Ви впевнені, що хочете оновити? mods.install=Встановити mods.save_as=Зберегти як +mods.show_detail=Детальніше nbt.entries=%s записів nbt.open.failed=Не вдалося відкрити файл diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 189da4c43d..05951845ed 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -865,7 +865,6 @@ mods.check_updates.empty=沒有需要更新的模組 mods.check_updates.failed_check=檢查更新失敗 mods.check_updates.failed_download=部分檔案下載失敗 mods.check_updates.file=檔案 -mods.check_updates.show_detail=顯示詳情 mods.check_updates.source=來源 mods.check_updates.target_version=目標版本 mods.check_updates.update_mod=更新模組 - %1s @@ -898,6 +897,7 @@ mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包 mods.warning.loader_mismatch=模組載入器不匹配 mods.install=安裝到目前實例 mods.save_as=下載到本機目錄 +mods.show_detail=顯示詳情 mods.unknown=未知模組 nbt.entries=%s 個條目 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 2bbfda9571..993a6b6f16 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -875,7 +875,6 @@ mods.check_updates.empty=没有需要更新的模组 mods.check_updates.failed_check=检查更新失败 mods.check_updates.failed_download=部分文件下载失败 mods.check_updates.file=文件 -mods.check_updates.show_detail=显示详情 mods.check_updates.source=来源 mods.check_updates.target_version=目标版本 mods.check_updates.update_mod=更新模组 - %1s @@ -908,6 +907,7 @@ mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包 mods.warning.loader_mismatch=模组加载器不匹配 mods.install=安装到当前实例 mods.save_as=下载到本地文件夹 +mods.show_detail=显示详情 mods.unknown=未知模组 nbt.entries=%s 个条目 From 8ad29d1321d2b978b45881d8ec6432b38a566deb Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 3 Jan 2026 13:44:05 +0800 Subject: [PATCH 19/77] update --- .../src/main/java/org/jackhuang/hmcl/util/StringUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index f1274db16f..b7dd3b7ebf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -539,11 +539,12 @@ public static Optional nullIfBlank(String str) { return Optional.ofNullable(str).map(s -> s.isBlank() ? null : s); } + private static final Parser MD_PARSER = Parser.builder().extensions(List.of(AutolinkExtension.create())).build(); + @Contract(pure = true, value = "null -> null") public static String markdownToHTML(String md) { if (md == null) return null; - var extensions = List.of(AutolinkExtension.create()); - return HtmlRenderer.builder().build().render(Parser.builder().extensions(extensions).build().parse(md)); + return HtmlRenderer.builder().build().render(MD_PARSER.parse(md)); } public static class LevCalculator { From 1885faf8773bda378b9ad7e27271af331cc2297c Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 4 Jan 2026 18:23:30 +0800 Subject: [PATCH 20/77] update --- .../hmcl/ui/versions/DownloadPage.java | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 36fbc24a38..9a5683bb14 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -161,13 +161,13 @@ public void setFailed(boolean failed) { public void download(RemoteMod mod, RemoteMod.Version file) { if (this.callback == null) { - saveAs(mod, file); + saveAs(file); } else { this.callback.download(version.getProfile(), version.getVersion(), mod, file); } } - public void saveAs(RemoteMod mod, RemoteMod.Version file) { + public void saveAs(RemoteMod.Version file) { String extension = StringUtils.substringAfterLast(file.getFile().getFilename(), '.'); FileChooser fileChooser = new FileChooser(); @@ -196,12 +196,12 @@ public ReadOnlyObjectProperty stateProperty() { @Override protected Skin createDefaultSkin() { - return new ModDownloadPageSkin(this); + return new DownloadPageSkin(this); } - private static class ModDownloadPageSkin extends SkinBase { + private static class DownloadPageSkin extends SkinBase { - protected ModDownloadPageSkin(DownloadPage control) { + protected DownloadPageSkin(DownloadPage control) { super(control); VBox pane = new VBox(8); @@ -289,7 +289,7 @@ protected ModDownloadPageSkin(DownloadPage control) { if (targetLoaders.contains(loader)) { list.getContent().addAll( ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)), - new ModItem(control.addon, modVersion, control) + new AddonItem(control.addon, modVersion, control) ); break resolve; } @@ -299,26 +299,24 @@ protected ModDownloadPageSkin(DownloadPage control) { } } - for (String gameVersion : control.versions.keys().stream() + control.versions.keys().stream() .sorted(Collections.reverseOrder(GameVersionNumber::compare)) - .toList()) { - List versions = control.versions.get(gameVersion); - if (versions == null || versions.isEmpty()) { - continue; - } - - ComponentList sublist = new ComponentList(() -> { - ArrayList items = new ArrayList<>(versions.size()); - for (RemoteMod.Version v : versions) { - items.add(new ModItem(control.addon, v, control)); - } - return items; - }); - sublist.getStyleClass().add("no-padding"); - sublist.setTitle("Minecraft " + gameVersion); - - list.getContent().add(sublist); - } + .forEach(gameVersion -> { + List versions = control.versions.get(gameVersion); + if (versions == null || versions.isEmpty()) { + return; + } + ComponentList sublist = new ComponentList(() -> { + ArrayList items = new ArrayList<>(versions.size()); + for (RemoteMod.Version v : versions) { + items.add(new AddonItem(control.addon, v, control)); + } + return items; + }); + sublist.getStyleClass().add("no-padding"); + sublist.setTitle("Minecraft " + gameVersion); + list.getContent().add(sublist); + }); }); } @@ -326,7 +324,7 @@ protected ModDownloadPageSkin(DownloadPage control) { } } - private static final class DependencyModItem extends StackPane { + private static final class DependencyAddonItem extends StackPane { public static final EnumMap I18N_KEY = new EnumMap<>(Lang.mapOf( Pair.pair(RemoteMod.DependencyType.EMBEDDED, "mods.dependency.embedded"), Pair.pair(RemoteMod.DependencyType.OPTIONAL, "mods.dependency.optional"), @@ -337,7 +335,7 @@ private static final class DependencyModItem extends StackPane { Pair.pair(RemoteMod.DependencyType.BROKEN, "mods.dependency.broken") )); - DependencyModItem(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, DownloadCallback callback) { + DependencyAddonItem(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, DownloadCallback callback) { HBox pane = new HBox(8); pane.setPadding(new Insets(0, 8, 0, 8)); pane.setAlignment(Pos.CENTER_LEFT); @@ -373,13 +371,13 @@ private static final class DependencyModItem extends StackPane { } } - private static final class ModItem extends StackPane { + private static final class AddonItem extends StackPane { - ModItem(RemoteMod mod, RemoteMod.Version dataItem) { + AddonItem(RemoteMod mod, RemoteMod.Version dataItem) { this(mod, dataItem, null); } - ModItem(RemoteMod mod, RemoteMod.Version dataItem, DownloadPage selfPage) { + AddonItem(RemoteMod mod, RemoteMod.Version dataItem, DownloadPage selfPage) { VBox pane = new VBox(8); pane.setPadding(new Insets(8, 0, 8, 0)); @@ -442,7 +440,7 @@ private static final class ModItem extends StackPane { RipplerContainer container = new RipplerContainer(pane); if (selfPage != null) { - FXUtils.onClicked(container, () -> Controllers.dialog(new ModVersion(mod, dataItem, selfPage))); + FXUtils.onClicked(container, () -> Controllers.dialog(new AddonVersion(mod, dataItem, selfPage))); } getChildren().setAll(container); @@ -451,10 +449,10 @@ private static final class ModItem extends StackPane { } } - private static final class ModVersion extends JFXDialogLayout { + private static final class AddonVersion extends JFXDialogLayout { private final String title; - public ModVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfPage) { + public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfPage) { RemoteModRepository.Type type = selfPage.repository.getType(); String title = switch (type) { @@ -469,7 +467,7 @@ public ModVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfPag VBox box = new VBox(8); box.setPadding(new Insets(8)); - box.getChildren().setAll(new ModItem(mod, version)); + box.getChildren().setAll(new AddonItem(mod, version)); Button changelogButton = new JFXButton(i18n("mods.show_detail")); changelogButton.getStyleClass().add("dialog-accept"); @@ -506,7 +504,7 @@ public ModVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfPag if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { fireEvent(new DialogCloseEvent()); } - selfPage.saveAs(mod, version); + selfPage.saveAs(version); }); JFXButton cancelButton = new JFXButton(i18n("button.cancel")); @@ -550,13 +548,13 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag if (!dependencies.containsKey(dependency.getType())) { List list = new ArrayList<>(); - Label title = new Label(i18n(DependencyModItem.I18N_KEY.get(dependency.getType()))); + Label title = new Label(i18n(DependencyAddonItem.I18N_KEY.get(dependency.getType()))); title.setPadding(new Insets(0, 8, 0, 8)); list.add(title); dependencies.put(dependency.getType(), list); } - DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback); - dependencies.get(dependency.getType()).add(dependencyModItem); + DependencyAddonItem dependencyAddonItem = new DependencyAddonItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback); + dependencies.get(dependency.getType()).add(dependencyAddonItem); } return new Pair<>(changelog, dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList())); @@ -566,7 +564,7 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag 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))); + changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(AddonVersion.this.title, s))); } else { changelogCache.put(version, null); changelogButton.setOnAction(null); @@ -583,9 +581,9 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag } } - private static final class ModChangelog extends JFXDialogLayout { + private static final class AddonChangelog extends JFXDialogLayout { - public ModChangelog(String title, String changelog) { + public AddonChangelog(String title, String changelog) { setHeading(new HBox(new Label(title))); VBox box = new VBox(8); From 926ca53f1947c9f78fc86f4222e30030c23d664a Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 5 Jan 2026 18:18:23 +0800 Subject: [PATCH 21/77] update i18n --- .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 2 +- .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 2 +- HMCL/src/main/resources/assets/lang/I18N.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_es.properties | 1 - HMCL/src/main/resources/assets/lang/I18N_ja.properties | 1 - HMCL/src/main/resources/assets/lang/I18N_ru.properties | 1 - HMCL/src/main/resources/assets/lang/I18N_uk.properties | 1 - HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 9 files changed, 5 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 9a5683bb14..98f3ae8f75 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -469,7 +469,7 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP box.setPadding(new Insets(8)); box.getChildren().setAll(new AddonItem(mod, version)); - Button changelogButton = new JFXButton(i18n("mods.show_detail")); + Button changelogButton = new JFXButton(i18n("mods.changelog")); changelogButton.getStyleClass().add("dialog-accept"); SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 7a3fb25c9d..21bc53d90c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -109,7 +109,7 @@ public ModUpdatesPage(ModManager modManager, List update }); return cell; }); - detailColumn.setCellValueFactory(it -> new SimpleStringProperty(i18n("mods.show_detail"))); + detailColumn.setCellValueFactory(it -> new SimpleStringProperty(i18n("mods.changelog"))); objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList())); FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new)); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index d778aacab9..9c10253050 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1059,6 +1059,7 @@ mods.add.success=%s was successfully added. mods.broken_dependency.title=Broken dependency mods.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source. mods.category=Category +mods.changelog=Changelog mods.channel.alpha=Alpha mods.channel.beta=Beta mods.channel.release=Release @@ -1102,7 +1103,6 @@ mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparab mods.warning.loader_mismatch=Mod loader mismatch mods.install=Install mods.save_as=Save As -mods.show_detail=Show Detail mods.unknown=Unknown Mod nbt.entries=%s entries diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 8e74282ac6..601aa2caca 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1083,7 +1083,6 @@ mods.url=Página oficial mods.update_modpack_mod.warning=Actualizar mods en un modpack puede generar resultados irreparables, posiblemente corrompiendo el modpack para que no pueda iniciarse. ¿Seguro que quieres actualizar? mods.install=Instalar mods.save_as=Guardar como -mods.show_detail=Ver detalles nbt.entries=%s entradas nbt.open.failed=No se ha podido abrir el archivo diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index db38695d89..980c3c946f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -685,7 +685,6 @@ mods.not_modded=最初にmodloaderをインストールする必要がありま mods.restore=Restore mods.url=公式ページ mods.update_modpack_mod.warning=modpack 内の mod を更新すると、修復不可能な結果につながる可能性があり、modpack が破損して起動できなくなる可能性があります。更新してもよろしいですか? -mods.show_detail=詳細を表示 datapack=Datapacks datapack.add=データパックをインストールします diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 68bc99f27d..99a69317b8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1076,7 +1076,6 @@ mods.url=Официальная страница mods.update_modpack_mod.warning=Обновление модов в пакете модов может привести к непоправимым результатам, возможно, повредить пакет модов так, что он не сможет запуститься. Вы уверены, что хотите обновить? mods.install=Установить mods.save_as=Сохранить как -mods.show_detail=Подробнее nbt.entries=%s записи nbt.open.failed=Не удалось открыть файл diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 4e78fdf1eb..4c88dc73ef 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1020,7 +1020,6 @@ mods.url=Офіційна сторінка mods.update_modpack_mod.warning=Оновлення модів у модпаку може призвести до непоправних наслідків, можливо, пошкодивши модпак так, що він не зможе запуститися. Ви впевнені, що хочете оновити? mods.install=Встановити mods.save_as=Зберегти як -mods.show_detail=Детальніше nbt.entries=%s записів nbt.open.failed=Не вдалося відкрити файл diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 17fa882d39..4bcc60e253 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -847,6 +847,7 @@ mods.add.success=成功新增模組「%s」。 mods.broken_dependency.title=損壞的相依模組 mods.broken_dependency.desc=該相依模組曾經存在於模組倉庫中,但現在已被刪除,請嘗試其他下載源。 mods.category=類別 +mods.changelog=更新日誌 mods.channel.alpha=Alpha mods.channel.beta=Beta mods.channel.release=Release @@ -890,7 +891,6 @@ mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包 mods.warning.loader_mismatch=模組載入器不匹配 mods.install=安裝到目前實例 mods.save_as=下載到本機目錄 -mods.show_detail=顯示詳情 mods.unknown=未知模組 nbt.entries=%s 個條目 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 2b35dd4f10..5a54e0e638 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -851,6 +851,7 @@ mods.add.success=成功添加模组 %s。 mods.broken_dependency.title=损坏的前置模组 mods.broken_dependency.desc=该前置模组曾经在该模组仓库上存在过,但现在被删除了。换个下载源试试吧。 mods.category=类别 +mods.changelog=更新日志 mods.channel.alpha=快照版本 mods.channel.beta=测试版本 mods.channel.release=稳定版本 @@ -894,7 +895,6 @@ mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包 mods.warning.loader_mismatch=模组加载器不匹配 mods.install=安装到当前实例 mods.save_as=下载到本地文件夹 -mods.show_detail=显示详情 mods.unknown=未知模组 nbt.entries=%s 个条目 From 25123f350c424c15cc0ed7de5ab036c02712785f Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 5 Jan 2026 18:28:58 +0800 Subject: [PATCH 22/77] update style --- .../hmcl/ui/versions/ModUpdatesPage.java | 32 +++++++++-------- HMCL/src/main/resources/assets/css/root.css | 36 ++++++++++++------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 21bc53d90c..8e39712cbf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -96,27 +96,31 @@ public ModUpdatesPage(ModManager modManager, List update TableColumn sourceColumn = new TableColumn<>(i18n("mods.check_updates.source")); setupCellValueFactory(sourceColumn, ModUpdateObject::sourceProperty); - TableColumn detailColumn = new TableColumn<>(); - detailColumn.setCellFactory(param -> { - TableCell cell = (TableCell) TableColumn.DEFAULT_CELL_FACTORY.call(param); - cell.setOnMouseClicked(event -> { - List items = cell.getTableColumn().getTableView().getItems(); - if (cell.getIndex() >= items.size()) { - return; - } - ModUpdateObject object = items.get(cell.getIndex()); - Controllers.dialog(new ModDetail(object)); + TableColumn changelogColumn = new TableColumn<>(); + { + var oldCellFactory = changelogColumn.getCellFactory(); + changelogColumn.setCellFactory(param -> { + TableCell cell = oldCellFactory.call(param); + cell.getStyleClass().add("mod-changelog-table-cell"); + cell.setOnMouseClicked(event -> { + List items = cell.getTableColumn().getTableView().getItems(); + if (cell.getIndex() >= items.size()) { + return; + } + ModUpdateObject object = items.get(cell.getIndex()); + Controllers.dialog(new ModDetail(object)); + }); + return cell; }); - return cell; - }); - detailColumn.setCellValueFactory(it -> new SimpleStringProperty(i18n("mods.changelog"))); + } + changelogColumn.setCellValueFactory(__ -> new SimpleStringProperty(i18n("mods.changelog"))); objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList())); FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new)); TableView table = new TableView<>(objects); table.setEditable(true); - table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn, detailColumn); + table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn, changelogColumn); setMargin(table, new Insets(10, 10, 5, 10)); setCenter(table); diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 87c096f666..09e19e02a6 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -1833,6 +1833,27 @@ -fx-font-style: italic; } +/******************************************************************************* + * * + * Tooltip * + * * + ******************************************************************************/ + +.tooltip { + -fx-text-fill: -monet-inverse-on-surface; + -fx-background-color: -monet-inverse-surface; +} + +.tooltip .text { + -fx-fill: -monet-inverse-on-surface; +} + +/******************************************************************************* + * * + * Mod Changelog * + * * + ******************************************************************************/ + .mod-changelog .html { -fx-background-color: -monet-surface; -fx-background-radius: 4; @@ -1853,17 +1874,6 @@ -fx-font-size: 13.5; } -/******************************************************************************* - * * - * Tooltip * - * * - ******************************************************************************/ - -.tooltip { - -fx-text-fill: -monet-inverse-on-surface; - -fx-background-color: -monet-inverse-surface; -} - -.tooltip .text { - -fx-fill: -monet-inverse-on-surface; +.mod-changelog-table-cell { + -fx-text-fill: -monet-primary; } From 5c4c724d1164a86598091ac0da26c455ef69656e Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 12 Jan 2026 22:03:50 +0800 Subject: [PATCH 23/77] add copy link button --- .../java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 14 +++++++++++--- .../src/main/resources/assets/lang/I18N.properties | 1 + .../main/resources/assets/lang/I18N_zh.properties | 1 + .../resources/assets/lang/I18N_zh_CN.properties | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index f3165dd4f4..5d86029a6e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -22,6 +22,7 @@ import javafx.scene.image.ImageView; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.util.StringUtils; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; @@ -53,9 +54,16 @@ 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); + var dialog = + new MessageDialogPane.Builder( + i18n("web.open_in_browser", uri), + i18n("message.confirm"), + MessageDialogPane.MessageType.QUESTION + ) + .addAction(i18n("button.copy"), () -> FXUtils.copyText(uri.toString())) + .yesOrNo(() -> FXUtils.openLink(uri.toString()), null) + .build(); + Controllers.dialog(dialog); }); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a70d87b6eb..b9c2b08989 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -178,6 +178,7 @@ assets.index.malformed=Index files of downloaded assets are corrupted. You can r button.cancel=Cancel button.change_source=Change Download Source button.clear=Clear +button.copy=Copy button.copy_and_exit=Copy and Exit button.delete=Delete button.do_not_show_again=Don't show again diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 48ca31f8ae..246b861fbd 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -173,6 +173,7 @@ assets.index.malformed=資源檔案的索引檔案損壞。你可以在相應實 button.cancel=取消 button.change_source=切換下載源 button.clear=清除 +button.copy=複製 button.copy_and_exit=複製並退出 button.delete=刪除 button.do_not_show_again=不再顯示 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index c79c3660e8..2548dcb28d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -175,6 +175,7 @@ assets.index.malformed=资源文件的索引文件损坏。你可以在相应实 button.cancel=取消 button.change_source=切换下载源 button.clear=清除 +button.copy=复制 button.copy_and_exit=复制并退出 button.delete=删除 button.do_not_show_again=不再显示 From d09c3659e8d351e7c5d254ee8bead3042cd885b6 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 13 Jan 2026 21:32:35 +0800 Subject: [PATCH 24/77] update --- .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 2 +- .../jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java | 1 + .../hmcl/mod/modrinth/ModrinthRemoteModRepository.java | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index cd0b11146a..b71512aa6b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -301,7 +301,7 @@ protected DownloadPageSkin(DownloadPage control) { control.versions.keys().stream() .sorted(Collections.reverseOrder(GameVersionNumber::compare)) - .forEach(gameVersion -> { + .forEachOrdered(gameVersion -> { List versions = control.versions.get(gameVersion); if (versions == null || versions.isEmpty()) { return; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 7d8fbc0954..ccc472dd8f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -36,6 +36,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.Semaphore; import java.util.stream.Stream; import static org.jackhuang.hmcl.util.Lang.mapOf; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index f2e8355139..627bcfc50b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -36,6 +36,7 @@ import java.nio.file.Path; import java.time.Instant; import java.util.*; +import java.util.concurrent.Semaphore; import java.util.stream.Collectors; import java.util.stream.Stream; From ed89893360e96a25700e877540a5e7f5f32c8a8d Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 13 Jan 2026 21:38:40 +0800 Subject: [PATCH 25/77] update --- HMCL/src/main/resources/assets/lang/I18N_lzh.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index efa07f1a6e..d0fbd697a5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -161,6 +161,7 @@ assets.index.malformed=資案之目有謬。或至於是例「司例」之頁, button.cancel=罷 button.change_source=迭引源 button.clear=清 +button.copy=鈔 button.copy_and_exit=鈔而辭 button.delete=刪 button.edit=改 @@ -830,6 +831,7 @@ mods.add.success=增改囊 %s 畢。 mods.broken_dependency.title=所依之壞者 mods.broken_dependency.desc=夫改囊素存於改囊庫,今闕矣,宜易他源。 mods.category=類 +mods.changelog=迭更誌 mods.channel.alpha=預版 mods.channel.beta=試版 mods.channel.release=當版 @@ -841,7 +843,6 @@ mods.check_updates.empty=無改囊可迭更 mods.check_updates.failed_check=檢囊迭更未成 mods.check_updates.failed_download=有引案未成 mods.check_updates.file=案 -mods.check_updates.show_detail=示詳 mods.check_updates.source=源 mods.check_updates.target_version=將至之版 mods.check_updates.update_mod=迭更改囊 - %1s From e51b677462cc82bcf1acb7297b4af160125b0f40 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 17 Jan 2026 21:49:23 +0800 Subject: [PATCH 26/77] update api --- .../hmcl/game/LocalizedRemoteModRepository.java | 4 ++-- .../org/jackhuang/hmcl/ui/versions/DownloadPage.java | 6 +----- .../org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 7 ++----- .../org/jackhuang/hmcl/mod/RemoteModRepository.java | 2 +- .../hmcl/mod/curse/CurseForgeRemoteModRepository.java | 4 ++-- .../hmcl/mod/modrinth/ModrinthRemoteModRepository.java | 10 ++++++++-- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java index fd051d817a..9a7bc9aee8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -130,7 +130,7 @@ public Stream getRemoteVersionsById(String id) throws IOExcep } @Override - public String getModChangelog(String modId, String fileId) throws IOException { - return getBackedRemoteModRepository().getModChangelog(modId, fileId); + public String getModChangelog(String modId, String versionId) throws IOException { + return getBackedRemoteModRepository().getModChangelog(modId, versionId); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index b71512aa6b..a4691b0ce5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -534,11 +534,7 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag } 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(); - } + changelog = StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())); } EnumMap> dependencies = new EnumMap<>(RemoteMod.DependencyType.class); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 8e39712cbf..dbbe1d1cac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -44,6 +44,7 @@ import org.jackhuang.hmcl.util.io.CSVTable; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import java.io.FileNotFoundException; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; @@ -417,11 +418,7 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro if (version.getChangelog() != null) { return StringUtils.nullIfBlank(version.getChangelog()); } - try { - return StringUtils.nullIfBlank(repository.getModChangelog(version.getModid(), version.getVersionId())); - } catch (UnsupportedOperationException e) { - return Optional.empty(); - } + return StringUtils.nullIfBlank(repository.getModChangelog(version.getModid(), version.getVersionId())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { result.map(StringUtils::markdownToHTML).ifPresent(s -> { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index a54600c402..8d1f18e4b8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -96,7 +96,7 @@ SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Null Stream getRemoteVersionsById(String id) throws IOException; - String getModChangelog(String modId, String fileId) throws IOException; + String getModChangelog(String modId, String versionId) throws IOException; Stream getCategories() throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index ccc472dd8f..18a998ed59 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -234,10 +234,10 @@ public Stream getRemoteVersionsById(String id) throws IOExcep } @Override - public String getModChangelog(String modId, String fileId) throws IOException { + public String getModChangelog(String modId, String versionId) throws IOException { SEMAPHORE.acquireUninterruptibly(); try { - Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s/changelog", PREFIX, modId, fileId))) + Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s/changelog", PREFIX, modId, versionId))) .getJson(Response.typeOf(String.class)); return response.getData(); } finally { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 627bcfc50b..e05944b8eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -163,8 +163,14 @@ public Stream getRemoteVersionsById(String id) throws IOExcep } @Override - public String getModChangelog(String modId, String fileId) throws IOException { - throw new UnsupportedOperationException(); + public String getModChangelog(String modId, String versionId) throws IOException { + SEMAPHORE.acquireUninterruptibly(); + try { + ProjectVersion version = HttpRequest.GET(PREFIX + "/v2/version/" + versionId).getJson(ProjectVersion.class); + return version.getChangelog(); + } finally { + SEMAPHORE.release(); + } } @Override From aa7c76166bba5d939c7a4572eef50db944fe2c66 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 17 Jan 2026 22:18:24 +0800 Subject: [PATCH 27/77] update --- .../main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index dbbe1d1cac..4242c75d7c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -44,7 +44,6 @@ import org.jackhuang.hmcl.util.io.CSVTable; import org.jackhuang.hmcl.util.javafx.BindingMapping; -import java.io.FileNotFoundException; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; From 8d9b2df13e7d91c377a9d6af7b4892f66f68f811 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 18 Jan 2026 22:14:19 +0800 Subject: [PATCH 28/77] update --- .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 9becc13300..aee253639a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -372,7 +372,7 @@ private static final class ModDetail extends JFXDialogLayout { public ModDetail(ModUpdateObject object) { this.repository = object.data.getRepository(); - RemoteMod.Version targetVersion = object.data.getCandidates().get(0); + RemoteMod.Version targetVersion = object.data.getCandidate(); String source = object.getSource(); this.setHeading(new HBox(new Label(i18n("mods.check_updates.update_mod", targetVersion.getName())))); @@ -413,7 +413,7 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro if (object.changelog != null) { return Optional.of(object.changelog); } - RemoteMod.Version version = object.data.getCandidates().get(0); + RemoteMod.Version version = object.data.getCandidate(); if (version.getChangelog() != null) { return StringUtils.nullIfBlank(version.getChangelog()); } From c3e038472cc4e81b67967b3cf13a7c5a3ddaa6e6 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 20 Jan 2026 21:54:54 +0800 Subject: [PATCH 29/77] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java | 6 ++++-- .../org/jackhuang/hmcl/ui/versions/DownloadPage.java | 3 +-- .../org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 5 ++--- HMCL/src/main/resources/assets/css/root.css | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 7cf2b72f05..0dedd21a2e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -1602,10 +1602,12 @@ public static JFXPopup.PopupVPosition determineOptimalPopupPosition(Node root, J : JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward } - public static TextFlow renderModChangelog(String changelogHTML) { + public static TextFlow renderAddonChangelog(String changelogHTML) { HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); renderer.appendNode(Jsoup.parse(changelogHTML)); renderer.mergeLineBreaks(); - return renderer.render(); + var textFlow = renderer.render(); + textFlow.getStyleClass().add("addon-changelog"); + return textFlow; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index a4691b0ce5..8bd0800f96 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -588,9 +588,8 @@ public AddonChangelog(String title, String changelog) { SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); - scrollPane.getStyleClass().add("mod-changelog"); scrollPane.setFitToWidth(true); - scrollPane.setContent(FXUtils.renderModChangelog(changelog)); + scrollPane.setContent(FXUtils.renderAddonChangelog(changelog)); spinnerPane.setContent(scrollPane); box.getChildren().add(spinnerPane); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index aee253639a..7cbbd16252 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -101,7 +101,7 @@ public ModUpdatesPage(ModManager modManager, List update var oldCellFactory = changelogColumn.getCellFactory(); changelogColumn.setCellFactory(param -> { TableCell cell = oldCellFactory.call(param); - cell.getStyleClass().add("mod-changelog-table-cell"); + cell.getStyleClass().add("addon-changelog-table-cell"); cell.setOnMouseClicked(event -> { List items = cell.getTableColumn().getTableView().getItems(); if (cell.getIndex() >= items.size()) { @@ -383,7 +383,6 @@ public ModDetail(ModUpdateObject object) { SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); - scrollPane.getStyleClass().add("mod-changelog"); scrollPane.setFitToWidth(true); loadChangelog(object, spinnerPane, scrollPane); @@ -422,7 +421,7 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro if (exception == null) { result.map(StringUtils::markdownToHTML).ifPresent(s -> { object.changelog = s; - scrollPane.setContent(FXUtils.renderModChangelog(s)); + scrollPane.setContent(FXUtils.renderAddonChangelog(s)); }); spinnerPane.setFailedReason(null); } else { diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 09e19e02a6..abc145b5b0 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -1854,7 +1854,7 @@ * * ******************************************************************************/ -.mod-changelog .html { +.addon-changelog { -fx-background-color: -monet-surface; -fx-background-radius: 4; -fx-padding: 10; @@ -1862,18 +1862,18 @@ -fx-text-fill: -monet-on-surface; } -.mod-changelog .html-h1 { +.addon-changelog .html-h1 { -fx-font-size: 16.5; } -.mod-changelog .html-h2 { +.addon-changelog .html-h2 { -fx-font-size: 15; } -.mod-changelog .html-h3 { +.addon-changelog .html-h3 { -fx-font-size: 13.5; } -.mod-changelog-table-cell { +.addon-changelog-table-cell { -fx-text-fill: -monet-primary; } From 4058ec167c4489c62e2da81c7dc0ca6d448d9c43 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 20 Jan 2026 22:27:29 +0800 Subject: [PATCH 30/77] =?UTF-8?q?=E6=8B=86=E5=88=86=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=92=8C=E4=BE=9D=E8=B5=96=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/DownloadPage.java | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 8bd0800f96..1a11280d7b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -475,8 +475,9 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); ComponentList dependenciesList = new ComponentList(Lang::immutableListOf); - loadChangelogAndDependencies(version, selfPage, spinnerPane, dependenciesList, changelogButton); - spinnerPane.setOnFailedAction(e -> loadChangelogAndDependencies(version, selfPage, spinnerPane, dependenciesList, changelogButton)); + loadDependencies(version, selfPage, spinnerPane, dependenciesList); + loadChangelog(version, selfPage, changelogButton); + spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList)); scrollPane.setContent(dependenciesList); scrollPane.setFitToWidth(true); @@ -524,19 +525,9 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP onEscPressed(this, cancelButton::fire); } - private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList, Button changelogButton) { + private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList) { spinnerPane.setLoading(true); - changelogButton.setDisable(true); Task.supplyAsync(() -> { - Optional changelog; - if (changelogCache.containsKey(version)) { - changelog = Optional.ofNullable(changelogCache.get(version)); - } else if (version.getChangelog() != null) { - changelog = StringUtils.nullIfBlank(version.getChangelog()); - } else { - changelog = StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())); - } - EnumMap> dependencies = new EnumMap<>(RemoteMod.DependencyType.class); for (RemoteMod.Dependency dependency : version.getDependencies()) { if (dependency.getType() == RemoteMod.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteMod.DependencyType.BROKEN) { @@ -554,11 +545,33 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag dependencies.get(dependency.getType()).add(dependencyAddonItem); } - return new Pair<>(changelog, dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList())); + return dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - if (result.getKey().isPresent()) { - String s = StringUtils.markdownToHTML(result.getKey().get()); + dependenciesList.getContent().setAll(result); + spinnerPane.setFailedReason(null); + } else { + dependenciesList.getContent().setAll(); + spinnerPane.setFailedReason(i18n("download.failed.refresh")); + } + spinnerPane.setLoading(false); + }).start(); + } + + private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, Button changelogButton) { + changelogButton.setDisable(true); + Task.supplyAsync(() -> { + if (changelogCache.containsKey(version)) { + return Optional.ofNullable(changelogCache.get(version)); + } else if (version.getChangelog() != null) { + return StringUtils.nullIfBlank(version.getChangelog()); + } else { + return StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())); + } + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + if (result.isPresent()) { + String s = StringUtils.markdownToHTML(result.get()); changelogCache.put(version, s); changelogButton.setDisable(false); changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(AddonVersion.this.title, s))); @@ -566,14 +579,9 @@ private void loadChangelogAndDependencies(RemoteMod.Version version, DownloadPag 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(); } } From 2cda56ddef51b0b0a7b9d09e310e9b4eef6e6b8f Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 20 Jan 2026 22:34:18 +0800 Subject: [PATCH 31/77] update --- .../src/main/java/org/jackhuang/hmcl/util/StringUtils.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index b7dd3b7ebf..b60603798d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -539,12 +539,14 @@ public static Optional nullIfBlank(String str) { return Optional.ofNullable(str).map(s -> s.isBlank() ? null : s); } + private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().build(); + private static final Parser MD_PARSER = Parser.builder().extensions(List.of(AutolinkExtension.create())).build(); @Contract(pure = true, value = "null -> null") public static String markdownToHTML(String md) { if (md == null) return null; - return HtmlRenderer.builder().build().render(MD_PARSER.parse(md)); + return HTML_RENDERER.render(MD_PARSER.parse(md)); } public static class LevCalculator { From 49e96fe20b942889dc9c29939b3c3444d33b7cbf Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 20 Jan 2026 22:39:05 +0800 Subject: [PATCH 32/77] update --- .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 7cbbd16252..9c4745e20e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -108,7 +108,7 @@ public ModUpdatesPage(ModManager modManager, List update return; } ModUpdateObject object = items.get(cell.getIndex()); - Controllers.dialog(new ModDetail(object)); + Controllers.dialog(new ModChangelog(object)); }); return cell; }); @@ -366,11 +366,11 @@ private static final class ModItem extends StackPane { } } - private static final class ModDetail extends JFXDialogLayout { + private static final class ModChangelog extends JFXDialogLayout { private final RemoteModRepository repository; - public ModDetail(ModUpdateObject object) { + public ModChangelog(ModUpdateObject object) { this.repository = object.data.getRepository(); RemoteMod.Version targetVersion = object.data.getCandidate(); String source = object.getSource(); From 6e15ca036631b92690103823c19f60e258ab8a11 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 21 Jan 2026 12:34:08 +0800 Subject: [PATCH 33/77] update --- .../hmcl/ui/versions/DownloadPage.java | 34 +++++++++---------- .../hmcl/ui/versions/ModUpdatesPage.java | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 1a11280d7b..2fd319bdaf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -299,24 +299,24 @@ protected DownloadPageSkin(DownloadPage control) { } } - control.versions.keys().stream() + for (String gameVersion : control.versions.keys().stream() .sorted(Collections.reverseOrder(GameVersionNumber::compare)) - .forEachOrdered(gameVersion -> { - List versions = control.versions.get(gameVersion); - if (versions == null || versions.isEmpty()) { - return; - } - ComponentList sublist = new ComponentList(() -> { - ArrayList items = new ArrayList<>(versions.size()); - for (RemoteMod.Version v : versions) { - items.add(new AddonItem(control.addon, v, control)); - } - return items; - }); - sublist.getStyleClass().add("no-padding"); - sublist.setTitle("Minecraft " + gameVersion); - list.getContent().add(sublist); - }); + .toList()) { + List versions = control.versions.get(gameVersion); + if (versions == null || versions.isEmpty()) { + return; + } + ComponentList sublist = new ComponentList(() -> { + ArrayList items = new ArrayList<>(versions.size()); + for (RemoteMod.Version v : versions) { + items.add(new AddonItem(control.addon, v, control)); + } + return items; + }); + sublist.getStyleClass().add("no-padding"); + sublist.setTitle("Minecraft " + gameVersion); + list.getContent().add(sublist); + } }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 9c4745e20e..8783db1452 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -112,8 +112,8 @@ public ModUpdatesPage(ModManager modManager, List update }); return cell; }); + changelogColumn.setCellValueFactory(__ -> new SimpleStringProperty(i18n("mods.changelog"))); } - changelogColumn.setCellValueFactory(__ -> new SimpleStringProperty(i18n("mods.changelog"))); objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList())); FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new)); From d9ed9eb0af28d7db8ac5a663b448748dbfdbca0f Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 21 Jan 2026 12:53:18 +0800 Subject: [PATCH 34/77] update --- .../hmcl/ui/versions/DownloadPage.java | 25 +++--- .../hmcl/ui/versions/ModUpdatesPage.java | 79 +------------------ .../resources/assets/lang/I18N.properties | 1 - .../resources/assets/lang/I18N_es.properties | 1 - .../resources/assets/lang/I18N_ja.properties | 1 - .../resources/assets/lang/I18N_ru.properties | 1 - .../resources/assets/lang/I18N_uk.properties | 1 - .../resources/assets/lang/I18N_zh.properties | 1 - .../assets/lang/I18N_zh_CN.properties | 1 - 9 files changed, 12 insertions(+), 99 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 2fd319bdaf..985b703454 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -304,7 +304,7 @@ protected DownloadPageSkin(DownloadPage control) { .toList()) { List versions = control.versions.get(gameVersion); if (versions == null || versions.isEmpty()) { - return; + continue; } ComponentList sublist = new ComponentList(() -> { ArrayList items = new ArrayList<>(versions.size()); @@ -315,6 +315,7 @@ protected DownloadPageSkin(DownloadPage control) { }); sublist.getStyleClass().add("no-padding"); sublist.setTitle("Minecraft " + gameVersion); + list.getContent().add(sublist); } }); @@ -374,10 +375,6 @@ private static final class DependencyAddonItem extends StackPane { private static final class AddonItem extends StackPane { - AddonItem(RemoteMod mod, RemoteMod.Version dataItem) { - this(mod, dataItem, null); - } - AddonItem(RemoteMod mod, RemoteMod.Version dataItem, DownloadPage selfPage) { VBox pane = new VBox(8); pane.setPadding(new Insets(8, 0, 8, 0)); @@ -440,9 +437,7 @@ private static final class AddonItem extends StackPane { } RipplerContainer container = new RipplerContainer(pane); - if (selfPage != null) { - FXUtils.onClicked(container, () -> Controllers.dialog(new AddonVersion(mod, dataItem, selfPage))); - } + FXUtils.onClicked(container, () -> Controllers.dialog(new AddonVersion(mod, dataItem, selfPage))); getChildren().setAll(container); // Workaround for https://github.com/HMCL-dev/HMCL/issues/2129 @@ -451,7 +446,6 @@ private static final class AddonItem extends StackPane { } private static final class AddonVersion extends JFXDialogLayout { - private final String title; public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfPage) { RemoteModRepository.Type type = selfPage.repository.getType(); @@ -463,12 +457,13 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP case SHADER_PACK -> "shaderpack.download.title"; default -> "mods.download.title"; }; - this.title = i18n(title, version.getName()); - this.setHeading(new HBox(new Label(this.title))); + this.setHeading(new HBox(new Label(I18n.i18n(title, version.getName())))); VBox box = new VBox(8); box.setPadding(new Insets(8)); - box.getChildren().setAll(new AddonItem(mod, version)); + var addonItem = new AddonItem(mod, version, selfPage); + addonItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again + box.getChildren().setAll(addonItem); Button changelogButton = new JFXButton(i18n("mods.changelog")); changelogButton.getStyleClass().add("dialog-accept"); @@ -574,7 +569,7 @@ private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, But String s = StringUtils.markdownToHTML(result.get()); changelogCache.put(version, s); changelogButton.setDisable(false); - changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(AddonVersion.this.title, s))); + changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(version, s))); } else { changelogCache.put(version, null); changelogButton.setOnAction(null); @@ -588,8 +583,8 @@ private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, But private static final class AddonChangelog extends JFXDialogLayout { - public AddonChangelog(String title, String changelog) { - setHeading(new HBox(new Label(title))); + public AddonChangelog(RemoteMod.Version version, String changelog) { + setHeading(new HBox(new Label(i18n("mods.changelog") + " - " + version.getName()))); VBox box = new VBox(8); box.setPadding(new Insets(8)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 8783db1452..ee2207da0c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -27,6 +27,7 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.*; import org.jackhuang.hmcl.mod.*; import org.jackhuang.hmcl.task.FileDownloadTask; @@ -34,13 +35,11 @@ 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.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; -import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.CSVTable; import org.jackhuang.hmcl.util.javafx.BindingMapping; @@ -294,78 +293,6 @@ public void setSource(String source) { } } - private static final class ModItem extends StackPane { - - ModItem(RemoteMod.Version targetVersion, String source) { - VBox pane = new VBox(8); - pane.setPadding(new Insets(8, 0, 8, 0)); - - { - HBox descPane = new HBox(8); - descPane.setPadding(new Insets(0, 8, 0, 8)); - descPane.setAlignment(Pos.CENTER_LEFT); - descPane.setMouseTransparent(true); - - { - StackPane graphicPane = new StackPane(); - TwoLineListItem content = new TwoLineListItem(); - HBox.setHgrow(content, Priority.ALWAYS); - content.setTitle(targetVersion.getVersion()); - content.setSubtitle(I18n.formatDateTime(targetVersion.getDatePublished())); - - switch (targetVersion.getVersionType()) { - case Alpha: - content.addTag(i18n("mods.channel.alpha")); - graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE.createIcon(24)); - break; - case Beta: - content.addTag(i18n("mods.channel.beta")); - graphicPane.getChildren().setAll(SVG.BETA_CIRCLE.createIcon(24)); - break; - case Release: - content.addTag(i18n("mods.channel.release")); - graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE.createIcon(24)); - break; - } - - for (ModLoaderType modLoaderType : targetVersion.getLoaders()) { - switch (modLoaderType) { - case FORGE: - content.addTag(i18n("install.installer.forge")); - break; - case CLEANROOM: - content.addTag(i18n("install.installer.cleanroom")); - break; - case NEO_FORGED: - content.addTag(i18n("install.installer.neoforge")); - break; - case FABRIC: - content.addTag(i18n("install.installer.fabric")); - break; - case LITE_LOADER: - content.addTag(i18n("install.installer.liteloader")); - break; - case QUILT: - content.addTag(i18n("install.installer.quilt")); - break; - } - } - - content.addTag(source); - - descPane.getChildren().setAll(graphicPane, content); - } - - pane.getChildren().add(descPane); - } - - getChildren().setAll(new RipplerContainer(pane)); - - // Workaround for https://github.com/HMCL-dev/HMCL/issues/2129 - this.setMinHeight(50); - } - } - private static final class ModChangelog extends JFXDialogLayout { private final RemoteModRepository repository; @@ -373,13 +300,11 @@ private static final class ModChangelog extends JFXDialogLayout { public ModChangelog(ModUpdateObject object) { this.repository = object.data.getRepository(); RemoteMod.Version targetVersion = object.data.getCandidate(); - String source = object.getSource(); - this.setHeading(new HBox(new Label(i18n("mods.check_updates.update_mod", targetVersion.getName())))); + this.setHeading(new HBox(new Label(i18n("mods.changelog") + " - " + targetVersion.getName()))); VBox box = new VBox(8); box.setPadding(new Insets(8)); - box.getChildren().setAll(new ModItem(targetVersion, source)); SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index f60c3a5c6b..73d6a3aac1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1079,7 +1079,6 @@ mods.check_updates.failed_download=Failed to download some files. mods.check_updates.file=File mods.check_updates.source=Source mods.check_updates.target_version=Target Version -mods.check_updates.update_mod=Update Mod - %1s mods.choose_mod=Choose mod mods.curseforge=CurseForge mods.dependency.embedded=Built-in Dependencies (Already packaged in the mod file by the author. No need to download separately) diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index e0cf4fee8e..9b1ddca4c7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1056,7 +1056,6 @@ mods.check_updates.failed_download=No se han podido descargar algunos de los arc mods.check_updates.file=Archivo mods.check_updates.source=Fuente mods.check_updates.target_version=Versión de destino -mods.check_updates.update_mod=Actualizar mod - %1s mods.choose_mod=Elige un mod mods.curseforge=CurseForge mods.dependency.embedded=Dependencias incorporadas (Already packaged in the mod file by the author. No need to download separately) diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 0d78df40b6..187db50375 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -668,7 +668,6 @@ mods.check_updates.failed_download=一部のファイルのダウンロードに mods.check_updates.file=ファイル mods.check_updates.source=Source mods.check_updates.target_version=Target -mods.check_updates.update_mod=Modを更新- %1s mods.choose_mod=modを選択してください mods.curseforge=CurseForge mods.disable=無効にする diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 1ba67750c8..60098a6ee7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1049,7 +1049,6 @@ mods.check_updates.failed_download=Не удалось скачать некот mods.check_updates.file=Файл mods.check_updates.source=Источник mods.check_updates.target_version=Целевая версия -mods.check_updates.update_mod=Обновить мод - %1s mods.choose_mod=Выберите мод mods.curseforge=CurseForge mods.dependency.embedded=Встроенные зависимости (Уже упакован в файл мода автором. Нет необходимости скачивать отдельно.) diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index e96121909d..978448e0cb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -993,7 +993,6 @@ mods.check_updates.failed_download=Не вдалося завантажити д mods.check_updates.file=Файл mods.check_updates.source=Джерело mods.check_updates.target_version=Цільова версія -mods.check_updates.update_mod=Оновити мод - %1s mods.choose_mod=Вибрати мод mods.curseforge=CurseForge mods.dependency.embedded=Вбудовані залежності (Вже запаковані в файл мода автором. Не потрібно завантажувати окремо) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 11b2917ae9..b3213d149c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -867,7 +867,6 @@ mods.check_updates.failed_download=部分檔案下載失敗 mods.check_updates.file=檔案 mods.check_updates.source=來源 mods.check_updates.target_version=目標版本 -mods.check_updates.update_mod=更新模組 - %1s mods.choose_mod=選取模組 mods.curseforge=CurseForge mods.dependency.embedded=內建相依模組 (作者已經打包在模組檔中,無需單獨下載) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index c075daf239..e682d08662 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -871,7 +871,6 @@ mods.check_updates.failed_download=部分文件下载失败 mods.check_updates.file=文件 mods.check_updates.source=来源 mods.check_updates.target_version=目标版本 -mods.check_updates.update_mod=更新模组 - %1s mods.choose_mod=选择模组 mods.curseforge=CurseForge mods.dependency.embedded=内置的前置模组 (已经由作者打包在模组文件中,无需另外下载) From 6f46bab0578a6cb5b4f711157ce0fb576740c10e Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 21 Jan 2026 12:56:47 +0800 Subject: [PATCH 35/77] update --- .../main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index ee2207da0c..012ade3e84 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -27,7 +27,6 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; -import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.*; import org.jackhuang.hmcl.mod.*; import org.jackhuang.hmcl.task.FileDownloadTask; From bf401569cdac284e301cd3eced8b602067e230bc Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 22 Jan 2026 23:03:10 +0800 Subject: [PATCH 36/77] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8D=A0=E4=BD=8D?= =?UTF-8?q?=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 6 ++---- HMCL/src/main/resources/assets/lang/I18N.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_zh.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 012ade3e84..906dc56cce 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -343,10 +343,8 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro return StringUtils.nullIfBlank(repository.getModChangelog(version.getModid(), version.getVersionId())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - result.map(StringUtils::markdownToHTML).ifPresent(s -> { - object.changelog = s; - scrollPane.setContent(FXUtils.renderAddonChangelog(s)); - }); + object.changelog = result.map(StringUtils::markdownToHTML).orElse(i18n("mods.changelog.empty")); + scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog)); spinnerPane.setFailedReason(null); } else { spinnerPane.setFailedReason(i18n("download.failed.refresh")); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 73d6a3aac1..4dbdb7c79b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1066,6 +1066,7 @@ mods.broken_dependency.title=Broken dependency mods.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source. mods.category=Category mods.changelog=Changelog +mods.changelog.empty=Currently no changelog mods.channel.alpha=Alpha mods.channel.beta=Beta mods.channel.release=Release diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index b3213d149c..91d77778d0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -854,6 +854,7 @@ mods.broken_dependency.title=損壞的相依模組 mods.broken_dependency.desc=該相依模組曾經存在於模組倉庫中,但現在已被刪除,請嘗試其他下載源。 mods.category=類別 mods.changelog=更新日誌 +mods.changelog.empty=暫無更新日誌 mods.channel.alpha=Alpha mods.channel.beta=Beta mods.channel.release=Release diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index e682d08662..c5b1b0e9c4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -858,6 +858,7 @@ mods.broken_dependency.title=损坏的前置模组 mods.broken_dependency.desc=该前置模组曾经在该模组仓库上存在过,但现在被删除了。换个下载源试试吧。 mods.category=类别 mods.changelog=更新日志 +mods.changelog.empty=暂无更新日志 mods.channel.alpha=快照版本 mods.channel.beta=测试版本 mods.channel.release=稳定版本 From 4ceeda368cca52053c2693b5188c0369ea00c9bb Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 25 Jan 2026 18:13:16 +0800 Subject: [PATCH 37/77] update --- .../java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 6 +++--- .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 985b703454..20128bf55c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -559,14 +559,14 @@ private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, But if (changelogCache.containsKey(version)) { return Optional.ofNullable(changelogCache.get(version)); } else if (version.getChangelog() != null) { - return StringUtils.nullIfBlank(version.getChangelog()); + return StringUtils.nullIfBlank(version.getChangelog()).map(StringUtils::markdownToHTML); } else { - return StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())); + return StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::markdownToHTML); } }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { if (result.isPresent()) { - String s = StringUtils.markdownToHTML(result.get()); + String s = result.get(); changelogCache.put(version, s); changelogButton.setDisable(false); changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(version, s))); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 906dc56cce..17d8a164d6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -338,12 +338,12 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro } RemoteMod.Version version = object.data.getCandidate(); if (version.getChangelog() != null) { - return StringUtils.nullIfBlank(version.getChangelog()); + return StringUtils.nullIfBlank(version.getChangelog()).map(StringUtils::markdownToHTML); } - return StringUtils.nullIfBlank(repository.getModChangelog(version.getModid(), version.getVersionId())); + return StringUtils.nullIfBlank(repository.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::markdownToHTML); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - object.changelog = result.map(StringUtils::markdownToHTML).orElse(i18n("mods.changelog.empty")); + object.changelog = result.orElse(i18n("mods.changelog.empty")); scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog)); spinnerPane.setFailedReason(null); } else { From 13620972bcc6f2776be1ffe6c8e713b1db953f9f Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 25 Jan 2026 18:40:18 +0800 Subject: [PATCH 38/77] fix image width --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 5d86029a6e..edb6574888 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -322,6 +322,13 @@ public TextFlow render() { TextFlow textFlow = new TextFlow(); textFlow.getStyleClass().add("html"); textFlow.getChildren().setAll(children); + for (javafx.scene.Node node : children) { + if (node instanceof ImageView img) { + double width = img.getImage().getWidth(); + img.setPreserveRatio(true); + img.fitWidthProperty().bind(textFlow.widthProperty().map(d -> Math.min((double) d - 20D, width))); + } + } return textFlow; } From eec92602290a432519826c203302d10dc4cdd893 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 25 Jan 2026 20:20:23 +0800 Subject: [PATCH 39/77] fix JEI html changelog --- .../org/jackhuang/hmcl/ui/versions/DownloadPage.java | 4 ++-- .../jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 4 ++-- .../java/org/jackhuang/hmcl/util/StringUtils.java | 11 ++++++++++- gradle/libs.versions.toml | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 20128bf55c..15fae839ae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -559,9 +559,9 @@ private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, But if (changelogCache.containsKey(version)) { return Optional.ofNullable(changelogCache.get(version)); } else if (version.getChangelog() != null) { - return StringUtils.nullIfBlank(version.getChangelog()).map(StringUtils::markdownToHTML); + return StringUtils.nullIfBlank(version.getChangelog()).map(StringUtils::convertToHtml); } else { - return StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::markdownToHTML); + return StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::convertToHtml); } }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 17d8a164d6..0b8a08603e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -338,9 +338,9 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro } RemoteMod.Version version = object.data.getCandidate(); if (version.getChangelog() != null) { - return StringUtils.nullIfBlank(version.getChangelog()).map(StringUtils::markdownToHTML); + return StringUtils.nullIfBlank(version.getChangelog()).map(StringUtils::convertToHtml); } - return StringUtils.nullIfBlank(repository.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::markdownToHTML); + return StringUtils.nullIfBlank(repository.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::convertToHtml); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { object.changelog = result.orElse(i18n("mods.changelog.empty")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index b60603798d..ec49bd5128 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -21,6 +21,8 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.jetbrains.annotations.Contract; +import org.jsoup.Jsoup; +import org.jsoup.safety.Safelist; import java.io.PrintWriter; import java.io.StringWriter; @@ -539,13 +541,20 @@ public static Optional nullIfBlank(String str) { return Optional.ofNullable(str).map(s -> s.isBlank() ? null : s); } + public static boolean isStringHtml(String str) { + if (isBlank(str)) return false; + if (str.startsWith("") || str.startsWith("") || str.startsWith("")) return true; + return Jsoup.isValid(str, Safelist.relaxed().addAttributes("a", "rel")); + } + private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().build(); private static final Parser MD_PARSER = Parser.builder().extensions(List.of(AutolinkExtension.create())).build(); @Contract(pure = true, value = "null -> null") - public static String markdownToHTML(String md) { + public static String convertToHtml(String md) { if (md == null) return null; + if (isStringHtml(md)) return md; return HTML_RENDERER.render(MD_PARSER.parse(md)); } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3e9f31aed..0bf789174b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ java-info = "1.0" authlib-injector = "1.2.7" monet-fx = "0.4.0" terracotta = "0.4.1" -commonmark = "0.27.0" +commonmark = "0.27.1" # testing junit = "6.0.1" From c0aaf33302fa6a2cf16f0d29462b3846af610e90 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 25 Jan 2026 21:13:03 +0800 Subject: [PATCH 40/77] fix quark warning color and size --- .../java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index edb6574888..4749837fdd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -20,10 +20,12 @@ import javafx.scene.Cursor; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.paint.Color; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.util.StringUtils; +import org.jsoup.Jsoup; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; @@ -77,6 +79,7 @@ public static HTMLRenderer openHyperlinkInBrowser() { private boolean highlight; private String headerLevel; private Node hyperlink; + private String style; private final Consumer onClickHyperlink; @@ -92,6 +95,7 @@ private void updateStyle() { highlight = false; headerLevel = null; hyperlink = null; + style = null; for (Node node : stack) { String nodeName = node.nodeName(); @@ -125,6 +129,13 @@ private void updateStyle() { headerLevel = nodeName; break; } + + String style = node.attr("style"); + if (StringUtils.isNotBlank(style)) { + this.style = style + .replace("color:", "-fx-fill:") + .replace("font-size:", "-fx-font-size:"); + } } } @@ -162,6 +173,10 @@ private void applyStyle(Text text) { if (headerLevel != null) text.getStyleClass().add("html-" + headerLevel); + + if (style != null) { + text.setStyle(style); + } } private void appendText(String text) { From d1f5585a34529552936c6413d07265cb33122da2 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 25 Jan 2026 21:27:35 +0800 Subject: [PATCH 41/77] update --- .../src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 2 -- .../main/java/org/jackhuang/hmcl/util/StringUtils.java | 8 +++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 4749837fdd..d9156cb225 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -20,12 +20,10 @@ import javafx.scene.Cursor; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.paint.Color; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.util.StringUtils; -import org.jsoup.Jsoup; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index ec49bd5128..fc6e03777e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -544,7 +544,13 @@ public static Optional nullIfBlank(String str) { public static boolean isStringHtml(String str) { if (isBlank(str)) return false; if (str.startsWith("") || str.startsWith("") || str.startsWith("")) return true; - return Jsoup.isValid(str, Safelist.relaxed().addAttributes("a", "rel")); + if (!Jsoup.isValid(str, Safelist.relaxed().addAttributes("a", "rel"))) { + return false; + } + var body = Jsoup.parse(str).body(); + if (body.childNodes().size() > 1) return true; + if (body.childNodes().isEmpty()) return false; + return !body.childNodes().get(0).nameIs("#text"); } private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().build(); From c06247efa467267ee7b2016730c268bcfbc48f58 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 26 Jan 2026 17:33:10 +0800 Subject: [PATCH 42/77] code block --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 69 +++++++++---------- HMCL/src/main/resources/assets/css/root.css | 14 ++++ 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index d9156cb225..806dfbf9b4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -17,12 +17,15 @@ */ package org.jackhuang.hmcl.ui; +import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; @@ -32,6 +35,7 @@ import java.util.List; import java.util.function.Consumer; +import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -75,9 +79,10 @@ public static HTMLRenderer openHyperlinkInBrowser() { private boolean underline; private boolean strike; private boolean highlight; + private boolean code; private String headerLevel; private Node hyperlink; - private String style; + private String fxStyle; private final Consumer onClickHyperlink; @@ -91,48 +96,29 @@ private void updateStyle() { underline = false; strike = false; highlight = false; + code = false; headerLevel = null; hyperlink = null; - style = null; + fxStyle = null; for (Node node : stack) { String nodeName = node.nodeName(); switch (nodeName) { - case "b": - case "strong": - bold = true; - break; - case "i": - case "em": - italic = true; - break; - case "ins": - underline = true; - break; - case "del": - strike = true; - break; - case "mark": - highlight = true; - break; - case "a": - hyperlink = node; - break; - case "h1": - case "h2": - case "h3": - case "h4": - case "h5": - case "h6": - headerLevel = nodeName; - break; + case "b", "strong" -> bold = true; + case "i", "em" -> italic = true; + case "ins" -> underline = true; + case "del" -> strike = true; + case "mark" -> highlight = true; + case "code" -> code = true; + case "a" -> hyperlink = node; + case "h1", "h2", "h3", "h4", "h5", "h6" -> headerLevel = nodeName; } String style = node.attr("style"); if (StringUtils.isNotBlank(style)) { - this.style = style + fxStyle = style .replace("color:", "-fx-fill:") - .replace("font-size:", "-fx-font-size:"); + .replace("font-size:", "-fx-font-size:"); // And more } } } @@ -169,18 +155,29 @@ private void applyStyle(Text text) { if (italic) text.getStyleClass().add("html-italic"); + if (code) { + text.getStyleClass().add("html-code"); + text.setStyle("-fx-font-family: \"%s\";".formatted(Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT))); + } + if (headerLevel != null) text.getStyleClass().add("html-" + headerLevel); - if (style != null) { - text.setStyle(style); - } + if (fxStyle != null) + text.setStyle(fxStyle); } private void appendText(String text) { Text textNode = new Text(text); applyStyle(textNode); - children.add(textNode); + if (code) { + var block = new VBox(textNode); + block.setAlignment(Pos.CENTER); + block.getStyleClass().add("html-code-block"); + children.add(block); + } else { + children.add(textNode); + } } private void appendAutoLineBreak(String text) { diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 9386994d4a..f86bcb4507 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -1836,6 +1836,10 @@ -fx-font-style: italic; } +.html-code { + -fx-font-size: 15; +} + /******************************************************************************* * * * Tooltip * @@ -1877,6 +1881,16 @@ -fx-font-size: 13.5; } +.addon-changelog .html-code { + -fx-font-size: 11.25; +} + +.addon-changelog .html-code-block { + -fx-padding: 0 3 0 3; + -fx-background-radius: 5; + -fx-background-color: -monet-surface-dim; +} + .addon-changelog-table-cell { -fx-text-fill: -monet-primary; } From c7b9e20a9e0f0951eda998b9aed4903c641ab2e4 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 26 Jan 2026 17:52:39 +0800 Subject: [PATCH 43/77] base uri --- .../hmcl/game/LocalizedRemoteModRepository.java | 10 ++++++++++ .../src/main/java/org/jackhuang/hmcl/ui/FXUtils.java | 4 ++-- .../org/jackhuang/hmcl/ui/versions/DownloadPage.java | 6 +++--- .../jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 2 +- .../org/jackhuang/hmcl/mod/RemoteModRepository.java | 4 ++++ .../mod/curse/CurseForgeRemoteModRepository.java | 11 +++++++++++ .../mod/modrinth/ModrinthRemoteModRepository.java | 12 ++++++++++++ 7 files changed, 43 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java index 9a7bc9aee8..a5a5cdd793 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -41,6 +41,16 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor protected abstract SortType getBackedRemoteModRepositorySortOrder(); + @Override + public String getApiBaseUrl() { + return getBackedRemoteModRepository().getApiBaseUrl(); + } + + @Override + public String getBaseUrl() { + return getBackedRemoteModRepository().getBaseUrl(); + } + @Override public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { if (!StringUtils.containsChinese(searchFilter)) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 275b0bc132..118c0eb674 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -1659,9 +1659,9 @@ public static void useJFXContextMenu(TextInputControl control) { }); } - public static TextFlow renderAddonChangelog(String changelogHTML) { + public static TextFlow renderAddonChangelog(String changelogHtml, String baseUri) { HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); - renderer.appendNode(Jsoup.parse(changelogHTML)); + renderer.appendNode(Jsoup.parse(changelogHtml, baseUri)); renderer.mergeLineBreaks(); var textFlow = renderer.render(); textFlow.getStyleClass().add("addon-changelog"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 15fae839ae..1183495c87 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -569,7 +569,7 @@ private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, But String s = result.get(); changelogCache.put(version, s); changelogButton.setDisable(false); - changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(version, s))); + changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(version, s, selfPage.repository))); } else { changelogCache.put(version, null); changelogButton.setOnAction(null); @@ -583,7 +583,7 @@ private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, But private static final class AddonChangelog extends JFXDialogLayout { - public AddonChangelog(RemoteMod.Version version, String changelog) { + public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepository repo) { setHeading(new HBox(new Label(i18n("mods.changelog") + " - " + version.getName()))); VBox box = new VBox(8); @@ -592,7 +592,7 @@ public AddonChangelog(RemoteMod.Version version, String changelog) { SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToWidth(true); - scrollPane.setContent(FXUtils.renderAddonChangelog(changelog)); + scrollPane.setContent(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); spinnerPane.setContent(scrollPane); box.getChildren().add(spinnerPane); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 0b8a08603e..9924fc4fde 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -344,7 +344,7 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { object.changelog = result.orElse(i18n("mods.changelog.empty")); - scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog)); + scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, object.data.getRepository().getBaseUrl())); spinnerPane.setFailedReason(null); } else { spinnerPane.setFailedReason(i18n("download.failed.refresh")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index 8d1f18e4b8..6f267d15a2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -39,6 +39,10 @@ enum Type { Type getType(); + String getApiBaseUrl(); + + String getBaseUrl(); + enum SortType { POPULARITY, NAME, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 39fef7539f..55a901ee1c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -46,6 +46,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository { private static final String PREFIX = "https://api.curseforge.com"; + private static final String BASE = "https://www.curseforge.com"; private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", "")); private static final Semaphore SEMAPHORE = new Semaphore(16); @@ -75,6 +76,16 @@ public Type getType() { return type; } + @Override + public String getApiBaseUrl() { + return PREFIX; + } + + @Override + public String getBaseUrl() { + return BASE; + } + private int toModsSearchSortField(SortType sort) { // https://docs.curseforge.com/#tocS_ModsSearchSortField switch (sort) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 309c193637..b1111e6a92 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -54,6 +54,8 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { private static final String PREFIX = "https://api.modrinth.com"; + private static final String BASE = "https://modrinth.com"; + private final String projectType; private ModrinthRemoteModRepository(String projectType) { @@ -65,6 +67,16 @@ public Type getType() { return Type.MOD; } + @Override + public String getApiBaseUrl() { + return PREFIX; + } + + @Override + public String getBaseUrl() { + return BASE; + } + private static String convertSortType(SortType sortType) { switch (sortType) { case DATE_CREATED: From 0fd002a818ff51d4c67808c05093e82817ff3c03 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 26 Jan 2026 20:30:06 +0800 Subject: [PATCH 44/77] table --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 95 ++++++++++++------- HMCLCore/build.gradle.kts | 3 + .../org/jackhuang/hmcl/util/StringUtils.java | 11 ++- gradle/libs.versions.toml | 3 + 4 files changed, 78 insertions(+), 34 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 806dfbf9b4..54179f3deb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -17,8 +17,12 @@ */ package org.jackhuang.hmcl.ui; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; import javafx.geometry.Pos; import javafx.scene.Cursor; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; @@ -27,11 +31,13 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; +import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Consumer; @@ -224,6 +230,7 @@ private void appendImage(Node node) { imageView.setCursor(Cursor.HAND); } } + imageView.setPreserveRatio(true); children.add(imageView); return; } catch (Throwable e) { @@ -235,6 +242,48 @@ private void appendImage(Node node) { appendText(alt); } + private void appendTable(Node table) { + var childNodes = table.childNodes(); + + var headOptional = childNodes.stream().filter(n -> n.nameIs("thead")).findFirst(); + if (headOptional.isEmpty()) return; + var head = (Element) headOptional.get(); + String[] headRow = head.getAllElements().stream() + .filter(n -> n.nameIs("th")) + .map(Element::text) + .toArray(String[]::new); + if (headRow.length == 0) return; + + var bodyOptional = childNodes.stream().filter(n -> n.nameIs("tbody")).findFirst(); + String[][] bodyRows; + if (bodyOptional.isEmpty()) { + bodyRows = new String[0][headRow.length]; + } else { + var body = (Element) bodyOptional.get(); + var r = body.getAllElements().stream() + .filter(n -> n.nameIs("tr")) + .map(n -> n.getAllElements().stream() + .filter(e -> e.nameIs("td")) + .map(Element::text) + .toArray(String[]::new)) + .toList(); + bodyRows = new String[r.size()][headRow.length]; + for (int i = 0; i < r.size(); i++) { + bodyRows[i] = r.get(i); + } + } + + TableView tableView = new TableView<>(FXCollections.observableList(Arrays.asList(bodyRows))); + for (int i = 0; i < headRow.length; i++) { + int finalI = i; + TableColumn c = new TableColumn<>(headRow[i]); + c.setCellValueFactory(param -> new SimpleStringProperty(param.getValue()[finalI])); + tableView.getColumns().add(c); + } + + children.add(tableView); + } + public void appendNode(Node node) { if (node instanceof TextNode) { appendText(((TextNode) node).text()); @@ -242,48 +291,29 @@ public void appendNode(Node node) { String name = node.nodeName(); switch (name) { - case "img": - appendImage(node); - break; - case "li": - appendText("\n \u2022 "); - break; - case "dt": - appendText(" "); - break; - case "p": - case "h1": - case "h2": - case "h3": - case "h4": - case "h5": - case "h6": - case "tr": + case "img" -> appendImage(node); + case "li" -> appendText("\n \u2022 "); + case "dt" -> appendText(" "); + case "p", "h1", "h2", "h3", "h4", "h5", "h6", "tr" -> { if (!children.isEmpty()) appendAutoLineBreak("\n\n"); - break; + } } if (node.childNodeSize() > 0) { pushNode(node); - for (Node childNode : node.childNodes()) { - appendNode(childNode); + if ("table".equals(name)) { + appendTable(node); + } else { + for (Node childNode : node.childNodes()) { + appendNode(childNode); + } } popNode(); } switch (name) { - case "br": - case "dd": - case "p": - case "h1": - case "h2": - case "h3": - case "h4": - case "h5": - case "h6": - appendAutoLineBreak("\n"); - break; + case "br", "dd", "p", "h1", "h2", "h3", "h4", "h5", "h6" -> appendAutoLineBreak("\n"); } } @@ -335,8 +365,9 @@ public TextFlow render() { for (javafx.scene.Node node : children) { if (node instanceof ImageView img) { double width = img.getImage().getWidth(); - img.setPreserveRatio(true); img.fitWidthProperty().bind(textFlow.widthProperty().map(d -> Math.min((double) d - 20D, width))); + } else if (node instanceof TableView table) { + table.prefWidthProperty().bind(textFlow.widthProperty().add(-20D)); } } return textFlow; diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index 659865d1b6..700cd4c328 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -28,6 +28,9 @@ dependencies { api(libs.pci.ids) api(libs.commonmark) api(libs.commonmark.autolink) + api(libs.commonmark.underline) + api(libs.commonmark.stikethrough) + api(libs.commonmark.table) compileOnlyApi(libs.jetbrains.annotations) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index fc6e03777e..efc66a16de 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -18,6 +18,9 @@ package org.jackhuang.hmcl.util; import org.commonmark.ext.autolink.AutolinkExtension; +import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; +import org.commonmark.ext.gfm.tables.TablesExtension; +import org.commonmark.ext.ins.InsExtension; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.jetbrains.annotations.Contract; @@ -553,9 +556,13 @@ public static boolean isStringHtml(String str) { return !body.childNodes().get(0).nameIs("#text"); } - private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().build(); + private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().extensions(List.of( + InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create() + )).build(); - private static final Parser MD_PARSER = Parser.builder().extensions(List.of(AutolinkExtension.create())).build(); + private static final Parser MD_PARSER = Parser.builder().extensions(List.of( + AutolinkExtension.create(), InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create() + )).build(); @Contract(pure = true, value = "null -> null") public static String convertToHtml(String md) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0bf789174b..cda6d0157a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,6 +52,9 @@ authlib-injector = { module = "org.glavo.hmcl:authlib-injector", version.ref = " monet-fx = { module = "org.glavo:MonetFX", version.ref = "monet-fx" } commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" } commonmark-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" } +commonmark-underline = { module = "org.commonmark:commonmark-ext-ins", version.ref = "commonmark" } +commonmark-stikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" } +commonmark-table = { module = "org.commonmark:commonmark-ext-gfm-tables", version.ref = "commonmark" } # testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } From a0de404960457a2727d070b108ea3c34044d6634 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 26 Jan 2026 20:55:03 +0800 Subject: [PATCH 45/77] fix space --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 20 ++++++++++++--- .../org/jackhuang/hmcl/util/StringUtils.java | 25 +++++++++++++++---- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 54179f3deb..4bacf603b3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -286,13 +286,27 @@ private void appendTable(Node table) { public void appendNode(Node node) { if (node instanceof TextNode) { - appendText(((TextNode) node).text()); + appendText(StringUtils.removeEmptyLinesAtBeginningAndEnd(((TextNode) node).getWholeText())); } String name = node.nodeName(); switch (name) { - case "img" -> appendImage(node); - case "li" -> appendText("\n \u2022 "); + case "img" -> { + if (!children.isEmpty()) + appendAutoLineBreak("\n"); + appendImage(node); + appendAutoLineBreak("\n"); + } + case "li" -> { + int i = 0; + var n = (Element) node; + while (true) { + n = n.parent(); + if (n == null) break; + if (n.nameIs("li")) i++; + } + appendText("\n " + " ".repeat(Math.max(0, i)) + "\u2022 "); + } case "dt" -> appendText(" "); case "p", "h1", "h2", "h3", "h4", "h5", "h6", "tr" -> { if (!children.isEmpty()) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index efc66a16de..4bca951071 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -32,6 +32,7 @@ import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * @author huangyuhui @@ -504,11 +505,7 @@ public static String parseEscapeSequence(String str) { } public static String repeats(char ch, int repeat) { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < repeat; i++) { - result.append(ch); - } - return result.toString(); + return String.valueOf(ch).repeat(Math.max(0, repeat)); } public static String truncate(String str, int limit) { @@ -544,6 +541,24 @@ public static Optional nullIfBlank(String str) { return Optional.ofNullable(str).map(s -> s.isBlank() ? null : s); } + public static String removeEmptyLinesAtBeginningAndEnd(String str) { + if (str == null) return null; + var lines = str.lines().toList(); + int i = 0; + for (; i < lines.size(); i++) { + if (isNotBlank(lines.get(i))) break; + } + int j = lines.size() - 1; + for (; j > 0; j--) { + if (isNotBlank(lines.get(j))) break; + } + j = Math.min(j + 1, lines.size() - 1); + if (i > j) { + return ""; + } + return lines.subList(i, j + 1).stream().collect(Collectors.joining(System.lineSeparator())); + } + public static boolean isStringHtml(String str) { if (isBlank(str)) return false; if (str.startsWith("") || str.startsWith("") || str.startsWith("")) return true; From 96dfcf92987458ee81fbc6141fc3b8025b48bf6d Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 26 Jan 2026 21:09:28 +0800 Subject: [PATCH 46/77] update --- .../main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 4bacf603b3..effc835fd8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -299,11 +299,12 @@ public void appendNode(Node node) { } case "li" -> { int i = 0; - var n = (Element) node; - while (true) { - n = n.parent(); - if (n == null) break; - if (n.nameIs("li")) i++; + if (node instanceof Element n) { + while (true) { + n = n.parent(); + if (n == null) break; + if (n.nameIs("li")) i++; + } } appendText("\n " + " ".repeat(Math.max(0, i)) + "\u2022 "); } From 81c117422a84934ba74434cf525880cb01bba4ca Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 26 Jan 2026 21:16:51 +0800 Subject: [PATCH 47/77] fix empty line --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index effc835fd8..aff7013272 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -299,17 +299,21 @@ public void appendNode(Node node) { } case "li" -> { int i = 0; - if (node instanceof Element n) { - while (true) { - n = n.parent(); - if (n == null) break; - if (n.nameIs("li")) i++; - } + var n = node; + while (true) { + n = n.parent(); + if (n == null) break; + if (n.nameIs("li")) i++; } appendText("\n " + " ".repeat(Math.max(0, i)) + "\u2022 "); } case "dt" -> appendText(" "); - case "p", "h1", "h2", "h3", "h4", "h5", "h6", "tr" -> { + case "p" -> { + var n = node.parent(); + if (!children.isEmpty() && (n == null || !n.nameIs("li"))) + appendAutoLineBreak("\n\n"); + } + case "h1", "h2", "h3", "h4", "h5", "h6" -> { if (!children.isEmpty()) appendAutoLineBreak("\n\n"); } @@ -328,7 +332,12 @@ public void appendNode(Node node) { } switch (name) { - case "br", "dd", "p", "h1", "h2", "h3", "h4", "h5", "h6" -> appendAutoLineBreak("\n"); + case "br", "dd", "h1", "h2", "h3", "h4", "h5", "h6" -> appendAutoLineBreak("\n"); + case "p" -> { + var n = node.parent(); + if (!children.isEmpty() && (n == null || !n.nameIs("li"))) + appendAutoLineBreak("\n"); + } } } From 2c7ccb12af943adfd350ed6ef3fe932d84288623 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 26 Jan 2026 22:10:54 +0800 Subject: [PATCH 48/77] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index aff7013272..aaa0937e4a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -335,7 +335,7 @@ public void appendNode(Node node) { case "br", "dd", "h1", "h2", "h3", "h4", "h5", "h6" -> appendAutoLineBreak("\n"); case "p" -> { var n = node.parent(); - if (!children.isEmpty() && (n == null || !n.nameIs("li"))) + if (n == null || !n.nameIs("li")) appendAutoLineBreak("\n"); } } From 92ece4dcd6324e283d8af032f0ab1f5f76196db2 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 26 Jan 2026 22:37:58 +0800 Subject: [PATCH 49/77] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index aaa0937e4a..528956ee6d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -285,8 +285,8 @@ private void appendTable(Node table) { } public void appendNode(Node node) { - if (node instanceof TextNode) { - appendText(StringUtils.removeEmptyLinesAtBeginningAndEnd(((TextNode) node).getWholeText())); + if (node instanceof TextNode n) { + appendText(StringUtils.removeEmptyLinesAtBeginningAndEnd(n.getWholeText())); } String name = node.nodeName(); From 21cd80ec1135ca2c4bae8868039276133c0dcece Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 27 Jan 2026 17:43:21 +0800 Subject: [PATCH 50/77] fix table space --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 528956ee6d..d7317638ea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -254,6 +254,7 @@ private void appendTable(Node table) { .toArray(String[]::new); if (headRow.length == 0) return; + appendAutoLineBreak("\n"); var bodyOptional = childNodes.stream().filter(n -> n.nameIs("tbody")).findFirst(); String[][] bodyRows; if (bodyOptional.isEmpty()) { From d1e1be29ff743bb604d90006e8e0777639479460 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 27 Jan 2026 20:03:09 +0800 Subject: [PATCH 51/77] add link --- .../game/LocalizedRemoteModRepository.java | 5 ++++ .../java/org/jackhuang/hmcl/ui/FXUtils.java | 19 +++++++++++++ .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 19 ++++--------- .../hmcl/ui/versions/DownloadPage.java | 10 +++++++ .../hmcl/mod/RemoteModRepository.java | 2 ++ .../curse/CurseForgeRemoteModRepository.java | 28 +++++++++++++++++++ .../modrinth/ModrinthRemoteModRepository.java | 5 ++++ .../org/jackhuang/hmcl/util/StringUtils.java | 2 +- 8 files changed, 76 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java index a5a5cdd793..bf8a3fd827 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -143,4 +143,9 @@ public Stream getRemoteVersionsById(String id) throws IOExcep public String getModChangelog(String modId, String versionId) throws IOException { return getBackedRemoteModRepository().getModChangelog(modId, versionId); } + + @Override + public String getVersionPageUrl(RemoteMod.Version version) throws IOException { + return getBackedRemoteModRepository().getVersionPageUrl(version); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 118c0eb674..a284bf7007 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -70,6 +70,7 @@ import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.construct.IconedMenuItem; import org.jackhuang.hmcl.ui.construct.MenuSeparator; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.PopupMenu; import org.jackhuang.hmcl.ui.image.ImageLoader; import org.jackhuang.hmcl.ui.image.ImageUtils; @@ -1667,4 +1668,22 @@ public static TextFlow renderAddonChangelog(String changelogHtml, String baseUri textFlow.getStyleClass().add("addon-changelog"); return textFlow; } + + public static void openUriInBrowser(URI uri) { + if (uri == null) return; + openUriInBrowser(uri.toString()); + } + + public static void openUriInBrowser(String uri) { + if (uri == null) return; + var dialog = new MessageDialogPane.Builder( + i18n("web.open_in_browser", uri), + i18n("message.confirm"), + MessageDialogPane.MessageType.QUESTION + ) + .addAction(i18n("button.copy"), () -> copyText(uri)) + .yesOrNo(() -> openLink(uri), null) + .build(); + Controllers.dialog(dialog); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index d7317638ea..ec8eb26417 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -63,18 +63,7 @@ private static URI resolveLink(Node linkNode) { } public static HTMLRenderer openHyperlinkInBrowser() { - return new HTMLRenderer(uri -> { - var dialog = - new MessageDialogPane.Builder( - i18n("web.open_in_browser", uri), - i18n("message.confirm"), - MessageDialogPane.MessageType.QUESTION - ) - .addAction(i18n("button.copy"), () -> FXUtils.copyText(uri.toString())) - .yesOrNo(() -> FXUtils.openLink(uri.toString()), null) - .build(); - Controllers.dialog(dialog); - }); + return new HTMLRenderer(FXUtils::openUriInBrowser); } private final List children = new ArrayList<>(); @@ -306,7 +295,11 @@ public void appendNode(Node node) { if (n == null) break; if (n.nameIs("li")) i++; } - appendText("\n " + " ".repeat(Math.max(0, i)) + "\u2022 "); + if (i == 0) { + appendText("\n \u2022 "); + } else { + appendText("\n " + " ".repeat(Math.max(0, i)) + "\u2022 "); + } } case "dt" -> appendText(" "); case "p" -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 1183495c87..a0d3967fe5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -598,6 +598,16 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo box.getChildren().add(spinnerPane); VBox.setVgrow(spinnerPane, Priority.SOMETIMES); + JFXHyperlink versionPage = new JFXHyperlink(i18n("mods.url")); + versionPage.setDisable(true); + Task.supplyAsync(() -> repo.getVersionPageUrl(version)).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null && StringUtils.isNotBlank(result)) { + versionPage.setOnAction(__ -> FXUtils.openUriInBrowser(result)); + versionPage.setDisable(false); + } + }).start(); + box.getChildren().add(versionPage); + this.setBody(box); JFXButton closeButton = new JFXButton(i18n("button.ok")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index 6f267d15a2..fd18970adf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -102,6 +102,8 @@ SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Null String getModChangelog(String modId, String versionId) throws IOException; + String getVersionPageUrl(RemoteMod.Version version) throws IOException; + Stream getCategories() throws IOException; class Category { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 55a901ee1c..222e18cb40 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -260,6 +260,32 @@ public String getModChangelog(String modId, String versionId) throws IOException } } + @Override + public String getVersionPageUrl(RemoteMod.Version version) throws IOException { + SEMAPHORE.acquireUninterruptibly(); + try { + Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + version.getModid())) + .getJson(Response.typeOf(CurseAddon.class)); + var addon = response.getData(); + var classId = addon.getClassId(); + var clazz = switch (classId) { + case SECTION_MOD -> "mc-mods"; + case SECTION_RESOURCE_PACK -> "texture-packs"; + case SECTION_WORLD -> "worlds"; + case SECTION_MODPACK -> "modpacks"; + case SECTION_DATAPACK -> "data-packs"; + case SECTION_BUKKIT_PLUGIN -> "bukkit-plugins"; + case SECTION_ADDONS -> "mc-addons"; + case SECTION_CUSTOMIZATION -> "customization"; + case SECTION_SHADERS -> "shaders"; + default -> throw new IllegalArgumentException("Unsupported CurseForge class id [%d]".formatted(classId)); + }; + return "%s/minecraft/%s/%s/files/%s".formatted(BASE, clazz, addon.getSlug(), version.getVersionId()); + } finally { + SEMAPHORE.release(); + } + } + @Override public Stream getCategories() throws IOException { SEMAPHORE.acquireUninterruptibly(); @@ -297,6 +323,8 @@ private List reorganizeCategories(List public static final int SECTION_BUKKIT_PLUGIN = 5; public static final int SECTION_MOD = 6; public static final int SECTION_RESOURCE_PACK = 12; + public static final int SECTION_DATAPACK = 6945; + public static final int SECTION_SHADERS = 6552; public static final int SECTION_WORLD = 17; public static final int SECTION_MODPACK = 4471; public static final int SECTION_CUSTOMIZATION = 4546; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index b1111e6a92..4f96c1ade9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -185,6 +185,11 @@ public String getModChangelog(String modId, String versionId) throws IOException } } + @Override + public String getVersionPageUrl(RemoteMod.Version version) { + return "%s/mod/%s/version/%s".formatted(BASE, version.getModid(), version.getVersionId()); // Modrinth will help us redirect + } + @Override public Stream getCategories() throws IOException { SEMAPHORE.acquireUninterruptibly(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 4bca951071..4d03c0abfc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -562,7 +562,7 @@ public static String removeEmptyLinesAtBeginningAndEnd(String str) { public static boolean isStringHtml(String str) { if (isBlank(str)) return false; if (str.startsWith("") || str.startsWith("") || str.startsWith("")) return true; - if (!Jsoup.isValid(str, Safelist.relaxed().addAttributes("a", "rel"))) { + if (!Jsoup.isValid(str, Safelist.relaxed().addAttributes("a", "rel", "target"))) { return false; } var body = Jsoup.parse(str).body(); From 6b69b4dc1c92c31bc771d7135b44baefa1e1e904 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 27 Jan 2026 20:41:13 +0800 Subject: [PATCH 52/77] fix whitespace --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 4 +- .../org/jackhuang/hmcl/util/StringUtils.java | 37 +++++++++++-------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index ec8eb26417..dce6406b98 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -28,7 +28,6 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; -import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jsoup.nodes.Element; @@ -42,7 +41,6 @@ import java.util.function.Consumer; import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** @@ -276,7 +274,7 @@ private void appendTable(Node table) { public void appendNode(Node node) { if (node instanceof TextNode n) { - appendText(StringUtils.removeEmptyLinesAtBeginningAndEnd(n.getWholeText())); + appendText(StringUtils.normaliseWhitespace(n.getWholeText())); } String name = node.nodeName(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 4d03c0abfc..1c82f17ecd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -25,6 +25,7 @@ import org.commonmark.renderer.html.HtmlRenderer; import org.jetbrains.annotations.Contract; import org.jsoup.Jsoup; +import org.jsoup.internal.StringUtil; import org.jsoup.safety.Safelist; import java.io.PrintWriter; @@ -32,7 +33,6 @@ import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; /** * @author huangyuhui @@ -541,22 +541,27 @@ public static Optional nullIfBlank(String str) { return Optional.ofNullable(str).map(s -> s.isBlank() ? null : s); } - public static String removeEmptyLinesAtBeginningAndEnd(String str) { - if (str == null) return null; - var lines = str.lines().toList(); - int i = 0; - for (; i < lines.size(); i++) { - if (isNotBlank(lines.get(i))) break; - } - int j = lines.size() - 1; - for (; j > 0; j--) { - if (isNotBlank(lines.get(j))) break; - } - j = Math.min(j + 1, lines.size() - 1); - if (i > j) { - return ""; + /// @see StringUtil#normaliseWhitespace(String) + /// @see StringUtil#isActuallyWhitespace(int) + public static String normaliseWhitespace(String str) { + var accum = new StringBuilder(); + boolean lastWasWhite = false; + int len = str.length(); + int c; + for (int i = 0; i < len; i += Character.charCount(c)) { + c = str.codePointAt(i); + if (StringUtil.isWhitespace(c)) { // Ignore   + if (lastWasWhite) + continue; + accum.append(' '); + lastWasWhite = true; + } + else if (!StringUtil.isInvisibleChar(c)) { + accum.appendCodePoint(c); + lastWasWhite = false; + } } - return lines.subList(i, j + 1).stream().collect(Collectors.joining(System.lineSeparator())); + return accum.toString(); } public static boolean isStringHtml(String str) { From 95af42f5d63ba69bf28831625105000fd55654b0 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 27 Jan 2026 22:06:04 +0800 Subject: [PATCH 53/77] update --- .../hmcl/ui/versions/DownloadPage.java | 37 +++++++++++-------- .../hmcl/ui/versions/ModUpdatesPage.java | 25 ++++++++++++- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index a0d3967fe5..fe83a676a0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -471,7 +471,7 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP ScrollPane scrollPane = new ScrollPane(); ComponentList dependenciesList = new ComponentList(Lang::immutableListOf); loadDependencies(version, selfPage, spinnerPane, dependenciesList); - loadChangelog(version, selfPage, changelogButton); + loadChangelog(version, selfPage.repository, changelogButton); spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList)); scrollPane.setContent(dependenciesList); @@ -483,6 +483,10 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP this.setBody(box); + JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); + versionPageBtn.setDisable(true); + loadVersionPageUrl(version, selfPage.repository, versionPageBtn); + JFXButton downloadButton = null; if (selfPage.callback != null) { downloadButton = new JFXButton(type == RemoteModRepository.Type.MODPACK ? i18n("install.modpack") : i18n("mods.install")); @@ -509,9 +513,9 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); if (downloadButton == null) { - this.setActions(changelogButton, saveAsButton, cancelButton); + this.setActions(versionPageBtn, changelogButton, saveAsButton, cancelButton); } else { - this.setActions(changelogButton, downloadButton, saveAsButton, cancelButton); + this.setActions(versionPageBtn, changelogButton, downloadButton, saveAsButton, cancelButton); } this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7)); @@ -553,7 +557,7 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, }).start(); } - private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, Button changelogButton) { + private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, Button changelogButton) { changelogButton.setDisable(true); Task.supplyAsync(() -> { if (changelogCache.containsKey(version)) { @@ -561,7 +565,7 @@ private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, But } else if (version.getChangelog() != null) { return StringUtils.nullIfBlank(version.getChangelog()).map(StringUtils::convertToHtml); } else { - return StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::convertToHtml); + return StringUtils.nullIfBlank(repo.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::convertToHtml); } }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { @@ -569,7 +573,7 @@ private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, But String s = result.get(); changelogCache.put(version, s); changelogButton.setDisable(false); - changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(version, s, selfPage.repository))); + changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(version, s, repo))); } else { changelogCache.put(version, null); changelogButton.setOnAction(null); @@ -579,6 +583,17 @@ private void loadChangelog(RemoteMod.Version version, DownloadPage selfPage, But } }).start(); } + + private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink button) { + Task.supplyAsync(() -> repo.getVersionPageUrl(version)) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null && StringUtils.isNotBlank(result)) { + button.setOnAction(__ -> FXUtils.openUriInBrowser(result)); + button.setDisable(false); + } + }) + .start(); + } } private static final class AddonChangelog extends JFXDialogLayout { @@ -598,16 +613,6 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo box.getChildren().add(spinnerPane); VBox.setVgrow(spinnerPane, Priority.SOMETIMES); - JFXHyperlink versionPage = new JFXHyperlink(i18n("mods.url")); - versionPage.setDisable(true); - Task.supplyAsync(() -> repo.getVersionPageUrl(version)).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null && StringUtils.isNotBlank(result)) { - versionPage.setOnAction(__ -> FXUtils.openUriInBrowser(result)); - versionPage.setDisable(false); - } - }).start(); - box.getChildren().add(versionPage); - this.setBody(box); JFXButton closeButton = new JFXButton(i18n("button.ok")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 9924fc4fde..4bec602c97 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -27,8 +27,14 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.*; -import javafx.scene.layout.*; -import org.jackhuang.hmcl.mod.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -318,6 +324,10 @@ public ModChangelog(ModUpdateObject object) { this.setBody(box); + JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); + versionPageBtn.setDisable(true); + loadVersionPageUrl(object, versionPageBtn); + JFXButton closeButton = new JFXButton(i18n("button.ok")); closeButton.getStyleClass().add("dialog-accept"); closeButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); @@ -352,6 +362,17 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro spinnerPane.setLoading(false); }).start(); } + + private void loadVersionPageUrl(ModUpdateObject object, JFXHyperlink button) { + Task.supplyAsync(() -> repository.getVersionPageUrl(object.data.getCandidate())) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null && StringUtils.isNotBlank(result)) { + button.setOnAction(__ -> FXUtils.openUriInBrowser(result)); + button.setDisable(false); + } + }) + .start(); + } } public static class ModUpdateTask extends Task { From 21cae91eb9e8de35099b219050bcb3dd179bb522 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 27 Jan 2026 22:37:27 +0800 Subject: [PATCH 54/77] update --- .../main/java/org/jackhuang/hmcl/util/StringUtils.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 1c82f17ecd..ca1010e72c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -564,10 +564,13 @@ else if (!StringUtil.isInvisibleChar(c)) { return accum.toString(); } - public static boolean isStringHtml(String str) { + private static final Safelist all = Safelist.relaxed() + .addAttributes("a", "rel", "target"); + + public static boolean isHtml(String str) { if (isBlank(str)) return false; if (str.startsWith("") || str.startsWith("") || str.startsWith("")) return true; - if (!Jsoup.isValid(str, Safelist.relaxed().addAttributes("a", "rel", "target"))) { + if (!Jsoup.isValid(str, all)) { return false; } var body = Jsoup.parse(str).body(); @@ -584,10 +587,9 @@ public static boolean isStringHtml(String str) { AutolinkExtension.create(), InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create() )).build(); - @Contract(pure = true, value = "null -> null") public static String convertToHtml(String md) { if (md == null) return null; - if (isStringHtml(md)) return md; + if (isHtml(md)) return md; return HTML_RENDERER.render(MD_PARSER.parse(md)); } From c750de99a59d43d8a4b87dbbc31914e2f8bfdc88 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 27 Jan 2026 22:45:22 +0800 Subject: [PATCH 55/77] update --- .../main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 10 +++------- .../org/jackhuang/hmcl/ui/versions/DownloadPage.java | 6 +++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index dce6406b98..5d0146c9c0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -293,11 +293,7 @@ public void appendNode(Node node) { if (n == null) break; if (n.nameIs("li")) i++; } - if (i == 0) { - appendText("\n \u2022 "); - } else { - appendText("\n " + " ".repeat(Math.max(0, i)) + "\u2022 "); - } + appendText("\n " + " ".repeat(Math.max(0, i)) + "\u2022 "); } case "dt" -> appendText(" "); case "p" -> { @@ -312,15 +308,15 @@ public void appendNode(Node node) { } if (node.childNodeSize() > 0) { - pushNode(node); if ("table".equals(name)) { appendTable(node); } else { + pushNode(node); for (Node childNode : node.childNodes()) { appendNode(childNode); } + popNode(); } - popNode(); } switch (name) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index fe83a676a0..4384b0654b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -446,7 +446,6 @@ private static final class AddonItem extends StackPane { } private static final class AddonVersion extends JFXDialogLayout { - public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfPage) { RemoteModRepository.Type type = selfPage.repository.getType(); @@ -457,7 +456,7 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP case SHADER_PACK -> "shaderpack.download.title"; default -> "mods.download.title"; }; - this.setHeading(new HBox(new Label(I18n.i18n(title, version.getName())))); + this.setHeading(new HBox(new Label(i18n(title, version.getName())))); VBox box = new VBox(8); box.setPadding(new Insets(8)); @@ -467,11 +466,12 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP Button changelogButton = new JFXButton(i18n("mods.changelog")); changelogButton.getStyleClass().add("dialog-accept"); + loadChangelog(version, selfPage.repository, changelogButton); + SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); ComponentList dependenciesList = new ComponentList(Lang::immutableListOf); loadDependencies(version, selfPage, spinnerPane, dependenciesList); - loadChangelog(version, selfPage.repository, changelogButton); spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList)); scrollPane.setContent(dependenciesList); From 21fabbc716fc450fa6cf287222d327513cf8c122 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 27 Jan 2026 23:00:48 +0800 Subject: [PATCH 56/77] =?UTF-8?q?=E5=BC=82=E6=AD=A5=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=97=A5=E5=BF=97=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 5d0146c9c0..65e19c0b5e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -17,17 +17,19 @@ */ package org.jackhuang.hmcl.ui; +import javafx.beans.InvalidationListener; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; -import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jsoup.nodes.Element; @@ -204,12 +206,23 @@ private void appendImage(Node node) { } try { - Image image = FXUtils.getRemoteImageTask(src, width, height, true, true) - .run(); - if (image == null) - throw new AssertionError("Image loading task returned null"); + ImageView imageView = new ImageView(); + + int finalWidth = width; + int finalHeight = height; + Task.supplyAsync(() -> FXUtils.getRemoteImageTask(src, finalWidth, finalHeight, true, true).run()) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + if (result == null) { + throw new AssertionError("Image loading task returned null"); + } + imageView.setImage(result); + } else { + LOG.warning("Failed to load image: " + src, exception); + } + }) + .start(); - ImageView imageView = new ImageView(image); if (hyperlink != null) { URI target = resolveLink(hyperlink); if (target != null) { @@ -376,8 +389,10 @@ public TextFlow render() { textFlow.getChildren().setAll(children); for (javafx.scene.Node node : children) { if (node instanceof ImageView img) { - double width = img.getImage().getWidth(); - img.fitWidthProperty().bind(textFlow.widthProperty().map(d -> Math.min((double) d - 20D, width))); + InvalidationListener i = __ -> + img.setFitWidth(Math.min(textFlow.getWidth() - 20D, img.getImage() == null ? 0D : img.getImage().getWidth())); + textFlow.widthProperty().addListener(i); + img.imageProperty().addListener(i); } else if (node instanceof TableView table) { table.prefWidthProperty().bind(textFlow.widthProperty().add(-20D)); } From 1e1b6ba2192ead0b50505799e9eff1946ce04d8d Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 27 Jan 2026 23:06:27 +0800 Subject: [PATCH 57/77] update --- .../main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 65e19c0b5e..d01ef3bb73 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -28,7 +28,6 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; -import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; @@ -210,15 +209,15 @@ private void appendImage(Node node) { int finalWidth = width; int finalHeight = height; - Task.supplyAsync(() -> FXUtils.getRemoteImageTask(src, finalWidth, finalHeight, true, true).run()) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { + Task.runAsync(() -> { + try { + var result = FXUtils.getRemoteImageTask(src, finalWidth, finalHeight, true, true).run(); if (result == null) { throw new AssertionError("Image loading task returned null"); } imageView.setImage(result); - } else { - LOG.warning("Failed to load image: " + src, exception); + } catch (Throwable e) { + LOG.warning("Failed to load image: " + src, e); } }) .start(); From 19a442caa48a1a4829c32b246d91bb337514f925 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 28 Jan 2026 07:15:10 +0800 Subject: [PATCH 58/77] update --- .../hmcl/ui/versions/DownloadPage.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 4384b0654b..372cdfaaa8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -467,7 +467,7 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP Button changelogButton = new JFXButton(i18n("mods.changelog")); changelogButton.getStyleClass().add("dialog-accept"); loadChangelog(version, selfPage.repository, changelogButton); - + SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); ComponentList dependenciesList = new ComponentList(Lang::immutableListOf); @@ -483,10 +483,6 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP this.setBody(box); - JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); - versionPageBtn.setDisable(true); - loadVersionPageUrl(version, selfPage.repository, versionPageBtn); - JFXButton downloadButton = null; if (selfPage.callback != null) { downloadButton = new JFXButton(type == RemoteModRepository.Type.MODPACK ? i18n("install.modpack") : i18n("mods.install")); @@ -513,9 +509,9 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); if (downloadButton == null) { - this.setActions(versionPageBtn, changelogButton, saveAsButton, cancelButton); + this.setActions(changelogButton, saveAsButton, cancelButton); } else { - this.setActions(versionPageBtn, changelogButton, downloadButton, saveAsButton, cancelButton); + this.setActions(changelogButton, downloadButton, saveAsButton, cancelButton); } this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7)); @@ -583,17 +579,6 @@ private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, } }).start(); } - - private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink button) { - Task.supplyAsync(() -> repo.getVersionPageUrl(version)) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null && StringUtils.isNotBlank(result)) { - button.setOnAction(__ -> FXUtils.openUriInBrowser(result)); - button.setDisable(false); - } - }) - .start(); - } } private static final class AddonChangelog extends JFXDialogLayout { @@ -615,17 +600,32 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo this.setBody(box); + JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); + versionPageBtn.setDisable(true); + loadVersionPageUrl(version, repo, versionPageBtn); + JFXButton closeButton = new JFXButton(i18n("button.ok")); closeButton.getStyleClass().add("dialog-accept"); closeButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - setActions(closeButton); + setActions(versionPageBtn, 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); } + + private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink button) { + Task.supplyAsync(() -> repo.getVersionPageUrl(version)) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null && StringUtils.isNotBlank(result)) { + button.setOnAction(__ -> FXUtils.openUriInBrowser(result)); + button.setDisable(false); + } + }) + .start(); + } } public interface DownloadCallback { From 89e7e3ce50774699eb3aeb73570ac5b6a069d09a Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 28 Jan 2026 16:31:25 +0800 Subject: [PATCH 59/77] update --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index d01ef3bb73..ee5e0fb5b5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -210,17 +210,16 @@ private void appendImage(Node node) { int finalWidth = width; int finalHeight = height; Task.runAsync(() -> { - try { - var result = FXUtils.getRemoteImageTask(src, finalWidth, finalHeight, true, true).run(); - if (result == null) { - throw new AssertionError("Image loading task returned null"); - } - imageView.setImage(result); - } catch (Throwable e) { - LOG.warning("Failed to load image: " + src, e); - } - }) - .start(); + try { + var result = FXUtils.getRemoteImageTask(src, finalWidth, finalHeight, true, true).run(); + if (result == null) { + throw new AssertionError("Image loading task returned null"); + } + imageView.setImage(result); + } catch (Throwable e) { + LOG.warning("Failed to load image: " + src, e); + } + }).start(); if (hyperlink != null) { URI target = resolveLink(hyperlink); From 8315137166540c995bfe7613804c34dc4b22565a Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 28 Jan 2026 18:24:00 +0800 Subject: [PATCH 60/77] fix table empty line --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index ee5e0fb5b5..30bd41590c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -273,6 +273,8 @@ private void appendTable(Node table) { } TableView tableView = new TableView<>(FXCollections.observableList(Arrays.asList(bodyRows))); + tableView.setFixedCellSize(25); + tableView.setPrefHeight(25 * (bodyRows.length + 1)); for (int i = 0; i < headRow.length; i++) { int finalI = i; TableColumn c = new TableColumn<>(headRow[i]); From c5ef9f992912652289bec013902edc912e8e24c6 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 28 Jan 2026 20:11:46 +0800 Subject: [PATCH 61/77] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 30bd41590c..e3cff318b8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -274,7 +274,7 @@ private void appendTable(Node table) { TableView tableView = new TableView<>(FXCollections.observableList(Arrays.asList(bodyRows))); tableView.setFixedCellSize(25); - tableView.setPrefHeight(25 * (bodyRows.length + 1)); + tableView.setPrefHeight(25 * (bodyRows.length + 1) + 5); for (int i = 0; i < headRow.length; i++) { int finalI = i; TableColumn c = new TableColumn<>(headRow[i]); From e42a93a3b5b220ff16e5adbf1b18c5cddf96de2e Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 13:49:20 +0800 Subject: [PATCH 62/77] update --- .../org/jackhuang/hmcl/ui/versions/DownloadPage.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index e1b6eafc59..2d127f367a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -590,14 +590,11 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo VBox box = new VBox(8); box.setPadding(new Insets(8)); - SpinnerPane spinnerPane = new SpinnerPane(); - ScrollPane scrollPane = new ScrollPane(); + ScrollPane scrollPane = new ScrollPane(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); scrollPane.setFitToWidth(true); - scrollPane.setContent(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); - spinnerPane.setContent(scrollPane); - box.getChildren().add(spinnerPane); - VBox.setVgrow(spinnerPane, Priority.SOMETIMES); + VBox.setVgrow(scrollPane, Priority.SOMETIMES); + box.getChildren().add(scrollPane); this.setBody(box); From 71c1dd658b34c166033929dd8b21b8eae59c12f8 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 14:22:33 +0800 Subject: [PATCH 63/77] update --- .../java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 2d127f367a..6c0f029d37 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -608,8 +608,8 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo setActions(versionPageBtn, 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)); + this.prefWidthProperty().bind(Controllers.getStage().widthProperty().multiply(0.7)); + this.prefHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.7)); onEscPressed(this, closeButton::fire); } From 9c6a25b7f674203fcef665cf4e985a1b383d6cc4 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 18:16:50 +0800 Subject: [PATCH 64/77] update --- .../hmcl/mod/curse/CurseForgeRemoteModRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 222e18cb40..d5c397f3d1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -277,7 +277,7 @@ public String getVersionPageUrl(RemoteMod.Version version) throws IOException { case SECTION_BUKKIT_PLUGIN -> "bukkit-plugins"; case SECTION_ADDONS -> "mc-addons"; case SECTION_CUSTOMIZATION -> "customization"; - case SECTION_SHADERS -> "shaders"; + case SECTION_SHADER -> "shaders"; default -> throw new IllegalArgumentException("Unsupported CurseForge class id [%d]".formatted(classId)); }; return "%s/minecraft/%s/%s/files/%s".formatted(BASE, clazz, addon.getSlug(), version.getVersionId()); @@ -324,9 +324,9 @@ private List reorganizeCategories(List public static final int SECTION_MOD = 6; public static final int SECTION_RESOURCE_PACK = 12; public static final int SECTION_DATAPACK = 6945; - public static final int SECTION_SHADERS = 6552; public static final int SECTION_WORLD = 17; public static final int SECTION_MODPACK = 4471; + public static final int SECTION_SHADER = 6552; public static final int SECTION_CUSTOMIZATION = 4546; public static final int SECTION_ADDONS = 4559; // For Pocket Edition public static final int SECTION_UNKNOWN1 = 4944; From 485a881dc3a309b1b824696f3ac9bc714fd48a6a Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 20:55:11 +0800 Subject: [PATCH 65/77] smooth scrolling --- .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 1 + .../main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 1 + 2 files changed, 2 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index e5a97f9da1..087cf84220 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -594,6 +594,7 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo ScrollPane scrollPane = new ScrollPane(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); scrollPane.setFitToWidth(true); + FXUtils.smoothScrolling(scrollPane); VBox.setVgrow(scrollPane, Priority.SOMETIMES); box.getChildren().add(scrollPane); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 4bec602c97..f0a380d83e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -314,6 +314,7 @@ public ModChangelog(ModUpdateObject object) { SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToWidth(true); + FXUtils.smoothScrolling(scrollPane); loadChangelog(object, spinnerPane, scrollPane); spinnerPane.setOnFailedAction(e -> loadChangelog(object, spinnerPane, scrollPane)); From 554b0b08cb4b38ebde9aeff8c99b590f504831ac Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 15:57:53 +0800 Subject: [PATCH 66/77] update --- .../hmcl/ui/versions/DownloadPage.java | 36 +++++++++---------- .../hmcl/ui/versions/ModUpdatesPage.java | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 087cf84220..8543a5e634 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -486,6 +486,10 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP this.setBody(box); + JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); + versionPageBtn.setDisable(true); + loadVersionPageUrl(version, selfPage.repository, versionPageBtn); + JFXButton downloadButton = null; if (selfPage.callback != null) { downloadButton = new JFXButton(type == RemoteModRepository.Type.MODPACK ? i18n("install.modpack") : i18n("mods.install")); @@ -512,9 +516,9 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); if (downloadButton == null) { - this.setActions(changelogButton, saveAsButton, cancelButton); + this.setActions(versionPageBtn, changelogButton, saveAsButton, cancelButton); } else { - this.setActions(changelogButton, downloadButton, saveAsButton, cancelButton); + this.setActions(versionPageBtn, changelogButton, downloadButton, saveAsButton, cancelButton); } this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7)); @@ -582,6 +586,17 @@ private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, } }).start(); } + + private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink button) { + Task.supplyAsync(() -> repo.getVersionPageUrl(version)) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null && StringUtils.isNotBlank(result)) { + button.setOnAction(__ -> FXUtils.openUriInBrowser(result)); + button.setDisable(false); + } + }) + .start(); + } } private static final class AddonChangelog extends JFXDialogLayout { @@ -601,32 +616,17 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo this.setBody(box); - JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); - versionPageBtn.setDisable(true); - loadVersionPageUrl(version, repo, versionPageBtn); - JFXButton closeButton = new JFXButton(i18n("button.ok")); closeButton.getStyleClass().add("dialog-accept"); closeButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - setActions(versionPageBtn, closeButton); + setActions(closeButton); this.prefWidthProperty().bind(Controllers.getStage().widthProperty().multiply(0.7)); this.prefHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.7)); onEscPressed(this, closeButton::fire); } - - private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink button) { - Task.supplyAsync(() -> repo.getVersionPageUrl(version)) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null && StringUtils.isNotBlank(result)) { - button.setOnAction(__ -> FXUtils.openUriInBrowser(result)); - button.setDisable(false); - } - }) - .start(); - } } public interface DownloadCallback { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index f0a380d83e..40b06119fa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -314,7 +314,6 @@ public ModChangelog(ModUpdateObject object) { SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToWidth(true); - FXUtils.smoothScrolling(scrollPane); loadChangelog(object, spinnerPane, scrollPane); spinnerPane.setOnFailedAction(e -> loadChangelog(object, spinnerPane, scrollPane)); @@ -356,6 +355,7 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro if (exception == null) { object.changelog = result.orElse(i18n("mods.changelog.empty")); scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, object.data.getRepository().getBaseUrl())); + FXUtils.smoothScrolling(scrollPane); spinnerPane.setFailedReason(null); } else { spinnerPane.setFailedReason(i18n("download.failed.refresh")); From c7177498d32c8ae90a323b8486c321d76126cac9 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 21:01:45 +0800 Subject: [PATCH 67/77] update --- .../java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index e3cff318b8..5e4386d0f9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -110,9 +110,12 @@ private void updateStyle() { String style = node.attr("style"); if (StringUtils.isNotBlank(style)) { - fxStyle = style - .replace("color:", "-fx-fill:") - .replace("font-size:", "-fx-font-size:"); // And more + fxStyle = StringUtils.addSuffix( + style + .replace("color:", "-fx-fill:") + .replace("font-size:", "-fx-font-size:"), // And more + ";" + ); } } } @@ -128,6 +131,8 @@ private void popNode() { } private void applyStyle(Text text) { + var styleBuilder = new StringBuilder(); + if (hyperlink != null) { URI target = resolveLink(hyperlink); if (target != null) { @@ -151,14 +156,15 @@ private void applyStyle(Text text) { if (code) { text.getStyleClass().add("html-code"); - text.setStyle("-fx-font-family: \"%s\";".formatted(Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT))); + styleBuilder.append("-fx-font-family: \"%s\";".formatted(Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT))); } if (headerLevel != null) text.getStyleClass().add("html-" + headerLevel); if (fxStyle != null) - text.setStyle(fxStyle); + styleBuilder.append(fxStyle); + text.setStyle(styleBuilder.toString()); } private void appendText(String text) { From 384d0334e1b12f1b656c372d03c32102038ec7b5 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 21:08:34 +0800 Subject: [PATCH 68/77] update --- .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 40b06119fa..cfd0f1e110 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -332,7 +332,7 @@ public ModChangelog(ModUpdateObject object) { closeButton.getStyleClass().add("dialog-accept"); closeButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - setActions(closeButton); + setActions(versionPageBtn, 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)); From 6a3166480f49d6e229461358ffe6fc70104d3857 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 21:09:55 +0800 Subject: [PATCH 69/77] update --- HMCLCore/build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index 700cd4c328..cc66b3af4f 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -29,7 +29,7 @@ dependencies { api(libs.commonmark) api(libs.commonmark.autolink) api(libs.commonmark.underline) - api(libs.commonmark.stikethrough) + api(libs.commonmark.strikethrough) api(libs.commonmark.table) compileOnlyApi(libs.jetbrains.annotations) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba0eb29079..09ade5aeff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,7 +55,7 @@ nayuki-qrcodegen = { module = "io.nayuki:qrcodegen", version.ref = "nayuki-qrcod commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" } commonmark-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" } commonmark-underline = { module = "org.commonmark:commonmark-ext-ins", version.ref = "commonmark" } -commonmark-stikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" } +commonmark-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" } commonmark-table = { module = "org.commonmark:commonmark-ext-gfm-tables", version.ref = "commonmark" } # testing From fcc8afa748f0cc0f6a8f812cbb41d3dd788ab0f8 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 21:14:46 +0800 Subject: [PATCH 70/77] update --- .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index cfd0f1e110..0789f70b0f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -100,7 +100,7 @@ public ModUpdatesPage(ModManager modManager, List update TableColumn sourceColumn = new TableColumn<>(i18n("mods.check_updates.source")); setupCellValueFactory(sourceColumn, ModUpdateObject::sourceProperty); - TableColumn changelogColumn = new TableColumn<>(); + TableColumn changelogColumn = new TableColumn<>(i18n("mods.changelog")); { var oldCellFactory = changelogColumn.getCellFactory(); changelogColumn.setCellFactory(param -> { @@ -116,7 +116,7 @@ public ModUpdatesPage(ModManager modManager, List update }); return cell; }); - changelogColumn.setCellValueFactory(__ -> new SimpleStringProperty(i18n("mods.changelog"))); + changelogColumn.setCellValueFactory(__ -> new SimpleStringProperty(i18n("button.view"))); } objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList())); From a1edcac10c3291dae3012a87e5dcd6c938891b2c Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 21:15:55 +0800 Subject: [PATCH 71/77] update --- HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 20d5d09491..fc9a31e34a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -599,7 +599,7 @@ public static boolean isAlphabeticOrNumber(String str) { @Contract(pure = true) public static Optional nullIfBlank(String str) { - return Optional.ofNullable(str).map(s -> s.isBlank() ? null : s); + return Optional.ofNullable(str).filter(s -> !s.isBlank()); } /// @see StringUtil#normaliseWhitespace(String) From baa41cdbea881a84487dbc556e4e2a3e1ed495ab Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 21:22:19 +0800 Subject: [PATCH 72/77] update --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 37 ++++++++++++++++++- .../org/jackhuang/hmcl/util/StringUtils.java | 24 ------------ 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 5e4386d0f9..6eb51dfb3b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -31,6 +31,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; +import org.jsoup.internal.StringUtil; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; @@ -65,6 +66,40 @@ public static HTMLRenderer openHyperlinkInBrowser() { return new HTMLRenderer(FXUtils::openUriInBrowser); } + /// @see StringUtil#isWhitespace(int) + public static boolean isWhitespace(int c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r'; + } + + /// @see StringUtil#isInvisibleChar(int) + public static boolean isInvisibleChar(int c) { + return c == 8203 || c == 173; // zero width sp, soft hyphen + // previously also included zw non join, zw join - but removing those breaks semantic meaning of text + } + + /// @see StringUtil#normaliseWhitespace(String) + /// @see StringUtil#isActuallyWhitespace(int) + public static String normaliseWhitespace(String str) { + var accum = new StringBuilder(); + boolean lastWasWhite = false; + int len = str.length(); + int c; + for (int i = 0; i < len; i += Character.charCount(c)) { + c = str.codePointAt(i); + if (isWhitespace(c)) { // Ignore   + if (lastWasWhite) + continue; + accum.append(' '); + lastWasWhite = true; + } + else if (!isInvisibleChar(c)) { + accum.appendCodePoint(c); + lastWasWhite = false; + } + } + return accum.toString(); + } + private final List children = new ArrayList<>(); private final List stack = new ArrayList<>(); @@ -293,7 +328,7 @@ private void appendTable(Node table) { public void appendNode(Node node) { if (node instanceof TextNode n) { - appendText(StringUtils.normaliseWhitespace(n.getWholeText())); + appendText(normaliseWhitespace(n.getWholeText())); } String name = node.nodeName(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index fc9a31e34a..fb95ed8ce3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -25,7 +25,6 @@ import org.commonmark.renderer.html.HtmlRenderer; import org.jetbrains.annotations.Contract; import org.jsoup.Jsoup; -import org.jsoup.internal.StringUtil; import org.jsoup.safety.Safelist; import java.io.PrintWriter; @@ -602,29 +601,6 @@ public static Optional nullIfBlank(String str) { return Optional.ofNullable(str).filter(s -> !s.isBlank()); } - /// @see StringUtil#normaliseWhitespace(String) - /// @see StringUtil#isActuallyWhitespace(int) - public static String normaliseWhitespace(String str) { - var accum = new StringBuilder(); - boolean lastWasWhite = false; - int len = str.length(); - int c; - for (int i = 0; i < len; i += Character.charCount(c)) { - c = str.codePointAt(i); - if (StringUtil.isWhitespace(c)) { // Ignore   - if (lastWasWhite) - continue; - accum.append(' '); - lastWasWhite = true; - } - else if (!StringUtil.isInvisibleChar(c)) { - accum.appendCodePoint(c); - lastWasWhite = false; - } - } - return accum.toString(); - } - private static final Safelist all = Safelist.relaxed() .addAttributes("a", "rel", "target"); From 1a61b2c2eb633af9a4b2a70135f6a146368a816b Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 21:30:42 +0800 Subject: [PATCH 73/77] update --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 6eb51dfb3b..1de9f455a6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -28,7 +28,7 @@ import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; -import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jsoup.internal.StringUtil; @@ -66,12 +66,12 @@ public static HTMLRenderer openHyperlinkInBrowser() { return new HTMLRenderer(FXUtils::openUriInBrowser); } - /// @see StringUtil#isWhitespace(int) + /// @see StringUtil#isWhitespace(int) public static boolean isWhitespace(int c) { return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r'; } - /// @see StringUtil#isInvisibleChar(int) + /// @see StringUtil#isInvisibleChar(int) public static boolean isInvisibleChar(int c) { return c == 8203 || c == 173; // zero width sp, soft hyphen // previously also included zw non join, zw join - but removing those breaks semantic meaning of text @@ -91,8 +91,7 @@ public static String normaliseWhitespace(String str) { continue; accum.append(' '); lastWasWhite = true; - } - else if (!isInvisibleChar(c)) { + } else if (!isInvisibleChar(c)) { accum.appendCodePoint(c); lastWasWhite = false; } @@ -248,18 +247,17 @@ private void appendImage(Node node) { try { ImageView imageView = new ImageView(); - int finalWidth = width; - int finalHeight = height; - Task.runAsync(() -> { - try { - var result = FXUtils.getRemoteImageTask(src, finalWidth, finalHeight, true, true).run(); - if (result == null) { - throw new AssertionError("Image loading task returned null"); - } - imageView.setImage(result); - } catch (Throwable e) { + FXUtils.getRemoteImageTask( + src, width, height, true, true + ).whenComplete(Schedulers.javafx(), (res, e) -> { + if (e != null) { LOG.warning("Failed to load image: " + src, e); + return; + } + if (res == null) { + LOG.warning("Failed to load image: " + src, new AssertionError("Image loading task returned null")); } + imageView.setImage(res); }).start(); if (hyperlink != null) { From 37e69cab7fa7befd75723be50344da4caad26064 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 22:54:38 +0800 Subject: [PATCH 74/77] update --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 135 +++++++++++++----- .../java/org/jackhuang/hmcl/util/Lang.java | 10 ++ 2 files changed, 109 insertions(+), 36 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 1de9f455a6..56d88b7fbd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -37,9 +37,7 @@ import org.jsoup.nodes.TextNode; import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; import java.util.function.Consumer; import static org.jackhuang.hmcl.setting.ConfigHolder.config; @@ -280,48 +278,113 @@ private void appendImage(Node node) { } private void appendTable(Node table) { - var childNodes = table.childNodes(); - - var headOptional = childNodes.stream().filter(n -> n.nameIs("thead")).findFirst(); - if (headOptional.isEmpty()) return; - var head = (Element) headOptional.get(); - String[] headRow = head.getAllElements().stream() - .filter(n -> n.nameIs("th")) - .map(Element::text) - .toArray(String[]::new); - if (headRow.length == 0) return; - - appendAutoLineBreak("\n"); - var bodyOptional = childNodes.stream().filter(n -> n.nameIs("tbody")).findFirst(); - String[][] bodyRows; - if (bodyOptional.isEmpty()) { - bodyRows = new String[0][headRow.length]; - } else { - var body = (Element) bodyOptional.get(); - var r = body.getAllElements().stream() - .filter(n -> n.nameIs("tr")) - .map(n -> n.getAllElements().stream() - .filter(e -> e.nameIs("td")) - .map(Element::text) - .toArray(String[]::new)) - .toList(); - bodyRows = new String[r.size()][headRow.length]; - for (int i = 0; i < r.size(); i++) { - bodyRows[i] = r.get(i); + var childElements = ((Element) table).children(); + List captions = new ArrayList<>(); + + List head = new ArrayList<>(); + List> body = new ArrayList<>(); + List foot = new ArrayList<>(); + + boolean hasHead = false; + boolean hasBody = false; + boolean hasFoot = false; + int columnCount = 0; + for (Element child : childElements) { + if (child.nameIs("caption")) { + captions.add(child); + continue; + } + if (child.nameIs("thead")) { + if (hasHead) continue; + hasHead = true; + for (Element e : child.children()) { + if (e.nameIs("tr")) { + head.clear(); + head.addAll( + e.children().stream() + .filter(n -> n.nameIs("th") || n.nameIs("td")) + .map(Element::text) + .toList() + ); + break; + } + if (e.nameIs("th") || e.nameIs("td")) { + head.add(e.text()); + } + } + columnCount = Math.max(columnCount, head.size()); + continue; + } + if (child.nameIs("tbody")) { + if (hasBody) continue; + hasBody = true; + body.clear(); + for (Element e : child.children()) { + if (e.nameIs("tr")) { + List row = e.children().stream() + .filter(n -> n.nameIs("th") || n.nameIs("td")) + .map(Element::text) + .toList(); + columnCount = Math.max(columnCount, row.size()); + if (!row.isEmpty()) body.add(row); + } + } + continue; + } + if (child.nameIs("tfoot")) { + if (hasFoot) continue; + hasFoot = true; + for (Element e : child.children()) { + if (e.nameIs("tr")) { + foot.clear(); + foot.addAll( + e.children().stream() + .filter(n -> n.nameIs("th") || n.nameIs("td")) + .map(Element::text) + .toList() + ); + break; + } + if (e.nameIs("th") || e.nameIs("td")) { + foot.add(e.text()); + } + } + columnCount = Math.max(columnCount, foot.size()); + continue; + } + if (child.nameIs("tr") && !hasBody) { + List row = child.children().stream() + .filter(n -> n.nameIs("th") || n.nameIs("td")) + .map(Element::text) + .toList(); + columnCount = Math.max(columnCount, row.size()); + if (!row.isEmpty()) body.add(row); } } - TableView tableView = new TableView<>(FXCollections.observableList(Arrays.asList(bodyRows))); + List> rows = new ArrayList<>(hasFoot ? body.size() + 1 : body.size()); + for (List row : body) + rows.add(Lang.copyWithSize(row, columnCount, "")); + if (hasFoot) + rows.add(Lang.copyWithSize(foot, columnCount, "")); + + TableView> tableView = new TableView<>(FXCollections.observableList(rows)); tableView.setFixedCellSize(25); - tableView.setPrefHeight(25 * (bodyRows.length + 1) + 5); - for (int i = 0; i < headRow.length; i++) { + tableView.setPrefHeight(25 * (rows.size() + 1) + 5); + for (int i = 0; i < columnCount; i++) { int finalI = i; - TableColumn c = new TableColumn<>(headRow[i]); - c.setCellValueFactory(param -> new SimpleStringProperty(param.getValue()[finalI])); + TableColumn, String> c = new TableColumn<>(head.size() > i ? head.get(i) : ""); + c.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().get(finalI))); tableView.getColumns().add(c); } children.add(tableView); + + for (Element caption : captions) { + appendAutoLineBreak("\n\n"); + appendText(normaliseWhitespace(caption.wholeText())); + appendAutoLineBreak("\n"); + } } public void appendNode(Node node) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index f55008ca11..66cb93d132 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -419,6 +419,16 @@ public static void forEachZipped(Iterable i1, Iterable i2, BiConsum action.accept(it1.next(), it2.next()); } + public static List copyWithSize(List list, int newSize, T defaultValue) { + if (list.size() == newSize) return new ArrayList<>(list); + if (list.size() > newSize) return new ArrayList<>(list.subList(0, newSize)); + List result = new ArrayList<>(newSize); + result.addAll(list); + for (int i = list.size(); i < newSize; i++) + result.add(defaultValue); + return result; + } + public static Throwable resolveException(Throwable e) { if (e instanceof ExecutionException || e instanceof CompletionException) return resolveException(e.getCause()); From cf11f23c138cb09bdea97bc684c0b75fe54a0c16 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 22:56:41 +0800 Subject: [PATCH 75/77] update --- .../main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 56d88b7fbd..ab06481938 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -31,7 +31,6 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; -import org.jsoup.internal.StringUtil; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; @@ -64,19 +63,19 @@ public static HTMLRenderer openHyperlinkInBrowser() { return new HTMLRenderer(FXUtils::openUriInBrowser); } - /// @see StringUtil#isWhitespace(int) + /// @see org.jsoup.internal.StringUtil#isWhitespace(int) public static boolean isWhitespace(int c) { return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r'; } - /// @see StringUtil#isInvisibleChar(int) + /// @see org.jsoup.internal.StringUtil#isInvisibleChar(int) public static boolean isInvisibleChar(int c) { return c == 8203 || c == 173; // zero width sp, soft hyphen // previously also included zw non join, zw join - but removing those breaks semantic meaning of text } - /// @see StringUtil#normaliseWhitespace(String) - /// @see StringUtil#isActuallyWhitespace(int) + /// @see org.jsoup.internal.StringUtil#normaliseWhitespace(String) + /// @see org.jsoup.internal.StringUtil#isActuallyWhitespace(int) public static String normaliseWhitespace(String str) { var accum = new StringBuilder(); boolean lastWasWhite = false; From 72b31f6a343c566c7faef7cf32b51205d14a1b1f Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 20 Feb 2026 11:48:58 +0800 Subject: [PATCH 76/77] pre & ol --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 187 ++++++++++-------- 1 file changed, 101 insertions(+), 86 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index ab06481938..8062243c80 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -36,7 +36,8 @@ import org.jsoup.nodes.TextNode; import java.net.URI; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; import static org.jackhuang.hmcl.setting.ConfigHolder.config; @@ -104,7 +105,9 @@ public static String normaliseWhitespace(String str) { private boolean underline; private boolean strike; private boolean highlight; + private boolean preformatted; private boolean code; + private int listDepth; private String headerLevel; private Node hyperlink; private String fxStyle; @@ -121,7 +124,9 @@ private void updateStyle() { underline = false; strike = false; highlight = false; + preformatted = false; code = false; + listDepth = 0; headerLevel = null; hyperlink = null; fxStyle = null; @@ -134,9 +139,11 @@ private void updateStyle() { case "ins" -> underline = true; case "del" -> strike = true; case "mark" -> highlight = true; + case "pre" -> preformatted = true; case "code" -> code = true; case "a" -> hyperlink = node; case "h1", "h2", "h3", "h4", "h5", "h6" -> headerLevel = nodeName; + case "li" -> listDepth++; } String style = node.attr("style"); @@ -289,75 +296,72 @@ private void appendTable(Node table) { boolean hasFoot = false; int columnCount = 0; for (Element child : childElements) { - if (child.nameIs("caption")) { - captions.add(child); - continue; - } - if (child.nameIs("thead")) { - if (hasHead) continue; - hasHead = true; - for (Element e : child.children()) { - if (e.nameIs("tr")) { - head.clear(); - head.addAll( - e.children().stream() - .filter(n -> n.nameIs("th") || n.nameIs("td")) - .map(Element::text) - .toList() - ); - break; - } - if (e.nameIs("th") || e.nameIs("td")) { - head.add(e.text()); + switch (child.nodeName()) { + case "caption" -> captions.add(child); + case "thead" -> { + if (hasHead) continue; + hasHead = true; + for (Element e : child.children()) { + if (e.nameIs("tr")) { + head.clear(); + head.addAll( + e.children().stream() + .filter(n -> n.nameIs("th") || n.nameIs("td")) + .map(Element::text) + .toList() + ); + break; + } + if (e.nameIs("th") || e.nameIs("td")) { + head.add(e.text()); + } } + columnCount = Math.max(columnCount, head.size()); } - columnCount = Math.max(columnCount, head.size()); - continue; - } - if (child.nameIs("tbody")) { - if (hasBody) continue; - hasBody = true; - body.clear(); - for (Element e : child.children()) { - if (e.nameIs("tr")) { - List row = e.children().stream() - .filter(n -> n.nameIs("th") || n.nameIs("td")) - .map(Element::text) - .toList(); - columnCount = Math.max(columnCount, row.size()); - if (!row.isEmpty()) body.add(row); + case "tbody" -> { + if (hasBody) continue; + hasBody = true; + body.clear(); + for (Element e : child.children()) { + if (e.nameIs("tr")) { + List row = e.children().stream() + .filter(n -> n.nameIs("th") || n.nameIs("td")) + .map(Element::text) + .toList(); + columnCount = Math.max(columnCount, row.size()); + if (!row.isEmpty()) body.add(row); + } } } - continue; - } - if (child.nameIs("tfoot")) { - if (hasFoot) continue; - hasFoot = true; - for (Element e : child.children()) { - if (e.nameIs("tr")) { - foot.clear(); - foot.addAll( - e.children().stream() - .filter(n -> n.nameIs("th") || n.nameIs("td")) - .map(Element::text) - .toList() - ); - break; - } - if (e.nameIs("th") || e.nameIs("td")) { - foot.add(e.text()); + case "tfoot" -> { + if (hasFoot) continue; + hasFoot = true; + for (Element e : child.children()) { + if (e.nameIs("tr")) { + foot.clear(); + foot.addAll( + e.children().stream() + .filter(n -> n.nameIs("th") || n.nameIs("td")) + .map(Element::text) + .toList() + ); + break; + } + if (e.nameIs("th") || e.nameIs("td")) { + foot.add(e.text()); + } } + columnCount = Math.max(columnCount, foot.size()); + } + case "tr" -> { + if (hasBody) continue; + List row = child.children().stream() + .filter(n -> n.nameIs("th") || n.nameIs("td")) + .map(Element::text) + .toList(); + columnCount = Math.max(columnCount, row.size()); + if (!row.isEmpty()) body.add(row); } - columnCount = Math.max(columnCount, foot.size()); - continue; - } - if (child.nameIs("tr") && !hasBody) { - List row = child.children().stream() - .filter(n -> n.nameIs("th") || n.nameIs("td")) - .map(Element::text) - .toList(); - columnCount = Math.max(columnCount, row.size()); - if (!row.isEmpty()) body.add(row); } } @@ -386,9 +390,39 @@ private void appendTable(Node table) { } } + private void appendOrderedList(Node node) { + pushNode(node); + int ordinal = 0; + for (Node childNode : node.childNodes()) { + if (childNode.nameIs("li")) { + appendText("\n " + " ".repeat(listDepth) + ++ordinal + ". "); + appendChildren(childNode); + continue; + } + appendNode(childNode); + } + popNode(); + } + + private void appendChildren(Node node) { + if (node.childNodeSize() > 0) { + if (node.nameIs("table")) { + appendTable(node); + } else if (node.nameIs("ol")) { + appendOrderedList(node); + } else { + pushNode(node); + for (Node childNode : node.childNodes()) { + appendNode(childNode); + } + popNode(); + } + } + } + public void appendNode(Node node) { if (node instanceof TextNode n) { - appendText(normaliseWhitespace(n.getWholeText())); + appendText(preformatted ? n.getWholeText() : normaliseWhitespace(n.getWholeText())); } String name = node.nodeName(); @@ -399,16 +433,7 @@ public void appendNode(Node node) { appendImage(node); appendAutoLineBreak("\n"); } - case "li" -> { - int i = 0; - var n = node; - while (true) { - n = n.parent(); - if (n == null) break; - if (n.nameIs("li")) i++; - } - appendText("\n " + " ".repeat(Math.max(0, i)) + "\u2022 "); - } + case "li" -> appendText("\n " + " ".repeat(listDepth) + "\u2022 "); case "dt" -> appendText(" "); case "p" -> { var n = node.parent(); @@ -421,17 +446,7 @@ public void appendNode(Node node) { } } - if (node.childNodeSize() > 0) { - if ("table".equals(name)) { - appendTable(node); - } else { - pushNode(node); - for (Node childNode : node.childNodes()) { - appendNode(childNode); - } - popNode(); - } - } + appendChildren(node); switch (name) { case "br", "dd", "h1", "h2", "h3", "h4", "h5", "h6" -> appendAutoLineBreak("\n"); From 9dbe31871b59cbddfddae42c082d1bc9d007b37c Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 20 Feb 2026 11:57:15 +0800 Subject: [PATCH 77/77] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java index 8062243c80..a64a754ba8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -385,7 +385,7 @@ private void appendTable(Node table) { for (Element caption : captions) { appendAutoLineBreak("\n\n"); - appendText(normaliseWhitespace(caption.wholeText())); + appendChildren(caption); appendAutoLineBreak("\n"); } }