From 73b01cee815fc4784e3d56ff1f51210be08702a3 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 19 Nov 2025 20:00:03 +0800 Subject: [PATCH 01/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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" }