From 73b01cee815fc4784e3d56ff1f51210be08702a3 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 19 Nov 2025 20:00:03 +0800 Subject: [PATCH 001/123] =?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 8a15898173c..fd051d817aa 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 c62da41ee13..d757f3324de 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 a936f887be1..d06e5641514 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 5f74ba7d493..c0c54d94e22 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 e77fb259e16..c2adaeda33c 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 f884334c8ee..9ea7704a63c 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 4b60bf3cddf..70a46bdcebb 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 002/123] =?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 d757f3324de..c76c3098261 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 8b02ca9f859..544d6dac4a7 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 32f4abca599..a9dc9e83c47 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 5cb1a4403f2..8ff86eaac31 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 003/123] =?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 c76c3098261..dfbc0eae012 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 004/123] =?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 dfbc0eae012..04f8281f77d 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 544d6dac4a7..c559b8be303 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 a9dc9e83c47..488704e47f4 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 dd1ead57ed1..e01d171157a 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 71882ac1211..6487fdd1d9a 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 d4a5e9c68c5..83405f1f45d 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 26456b8f75c..5d52c51530c 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 005/123] =?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 f4e0bc6d41b..3284315d617 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 a7dd805c6b3..46df0a6a559 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 e83949930fb..da0e65465fc 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 e2684eb7735..da226856d70 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 006/123] 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 04f8281f77d..58905dd46fb 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 c559b8be303..0f38347b5a3 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 5d52c51530c..f66d9df3bcc 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 007/123] 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 f830043cdb0..f4344050ebd 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 008/123] 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 f4344050ebd..da9d15ecf91 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 009/123] 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 da9d15ecf91..e6f0df8f211 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 010/123] 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 c4233fcede9..a258656b978 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 011/123] 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 a7dcc2643d3..e45e412a2aa 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 2542d698f0d..2b56bc95737 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 28411edb0ab..d96cb7b9108 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 a258656b978..9777e367077 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 5b0ade9c98a..bcc814b39d4 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 00000000000..5bfe7ee9290 --- /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 f66d9df3bcc..aef1c3e65df 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 012/123] 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 c92acf202ce..6a4d70b5bf5 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 d96cb7b9108..9ab0ec1300c 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 9777e367077..802c1e15ec4 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 5bfe7ee9290..00000000000 --- 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 86ca2bde92f..d99f994f240 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 aef1c3e65df..4e266edbe23 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 7724d2e4cb0..826870e2665 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 013/123] =?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 6a4d70b5bf5..418f227efb7 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 d99f994f240..659865d1b6a 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 4e266edbe23..f1274db16f5 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 826870e2665..8894461e991 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 014/123] 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 d72871ec0ca..22f4fc1fcc4 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 015/123] 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 22f4fc1fcc4..26a67597f5e 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 016/123] 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 08ccf973b30..2bb5b7c1038 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 017/123] 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 2bb5b7c1038..ff3ec44fbdb 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 018/123] 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 3c714b1c1be..4781fc6251f 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 e45e412a2aa..f3165dd4f48 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 ff3ec44fbdb..2f064f12ff1 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 802c1e15ec4..7a3fb25c9d2 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 157fbbc1e12..87c096f666f 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 f0fdcefc9f9..f8bf1dd8338 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 88c7f72ecb4..3ba922175e0 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 4c04e702c2e..1131f7a37de 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 00dfe6f1593..239762c9b64 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 9c66eea457f..86671001346 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 189da4c43df..05951845ed1 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 2bbfda95717..993a6b6f16c 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 019/123] 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 f1274db16f5..b7dd3b7ebf8 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 020/123] 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 36fbc24a383..9a5683bb141 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 021/123] 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 9a5683bb141..98f3ae8f754 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 7a3fb25c9d2..21bc53d90c5 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 d778aacab98..9c102530508 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 8e74282ac68..601aa2caca6 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 db38695d894..980c3c946fb 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 68bc99f27db..99a69317b81 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 4e78fdf1ebf..4c88dc73efc 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 17fa882d393..4bcc60e2532 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 2b35dd4f102..5a54e0e6383 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 022/123] 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 21bc53d90c5..8e39712cbf4 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 87c096f666f..09e19e02a69 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 023/123] 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 f3165dd4f48..5d86029a6e7 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 a70d87b6eb1..b9c2b08989e 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 48ca31f8aeb..246b861fbda 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 c79c3660e8c..2548dcb28d3 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 024/123] 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 cd0b11146a9..b71512aa6bd 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 7d8fbc09540..ccc472dd8fe 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 f2e83551394..627bcfc50b1 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 025/123] 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 efa07f1a6ed..d0fbd697a58 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 026/123] 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 fd051d817aa..9a7bc9aee8c 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 b71512aa6bd..a4691b0ce5e 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 8e39712cbf4..dbbe1d1cace 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 a54600c402c..8d1f18e4b8f 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 ccc472dd8fe..18a998ed592 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 627bcfc50b1..e05944b8ebf 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 027/123] 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 dbbe1d1cace..4242c75d7c1 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 028/123] 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 9becc133001..aee253639ab 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 029/123] 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 7cf2b72f054..0dedd21a2e4 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 a4691b0ce5e..8bd0800f96f 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 aee253639ab..7cbbd162520 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 09e19e02a69..abc145b5b06 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 030/123] =?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 8bd0800f96f..1a11280d7be 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 031/123] 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 b7dd3b7ebf8..b60603798d7 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 032/123] 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 7cbbd162520..9c4745e20ed 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 033/123] 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 1a11280d7be..2fd319bdaf1 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 9c4745e20ed..8783db14529 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 034/123] 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 2fd319bdaf1..985b703454b 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 8783db14529..ee2207da0c7 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 f60c3a5c6ba..73d6a3aac13 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 e0cf4fee8e8..9b1ddca4c70 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 0d78df40b60..187db503758 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 1ba67750c85..60098a6ee72 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 e96121909dc..978448e0cbe 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 11b2917ae9e..b3213d149c8 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 c075daf2399..e682d086623 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 035/123] 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 ee2207da0c7..012ade3e842 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 036/123] =?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 012ade3e842..906dc56cce4 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 73d6a3aac13..4dbdb7c79bf 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 b3213d149c8..91d77778d08 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 e682d086623..c5b1b0e9c42 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 037/123] 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 985b703454b..20128bf55c2 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 906dc56cce4..17d8a164d6c 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 038/123] 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 5d86029a6e7..edb6574888d 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 039/123] 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 20128bf55c2..15fae839ae6 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 17d8a164d6c..0b8a08603ea 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 b60603798d7..ec49bd51282 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 c3e9f31aedc..0bf789174be 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 040/123] 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 edb6574888d..4749837fdd6 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 041/123] 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 4749837fdd6..d9156cb225c 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 ec49bd51282..fc6e03777ee 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 042/123] 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 d9156cb225c..806dfbf9b4f 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 9386994d4ae..f86bcb45077 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 043/123] 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 9a7bc9aee8c..a5a5cdd793c 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 275b0bc132d..118c0eb674f 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 15fae839ae6..1183495c87b 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 0b8a08603ea..9924fc4fde2 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 8d1f18e4b8f..6f267d15a21 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 39fef7539f5..55a901ee1ca 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 309c193637d..b1111e6a924 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 044/123] 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 806dfbf9b4f..54179f3deba 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 659865d1b6a..700cd4c328d 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 fc6e03777ee..efc66a16dee 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 0bf789174be..cda6d0157a8 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 045/123] 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 54179f3deba..4bacf603b3e 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 efc66a16dee..4bca951071b 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 046/123] 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 4bacf603b3e..effc835fd8f 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 047/123] 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 effc835fd8f..aff7013272c 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 048/123] 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 aff7013272c..aaa0937e4a5 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 049/123] 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 aaa0937e4a5..528956ee6d8 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 050/123] 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 528956ee6d8..d7317638eac 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 051/123] 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 a5a5cdd793c..bf8a3fd8275 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 118c0eb674f..a284bf70077 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 d7317638eac..ec8eb264170 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 1183495c87b..a0d3967fe59 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 6f267d15a21..fd18970adf7 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 55a901ee1ca..222e18cb400 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 b1111e6a924..4f96c1ade9e 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 4bca951071b..4d03c0abfc4 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 052/123] 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 ec8eb264170..dce6406b98d 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 4d03c0abfc4..1c82f17ecde 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 053/123] 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 a0d3967fe59..fe83a676a02 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 9924fc4fde2..4bec602c979 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 054/123] 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 1c82f17ecde..ca1010e72cd 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 055/123] 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 dce6406b98d..5d0146c9c07 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 fe83a676a02..4384b0654b1 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 056/123] =?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 5d0146c9c07..65e19c0b5e8 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 057/123] 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 65e19c0b5e8..d01ef3bb730 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 058/123] 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 4384b0654b1..372cdfaaa82 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 059/123] 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 d01ef3bb730..ee5e0fb5b58 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 060/123] 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 ee5e0fb5b58..30bd41590cb 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 061/123] 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 30bd41590cb..e3cff318b8d 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 062/123] 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 e1b6eafc595..2d127f367a5 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 063/123] 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 2d127f367a5..6c0f029d37b 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 064/123] 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 222e18cb400..d5c397f3d1e 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 065/123] 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 e5a97f9da1e..087cf842203 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 4bec602c979..f0a380d83e5 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 066/123] 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 087cf842203..8543a5e634c 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 f0a380d83e5..40b06119fae 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 067/123] 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 e3cff318b8d..5e4386d0f92 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 068/123] 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 40b06119fae..cfd0f1e1102 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 069/123] 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 700cd4c328d..cc66b3af4f7 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 ba0eb29079e..09ade5aeffd 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 070/123] 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 cfd0f1e1102..0789f70b0f9 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 071/123] 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 20d5d09491a..fc9a31e34ab 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 072/123] 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 5e4386d0f92..6eb51dfb3b0 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 fc9a31e34ab..fb95ed8ce3c 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 073/123] 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 6eb51dfb3b0..1de9f455a63 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 074/123] 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 1de9f455a63..56d88b7fbdd 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 f55008ca117..66cb93d132a 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 075/123] 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 56d88b7fbdd..ab064819389 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 076/123] 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 ab064819389..8062243c80a 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 077/123] 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 8062243c80a..a64a754ba8e 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"); } } From 9e79b065c83b1aec84f20ffb7cf19c076359ecf6 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 25 Feb 2026 10:51:17 +0800 Subject: [PATCH 078/123] =?UTF-8?q?=E4=BC=98=E5=8C=96UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 1 + .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 1 + HMCL/src/main/resources/assets/css/root.css | 4 +++- 3 files changed, 5 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 36612172c23..114955c6a0d 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 @@ -616,6 +616,7 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo ScrollPane scrollPane = new ScrollPane(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); scrollPane.setFitToWidth(true); + FXUtils.setOverflowHidden(scrollPane, 8); FXUtils.smoothScrolling(scrollPane); VBox.setVgrow(scrollPane, Priority.SOMETIMES); 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 0789f70b0f9..a20bb6ade20 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.setOverflowHidden(scrollPane, 8); loadChangelog(object, spinnerPane, scrollPane); spinnerPane.setOnFailedAction(e -> loadChangelog(object, spinnerPane, scrollPane)); diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index c5526ff4ba3..80fab493c1c 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -1998,6 +1998,7 @@ -fx-padding: 10; -fx-font-size: 12; -fx-text-fill: -monet-on-surface; + -fx-line-spacing: 2; } .addon-changelog .html-h1 { @@ -2019,7 +2020,8 @@ .addon-changelog .html-code-block { -fx-padding: 0 3 0 3; -fx-background-radius: 5; - -fx-background-color: -monet-surface-dim; + /* Should be -monet-surface-bright in dark mode and -dim in light mode, however that's not supported */ + -fx-background-color: -monet-surface-container; } .addon-changelog-table-cell { From 8bab4a9a7cf89682c638a2d8dcc088084ac9a13d Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 13 Mar 2026 16:53:18 +0800 Subject: [PATCH 079/123] 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 4f1a3050cf1..865b5060ab0 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 @@ -459,7 +459,7 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP 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")); + JFXHyperlink changelogButton = new JFXHyperlink(i18n("mods.changelog")); changelogButton.getStyleClass().add("dialog-accept"); loadChangelog(version, selfPage.repository, changelogButton); @@ -567,7 +567,7 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, }).start(); } - private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, Button changelogButton) { + private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink changelogButton) { changelogButton.setDisable(true); Task.supplyAsync(() -> { if (changelogCache.containsKey(version)) { From 917d7d958650834d9cbd1fcd7ff12b7575b4a187 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 30 Mar 2026 22:17:19 +0800 Subject: [PATCH 080/123] update --- .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 3 +-- 1 file changed, 1 insertion(+), 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 f84332d174b..bc4027ecadb 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 @@ -491,7 +491,7 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP box.getChildren().setAll(addonItem); JFXHyperlink changelogButton = new JFXHyperlink(i18n("mods.changelog")); - changelogButton.getStyleClass().add("dialog-accept"); + changelogButton.setDisable(true); loadChangelog(version, selfPage.repository, changelogButton); SpinnerPane spinnerPane = new SpinnerPane(); @@ -599,7 +599,6 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, } private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink changelogButton) { - changelogButton.setDisable(true); Task.supplyAsync(() -> { if (changelogCache.containsKey(version)) { return Optional.ofNullable(changelogCache.get(version)); From 33a5fea4390ec0f7f105fb4608cd3e79126cf442 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 3 Apr 2026 21:33:51 +0800 Subject: [PATCH 081/123] apply suggestions --- .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 2 +- .../java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java | 2 +- 2 files 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 bc4027ecadb..ab77f1fcb41 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 @@ -62,7 +62,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DownloadPage extends Control implements DecoratorPage { - private static final WeakHashMap changelogCache = new WeakHashMap<>(); + private static final Map changelogCache = Collections.synchronizedMap(new WeakHashMap<>()); private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final BooleanProperty loaded = new SimpleBooleanProperty(false); 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 a20bb6ade20..fa0baaf16a0 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 cell.getStyleClass().add("addon-changelog-table-cell"); cell.setOnMouseClicked(event -> { List items = cell.getTableColumn().getTableView().getItems(); - if (cell.getIndex() >= items.size()) { + if (cell.getIndex() >= items.size() || cell.getIndex() < 0) { return; } ModUpdateObject object = items.get(cell.getIndex()); From e5c49c85567f78946847494118e95553698efdbb Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 4 Apr 2026 17:49:24 +0800 Subject: [PATCH 082/123] update code block --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 17 ++++++++--------- .../hmcl/ui/versions/DownloadPage.java | 1 + .../hmcl/ui/versions/ModUpdatesPage.java | 1 + 3 files changed, 10 insertions(+), 9 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 a64a754ba8e..90b10611a4c 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.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.ImageView; -import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import org.jackhuang.hmcl.task.Schedulers; @@ -209,10 +207,9 @@ private void appendText(String text) { Text textNode = new Text(text); applyStyle(textNode); if (code) { - var block = new VBox(textNode); - block.setAlignment(Pos.CENTER); - block.getStyleClass().add("html-code-block"); - children.add(block); + var codeFlow = new TextFlow(textNode); + codeFlow.getStyleClass().add("html-code-block"); + children.add(codeFlow); } else { children.add(textNode); } @@ -505,12 +502,14 @@ public TextFlow render() { textFlow.getChildren().setAll(children); for (javafx.scene.Node node : children) { if (node instanceof ImageView img) { - InvalidationListener i = __ -> + InvalidationListener listener = __ -> img.setFitWidth(Math.min(textFlow.getWidth() - 20D, img.getImage() == null ? 0D : img.getImage().getWidth())); - textFlow.widthProperty().addListener(i); - img.imageProperty().addListener(i); + textFlow.widthProperty().addListener(listener); + img.imageProperty().addListener(listener); } else if (node instanceof TableView table) { table.prefWidthProperty().bind(textFlow.widthProperty().add(-20D)); + } else if (node instanceof TextFlow codeFlow) { + codeFlow.maxWidthProperty().bind(textFlow.widthProperty().add(-20D)); } } 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 04abc17bfff..f156f1a9e3b 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 @@ -646,6 +646,7 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo ScrollPane scrollPane = new ScrollPane(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); scrollPane.setFitToWidth(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); FXUtils.setOverflowHidden(scrollPane, 8); FXUtils.smoothScrolling(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 fa0baaf16a0..eb0cb75d91e 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); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); FXUtils.setOverflowHidden(scrollPane, 8); loadChangelog(object, spinnerPane, scrollPane); From cdb2a6c830ef36f5abd802bb873cd1948adc6f45 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 4 Apr 2026 18:12:57 +0800 Subject: [PATCH 083/123] fix html detector --- .../org/jackhuang/hmcl/util/StringUtils.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 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 fb95ed8ce3c..676d01531c7 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.nodes.Node; import org.jsoup.safety.Safelist; import java.io.PrintWriter; @@ -604,17 +605,7 @@ public static Optional nullIfBlank(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, all)) { - 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 Set mdParsableTags = Set.of("#text", "img"); private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().extensions(List.of( InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create() @@ -626,7 +617,10 @@ public static boolean isHtml(String str) { public static String convertToHtml(String md) { if (md == null) return null; - if (isHtml(md)) return md; + if (isBlank(md)) return ""; + if (md.startsWith("") || md.startsWith("") || md.startsWith("") + || (Jsoup.isValid(md, all) && !Jsoup.parse(md).body().childNodes().stream().map(Node::normalName).allMatch(mdParsableTags::contains))) + return md; return HTML_RENDERER.render(MD_PARSER.parse(md)); } From 3e1fbe22fbe665788cb660d1ded82a8d81f23235 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 4 Apr 2026 18:43:43 +0800 Subject: [PATCH 084/123] enhance table --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 52 ++++++++++++------- HMCL/src/main/resources/assets/css/root.css | 4 ++ 2 files changed, 36 insertions(+), 20 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 90b10611a4c..9082ce1ea68 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -18,12 +18,14 @@ package org.jackhuang.hmcl.ui; import javafx.beans.InvalidationListener; -import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.SimpleObjectProperty; 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.ImageView; +import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import org.jackhuang.hmcl.task.Schedulers; @@ -284,9 +286,9 @@ private void appendTable(Node table) { var childElements = ((Element) table).children(); List captions = new ArrayList<>(); - List head = new ArrayList<>(); - List> body = new ArrayList<>(); - List foot = new ArrayList<>(); + List head = new ArrayList<>(); + List> body = new ArrayList<>(); + List foot = new ArrayList<>(); boolean hasHead = false; boolean hasBody = false; @@ -304,13 +306,12 @@ private void appendTable(Node table) { 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()); + head.add(e); } } columnCount = Math.max(columnCount, head.size()); @@ -321,9 +322,8 @@ private void appendTable(Node table) { body.clear(); for (Element e : child.children()) { if (e.nameIs("tr")) { - List row = e.children().stream() + 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); @@ -339,22 +339,20 @@ private void appendTable(Node table) { 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()); + foot.add(e); } } columnCount = Math.max(columnCount, foot.size()); } case "tr" -> { if (hasBody) continue; - List row = child.children().stream() + 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); @@ -362,19 +360,31 @@ private void appendTable(Node table) { } } - List> rows = new ArrayList<>(hasFoot ? body.size() + 1 : body.size()); - for (List row : body) - rows.add(Lang.copyWithSize(row, columnCount, "")); + head = Lang.copyWithSize(head, columnCount, null); + + List> rows = new ArrayList<>(hasFoot ? body.size() + 1 : body.size()); + for (List row : body) + rows.add(Lang.copyWithSize(row, columnCount, null)); if (hasFoot) - rows.add(Lang.copyWithSize(foot, columnCount, "")); + rows.add(Lang.copyWithSize(foot, columnCount, null)); - TableView> tableView = new TableView<>(FXCollections.observableList(rows)); + TableView> tableView = new TableView<>(FXCollections.observableList(rows)); tableView.setFixedCellSize(25); tableView.setPrefHeight(25 * (rows.size() + 1) + 5); for (int i = 0; i < columnCount; i++) { int finalI = i; - TableColumn, String> c = new TableColumn<>(head.size() > i ? head.get(i) : ""); - c.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().get(finalI))); + TableColumn, javafx.scene.Node> c = new TableColumn<>(); + Element e = head.get(i); + if (e != null) { + var box = new VBox(HTMLRenderer.openHyperlinkInBrowser().appendNode(e).render()); + box.setAlignment(Pos.CENTER_LEFT); + c.setGraphic(box); + } + c.setCellValueFactory(param -> { + Element el = param.getValue().get(finalI); + if (el == null) return new SimpleObjectProperty<>(); + return new SimpleObjectProperty<>(HTMLRenderer.openHyperlinkInBrowser().appendNode(el).render()); + }); tableView.getColumns().add(c); } @@ -417,7 +427,7 @@ private void appendChildren(Node node) { } } - public void appendNode(Node node) { + public HTMLRenderer appendNode(Node node) { if (node instanceof TextNode n) { appendText(preformatted ? n.getWholeText() : normaliseWhitespace(n.getWholeText())); } @@ -453,6 +463,8 @@ public void appendNode(Node node) { appendAutoLineBreak("\n"); } } + + return this; } private static boolean isSpacing(String text) { diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 60dda0499f7..02ff8c0e99a 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -1968,6 +1968,10 @@ -fx-line-spacing: 2; } +.addon-changelog .html { + -fx-font-size: 12; +} + .addon-changelog .html-h1 { -fx-font-size: 16.5; } From b62d8e313201268696fb117f3ff814f1d778145b Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 5 Apr 2026 14:10:24 +0800 Subject: [PATCH 085/123] update css --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 25 ++++++++++++++----- HMCLCore/build.gradle.kts | 1 + gradle/libs.versions.toml | 4 ++- 3 files changed, 23 insertions(+), 7 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 9082ce1ea68..791e2b1d9f6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -17,6 +17,9 @@ */ package org.jackhuang.hmcl.ui; +import com.helger.css.decl.CSSDeclaration; +import com.helger.css.decl.CSSDeclarationList; +import com.helger.css.reader.CSSReaderDeclarationList; import javafx.beans.InvalidationListener; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; @@ -38,6 +41,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import static org.jackhuang.hmcl.setting.ConfigHolder.config; @@ -48,6 +52,11 @@ */ public final class HTMLRenderer { + private static final Map cssPropertyMapping = Map.of( + "color", "-fx-fill", + "font-size", "-fx-font-size" // And more + ); + private static URI resolveLink(Node linkNode) { String href = linkNode.absUrl("href"); if (href.isEmpty()) @@ -148,12 +157,16 @@ private void updateStyle() { String style = node.attr("style"); if (StringUtils.isNotBlank(style)) { - fxStyle = StringUtils.addSuffix( - style - .replace("color:", "-fx-fill:") - .replace("font-size:", "-fx-font-size:"), // And more - ";" - ); + var sBuilder = fxStyle != null ? new StringBuilder(fxStyle) : new StringBuilder(); + CSSDeclarationList cssDeclarations = CSSReaderDeclarationList.readFromString(style); + if (cssDeclarations != null) { + List declarations = cssDeclarations.getAllDeclarations(); + declarations.forEach(d -> { + d.setProperty(cssPropertyMapping.getOrDefault(d.getProperty(), d.getProperty())); + sBuilder.append(d.getAsCSSString()).append(";"); + }); + } + fxStyle = sBuilder.toString(); } } } diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index 9d1590684c7..d4d1715aaef 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { api(libs.commonmark.underline) api(libs.commonmark.strikethrough) api(libs.commonmark.table) + api(libs.ph.css) compileOnlyApi(libs.jetbrains.annotations) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ee569f533aa..351d70bf51e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,8 @@ lwjgl-unsafe-agent = "2.0" monet-fx = "0.4.0" terracotta = "0.4.2" nayuki-qrcodegen = "1.8.0" -commonmark = "0.27.1" +commonmark = "0.28.0" +ph-css = "8.1.1" # testing junit = "6.0.1" @@ -63,6 +64,7 @@ commonmark-autolink = { module = "org.commonmark:commonmark-ext-autolink", versi commonmark-underline = { module = "org.commonmark:commonmark-ext-ins", 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" } +ph-css = { module = "com.helger:ph-css", version.ref = "ph-css" } # testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } From 571ab99783631924d5ada7f01024c824080f51e2 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 5 Apr 2026 14:12:47 +0800 Subject: [PATCH 086/123] Revert "update css" This reverts commit b62d8e313201268696fb117f3ff814f1d778145b. --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 25 +++++-------------- HMCLCore/build.gradle.kts | 1 - gradle/libs.versions.toml | 4 +-- 3 files changed, 7 insertions(+), 23 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 791e2b1d9f6..9082ce1ea68 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -17,9 +17,6 @@ */ package org.jackhuang.hmcl.ui; -import com.helger.css.decl.CSSDeclaration; -import com.helger.css.decl.CSSDeclarationList; -import com.helger.css.reader.CSSReaderDeclarationList; import javafx.beans.InvalidationListener; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; @@ -41,7 +38,6 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.function.Consumer; import static org.jackhuang.hmcl.setting.ConfigHolder.config; @@ -52,11 +48,6 @@ */ public final class HTMLRenderer { - private static final Map cssPropertyMapping = Map.of( - "color", "-fx-fill", - "font-size", "-fx-font-size" // And more - ); - private static URI resolveLink(Node linkNode) { String href = linkNode.absUrl("href"); if (href.isEmpty()) @@ -157,16 +148,12 @@ private void updateStyle() { String style = node.attr("style"); if (StringUtils.isNotBlank(style)) { - var sBuilder = fxStyle != null ? new StringBuilder(fxStyle) : new StringBuilder(); - CSSDeclarationList cssDeclarations = CSSReaderDeclarationList.readFromString(style); - if (cssDeclarations != null) { - List declarations = cssDeclarations.getAllDeclarations(); - declarations.forEach(d -> { - d.setProperty(cssPropertyMapping.getOrDefault(d.getProperty(), d.getProperty())); - sBuilder.append(d.getAsCSSString()).append(";"); - }); - } - fxStyle = sBuilder.toString(); + fxStyle = StringUtils.addSuffix( + style + .replace("color:", "-fx-fill:") + .replace("font-size:", "-fx-font-size:"), // And more + ";" + ); } } } diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index d4d1715aaef..9d1590684c7 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -32,7 +32,6 @@ dependencies { api(libs.commonmark.underline) api(libs.commonmark.strikethrough) api(libs.commonmark.table) - api(libs.ph.css) compileOnlyApi(libs.jetbrains.annotations) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 351d70bf51e..ee569f533aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,8 +23,7 @@ lwjgl-unsafe-agent = "2.0" monet-fx = "0.4.0" terracotta = "0.4.2" nayuki-qrcodegen = "1.8.0" -commonmark = "0.28.0" -ph-css = "8.1.1" +commonmark = "0.27.1" # testing junit = "6.0.1" @@ -64,7 +63,6 @@ commonmark-autolink = { module = "org.commonmark:commonmark-ext-autolink", versi commonmark-underline = { module = "org.commonmark:commonmark-ext-ins", 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" } -ph-css = { module = "com.helger:ph-css", version.ref = "ph-css" } # testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } From 96e07cfa998ab7f9c6044f3914668225d3837718 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 5 Apr 2026 14:15:14 +0800 Subject: [PATCH 087/123] update css --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 6 ++++-- 1 file changed, 4 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 9082ce1ea68..de532b42b44 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -131,6 +131,7 @@ private void updateStyle() { hyperlink = null; fxStyle = null; + var styleBuilder = new StringBuilder(); for (Node node : stack) { String nodeName = node.nodeName(); switch (nodeName) { @@ -148,14 +149,15 @@ private void updateStyle() { String style = node.attr("style"); if (StringUtils.isNotBlank(style)) { - fxStyle = StringUtils.addSuffix( + styleBuilder.append(StringUtils.addSuffix( style .replace("color:", "-fx-fill:") .replace("font-size:", "-fx-font-size:"), // And more ";" - ); + )); } } + fxStyle = styleBuilder.toString(); } private void pushNode(Node node) { From f156e88767b11d55019f5e35407fc44506950edc Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 5 Apr 2026 23:08:17 +0800 Subject: [PATCH 088/123] remove optional & cache --- .../hmcl/ui/versions/DownloadPage.java | 20 +++++-------------- .../hmcl/ui/versions/ModUpdatesPage.java | 10 +++------- .../org/jackhuang/hmcl/mod/RemoteMod.java | 8 +------- .../jackhuang/hmcl/mod/curse/CurseAddon.java | 1 - .../modrinth/ModrinthRemoteModRepository.java | 1 - .../org/jackhuang/hmcl/util/StringUtils.java | 6 ------ 6 files changed, 9 insertions(+), 37 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 f156f1a9e3b..336376cd456 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 @@ -62,7 +62,6 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DownloadPage extends Control implements DecoratorPage { - private static final Map changelogCache = Collections.synchronizedMap(new WeakHashMap<>()); private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final BooleanProperty loaded = new SimpleBooleanProperty(false); @@ -599,23 +598,14 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, } private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink changelogButton) { - Task.supplyAsync(() -> { - if (changelogCache.containsKey(version)) { - return Optional.ofNullable(changelogCache.get(version)); - } else if (version.getChangelog() != null) { - return StringUtils.nullIfBlank(version.getChangelog()).map(StringUtils::convertToHtml); - } else { - return StringUtils.nullIfBlank(repo.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::convertToHtml); - } - }).whenComplete(Schedulers.javafx(), (result, exception) -> { + Task.supplyAsync(() -> + StringUtils.convertToHtml(repo.getModChangelog(version.getModid(), version.getVersionId())) + ).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - if (result.isPresent()) { - String s = result.get(); - changelogCache.put(version, s); + if (StringUtils.isNotBlank(result)) { changelogButton.setDisable(false); - changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(version, s, repo))); + changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(version, result, repo))); } else { - changelogCache.put(version, null); changelogButton.setOnAction(null); } } else { 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 eb0cb75d91e..97ebb58f417 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 @@ -55,7 +55,6 @@ 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; @@ -346,16 +345,13 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro spinnerPane.setLoading(true); Task.supplyAsync(() -> { if (object.changelog != null) { - return Optional.of(object.changelog); + return object.changelog; } RemoteMod.Version version = object.data.getCandidate(); - if (version.getChangelog() != null) { - return StringUtils.nullIfBlank(version.getChangelog()).map(StringUtils::convertToHtml); - } - return StringUtils.nullIfBlank(repository.getModChangelog(version.getModid(), version.getVersionId())).map(StringUtils::convertToHtml); + return StringUtils.convertToHtml(repository.getModChangelog(version.getModid(), version.getVersionId())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - object.changelog = result.orElse(i18n("mods.changelog.empty")); + object.changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, object.data.getRepository().getBaseUrl())); FXUtils.smoothScrolling(scrollPane); spinnerPane.setFailedReason(null); 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 2c5d321e8eb..503f1df4d42 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -218,7 +218,6 @@ public static class Version { private final String modid; private final String name; private final String version; - private final String changelog; private final Instant datePublished; private final VersionType versionType; private final File file; @@ -226,13 +225,12 @@ public static class Version { private final List gameVersions; private final 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) { + public Version(IVersion self, String versionId, String modid, String name, String version, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { this.versionId = versionId; this.self = self; this.modid = modid; this.name = name; this.version = version; - this.changelog = changelog; this.datePublished = datePublished; this.versionType = versionType; this.file = file; @@ -261,10 +259,6 @@ public String getVersion() { return version; } - public String getChangelog() { - return changelog; - } - public Instant getDatePublished() { return datePublished; } 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 6d9b0add56f..816888114b5 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 @@ -576,7 +576,6 @@ public RemoteMod.Version toVersion() { Integer.toString(modId), getDisplayName(), getFileName(), - null, getFileDate(), versionType, new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), 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 8213264c8b9..f336d40d84e 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 @@ -630,7 +630,6 @@ public Optional toVersion() { projectId, name, versionNumber, - changelog, datePublished, type, files.get(0).toFile(), 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 676d01531c7..03277db36e1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -23,7 +23,6 @@ import org.commonmark.ext.ins.InsExtension; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; -import org.jetbrains.annotations.Contract; import org.jsoup.Jsoup; import org.jsoup.nodes.Node; import org.jsoup.safety.Safelist; @@ -597,11 +596,6 @@ public static boolean isAlphabeticOrNumber(String str) { return true; } - @Contract(pure = true) - public static Optional nullIfBlank(String str) { - return Optional.ofNullable(str).filter(s -> !s.isBlank()); - } - private static final Safelist all = Safelist.relaxed() .addAttributes("a", "rel", "target"); From a06d42eaffb95b11a66d048da75d5c67455d515e Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 6 Apr 2026 12:55:44 +0800 Subject: [PATCH 089/123] update --- .../org/jackhuang/hmcl/ui/Controllers.java | 19 +++++++++++++++ .../java/org/jackhuang/hmcl/ui/FXUtils.java | 24 +------------------ .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 11 ++++----- .../java/org/jackhuang/hmcl/ui/WebPage.java | 2 +- .../hmcl/ui/versions/DownloadPage.java | 2 +- .../hmcl/ui/versions/ModUpdatesPage.java | 2 +- 6 files changed, 27 insertions(+), 33 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 17545849487..04b27324b16 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -73,6 +73,7 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.net.URI; import java.nio.file.Path; import java.time.LocalDate; import java.util.List; @@ -623,6 +624,24 @@ public static void onHyperlinkAction(String href) { } } + 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"), () -> FXUtils.copyText(uri)) + .yesOrNo(() -> FXUtils.openLink(uri), null) + .build(); + dialog(dialog); + } + public static boolean isStopped() { return decorator == null; } 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 c8e857c102c..bc5ae0a4000 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -70,7 +70,6 @@ import org.jackhuang.hmcl.ui.animation.Motion; 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; @@ -1683,29 +1682,8 @@ public static void useJFXContextMenu(TextInputControl control) { } public static TextFlow renderAddonChangelog(String changelogHtml, String baseUri) { - HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); - renderer.appendNode(Jsoup.parse(changelogHtml, baseUri)); - renderer.mergeLineBreaks(); - var textFlow = renderer.render(); + var textFlow = new HTMLRenderer(Controllers::openUriInBrowser).appendNode(Jsoup.parse(changelogHtml, baseUri)).mergeLineBreaks().render(); 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 de532b42b44..5865e185e80 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -60,10 +60,6 @@ private static URI resolveLink(Node linkNode) { } } - public static HTMLRenderer openHyperlinkInBrowser() { - return new HTMLRenderer(FXUtils::openUriInBrowser); - } - /// @see org.jsoup.internal.StringUtil#isWhitespace(int) public static boolean isWhitespace(int c) { return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r'; @@ -378,14 +374,14 @@ private void appendTable(Node table) { TableColumn, javafx.scene.Node> c = new TableColumn<>(); Element e = head.get(i); if (e != null) { - var box = new VBox(HTMLRenderer.openHyperlinkInBrowser().appendNode(e).render()); + var box = new VBox(new HTMLRenderer(Controllers::openUriInBrowser).appendNode(e).render()); box.setAlignment(Pos.CENTER_LEFT); c.setGraphic(box); } c.setCellValueFactory(param -> { Element el = param.getValue().get(finalI); if (el == null) return new SimpleObjectProperty<>(); - return new SimpleObjectProperty<>(HTMLRenderer.openHyperlinkInBrowser().appendNode(el).render()); + return new SimpleObjectProperty<>(new HTMLRenderer(Controllers::openUriInBrowser).appendNode(el).render()); }); tableView.getColumns().add(c); } @@ -481,7 +477,7 @@ private static boolean isSpacing(String text) { return true; } - public void mergeLineBreaks() { + public HTMLRenderer mergeLineBreaks() { for (int i = 0; i < this.children.size(); i++) { javafx.scene.Node child = this.children.get(i); if (child instanceof AutoLineBreak) { @@ -508,6 +504,7 @@ public void mergeLineBreaks() { } } } + return this; } public TextFlow render() { 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 2b56bc95737..92880c460e4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebPage.java @@ -47,7 +47,7 @@ public WebPage(String title, String content) { Task.supplyAsync(() -> { Document document = Jsoup.parseBodyFragment(content); - HTMLRenderer renderer = HTMLRenderer.openHyperlinkInBrowser(); + HTMLRenderer renderer = new HTMLRenderer(Controllers::openUriInBrowser); 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 336376cd456..fe8c9a9faae 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 @@ -618,7 +618,7 @@ private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository r Task.supplyAsync(() -> repo.getVersionPageUrl(version)) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null && StringUtils.isNotBlank(result)) { - button.setOnAction(__ -> FXUtils.openUriInBrowser(result)); + button.setOnAction(__ -> Controllers.openUriInBrowser(result)); button.setDisable(false); } }) 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 97ebb58f417..cb9e106cb67 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 @@ -366,7 +366,7 @@ 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.setOnAction(__ -> Controllers.openUriInBrowser(result)); button.setDisable(false); } }) From 531776acac6678ac2758111e77e62a0090ef6b2a Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 6 Apr 2026 13:31:36 +0800 Subject: [PATCH 090/123] load changelog later --- .../hmcl/ui/versions/DownloadPage.java | 55 ++++++++++--------- .../hmcl/ui/versions/ModUpdatesPage.java | 8 +-- 2 files changed, 31 insertions(+), 32 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 fe8c9a9faae..33c70803afd 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 @@ -489,10 +489,6 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP addonItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again box.getChildren().setAll(addonItem); - JFXHyperlink changelogButton = new JFXHyperlink(i18n("mods.changelog")); - changelogButton.setDisable(true); - loadChangelog(version, selfPage.repository, changelogButton); - SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); ComponentList dependenciesList = new ComponentList(); @@ -510,6 +506,9 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP this.setBody(box); + JFXHyperlink changelogButton = new JFXHyperlink(i18n("mods.changelog")); + changelogButton.setOnAction(__ -> Controllers.dialog(new AddonChangelog(version, selfPage.repository))); + JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); versionPageBtn.setDisable(true); loadVersionPageUrl(version, selfPage.repository, versionPageBtn); @@ -597,23 +596,6 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, }).start(); } - private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink changelogButton) { - Task.supplyAsync(() -> - StringUtils.convertToHtml(repo.getModChangelog(version.getModid(), version.getVersionId())) - ).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - if (StringUtils.isNotBlank(result)) { - changelogButton.setDisable(false); - changelogButton.setOnAction(e -> Controllers.dialog(new AddonChangelog(version, result, repo))); - } else { - changelogButton.setOnAction(null); - } - } else { - changelogButton.setOnAction(null); - } - }).start(); - } - private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink button) { Task.supplyAsync(() -> repo.getVersionPageUrl(version)) .whenComplete(Schedulers.javafx(), (result, exception) -> { @@ -628,20 +610,24 @@ private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository r private static final class AddonChangelog extends JFXDialogLayout { - public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepository repo) { + public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo) { setHeading(new HBox(new Label(i18n("mods.changelog") + " - " + version.getName()))); VBox box = new VBox(8); box.setPadding(new Insets(8)); - ScrollPane scrollPane = new ScrollPane(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); + SpinnerPane spinnerPane = new SpinnerPane(); + ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToWidth(true); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); FXUtils.setOverflowHidden(scrollPane, 8); - FXUtils.smoothScrolling(scrollPane); - VBox.setVgrow(scrollPane, Priority.SOMETIMES); - box.getChildren().add(scrollPane); + loadChangelog(version, repo, spinnerPane, scrollPane); + spinnerPane.setOnFailedAction(e -> loadChangelog(version, repo, spinnerPane, scrollPane)); + + spinnerPane.setContent(scrollPane); + VBox.setVgrow(spinnerPane, Priority.SOMETIMES); + box.getChildren().add(spinnerPane); this.setBody(box); @@ -656,6 +642,23 @@ public AddonChangelog(RemoteMod.Version version, String changelog, RemoteModRepo onEscPressed(this, closeButton::fire); } + + private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, SpinnerPane spinnerPane, ScrollPane scrollPane) { + spinnerPane.setLoading(true); + Task.supplyAsync(() -> + StringUtils.convertToHtml(repo.getModChangelog(version.getModid(), version.getVersionId())) + ).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + String changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); + scrollPane.setContent(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); + FXUtils.smoothScrolling(scrollPane); + spinnerPane.setFailedReason(null); + } else { + spinnerPane.setFailedReason(i18n("download.failed.refresh")); + } + spinnerPane.setLoading(false); + }).start(); + } } @FunctionalInterface 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 cb9e106cb67..2fa78ba509a 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.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; @@ -299,10 +298,7 @@ public void setSource(String source) { private static final class ModChangelog extends JFXDialogLayout { - private final RemoteModRepository repository; - public ModChangelog(ModUpdateObject object) { - this.repository = object.data.getRepository(); RemoteMod.Version targetVersion = object.data.getCandidate(); this.setHeading(new HBox(new Label(i18n("mods.changelog") + " - " + targetVersion.getName()))); @@ -348,7 +344,7 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro return object.changelog; } RemoteMod.Version version = object.data.getCandidate(); - return StringUtils.convertToHtml(repository.getModChangelog(version.getModid(), version.getVersionId())); + return StringUtils.convertToHtml(object.data.getRepository().getModChangelog(version.getModid(), version.getVersionId())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { object.changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); @@ -363,7 +359,7 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro } private void loadVersionPageUrl(ModUpdateObject object, JFXHyperlink button) { - Task.supplyAsync(() -> repository.getVersionPageUrl(object.data.getCandidate())) + Task.supplyAsync(() -> object.data.getRepository().getVersionPageUrl(object.data.getCandidate())) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null && StringUtils.isNotBlank(result)) { button.setOnAction(__ -> Controllers.openUriInBrowser(result)); From c4282195ebf2cfdd24734c8db025b5cf7da6e286 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 6 Apr 2026 17:02:39 +0800 Subject: [PATCH 091/123] 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 33c70803afd..4b20c6c6bcd 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 @@ -626,8 +626,8 @@ public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo) { spinnerPane.setOnFailedAction(e -> loadChangelog(version, repo, spinnerPane, scrollPane)); spinnerPane.setContent(scrollPane); - VBox.setVgrow(spinnerPane, Priority.SOMETIMES); box.getChildren().add(spinnerPane); + VBox.setVgrow(spinnerPane, Priority.SOMETIMES); this.setBody(box); From 3c3a132d0d95a4abcadcbe31b89812c979d8d39d Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 6 Apr 2026 21:37:35 +0800 Subject: [PATCH 092/123] update css --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 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 5865e185e80..4cd41658b4c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -36,8 +36,7 @@ import org.jsoup.nodes.TextNode; import java.net.URI; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import java.util.function.Consumer; import static org.jackhuang.hmcl.setting.ConfigHolder.config; @@ -48,6 +47,11 @@ */ public final class HTMLRenderer { + private static final Map cssPropertyMapping = Map.of( + "color", "-fx-fill", + "font-size", "-fx-font-size" + ); + private static URI resolveLink(Node linkNode) { String href = linkNode.absUrl("href"); if (href.isEmpty()) @@ -127,7 +131,7 @@ private void updateStyle() { hyperlink = null; fxStyle = null; - var styleBuilder = new StringBuilder(); + var declarations = new SimpleCssDeclarations(cssPropertyMapping); for (Node node : stack) { String nodeName = node.nodeName(); switch (nodeName) { @@ -143,17 +147,9 @@ private void updateStyle() { case "li" -> listDepth++; } - String style = node.attr("style"); - if (StringUtils.isNotBlank(style)) { - styleBuilder.append(StringUtils.addSuffix( - style - .replace("color:", "-fx-fill:") - .replace("font-size:", "-fx-font-size:"), // And more - ";" - )); - } + declarations.add(node.attr("style")); } - fxStyle = styleBuilder.toString(); + fxStyle = declarations.asString(); } private void pushNode(Node node) { @@ -531,4 +527,32 @@ public AutoLineBreak(String text) { super(text); } } + + private static final class SimpleCssDeclarations { + + private final Map declarations = new HashMap<>(); + private final Map mapping; + + public SimpleCssDeclarations(Map mapping) { + this.mapping = Map.copyOf(mapping); + } + + public void add(String inlineCss) { + if (StringUtils.isBlank(inlineCss)) return; + Arrays.stream(inlineCss.split(";")).filter(StringUtils::isNotBlank).forEach(s -> { + String[] declaration = s.split(":", 2); + if (declaration.length != 2) return; + declaration[0] = declaration[0].trim(); + declaration[1] = declaration[1].trim(); + declarations.put(mapping.getOrDefault(declaration[0], declaration[0]), declaration[1]); + }); + } + + public String asString() { + StringBuilder sb = new StringBuilder(); + declarations.forEach((property, value) -> sb.append(property).append(": ").append(value).append(";")); + return sb.toString(); + } + + } } From e4cc6aff15675bdae3fd99155c4f6cb3a75a2337 Mon Sep 17 00:00:00 2001 From: CalbootOnceMore Date: Wed, 29 Apr 2026 22:08:22 +0800 Subject: [PATCH 093/123] update translation --- HMCL/src/main/resources/assets/lang/I18N.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index cc851e19129..5f78ff7b98f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -362,7 +362,7 @@ download.external_link=Visit Download Website download.failed=Failed to download "%1$s", response code: %2$d. download.failed.empty=No versions are available. Please click here to go back. download.failed.no_code=Failed to download -download.failed.refresh=Failed to fetch version list. Please click here to retry. +download.failed.refresh=Failed to fetch data. Please click here to retry. download.game=New Game download.provider.bmclapi=BMCLAPI & MCIM download.provider.bmclapi.desc=This option is provided exclusively for users in Chinese Mainland. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index b0b1ed67428..025647ef1dd 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -359,7 +359,7 @@ download.external_link=開啟下載網站 download.failed=下載失敗:%1$s\n錯誤碼:%2$d download.failed.empty=沒有可供安裝的版本,按一下此處返回。 download.failed.no_code=下載失敗 -download.failed.refresh=載入版本清單失敗,按一下此處重試。 +download.failed.refresh=載入資料失敗,按一下此處重試。 download.game=新遊戲 download.provider.bmclapi=BMCLAPI & MCIM download.provider.bmclapi.desc=適用於中國大陸的鏡像源 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 d519fd49dcc..214a8eed39a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -361,7 +361,7 @@ download.external_link=打开下载网站 download.failed=下载失败: %1$s,\n错误码:%2$d\n你可以点击右上角帮助按钮进行求助。 download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你可以点击右上角帮助按钮进行求助) download.failed.no_code=下载失败 -download.failed.refresh=[加载版本列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助) +download.failed.refresh=[加载数据失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助) download.game=新游戏 download.provider.bmclapi=BMCLAPI & MCIM download.provider.bmclapi.desc=适用于中国大陆的镜像源 From e2883b53c2f5d6a1fb11c5355064dd1f5c82d9bb Mon Sep 17 00:00:00 2001 From: CalbootOnceMore Date: Thu, 30 Apr 2026 21:11:47 +0800 Subject: [PATCH 094/123] update ui --- .../hmcl/ui/versions/DownloadPage.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 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 2c452f81e6f..7b1efe5faed 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 @@ -491,6 +491,16 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP addonItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again box.getChildren().setAll(addonItem); + JFXHyperlink changelogButton = new JFXHyperlink(i18n("mods.changelog")); + changelogButton.setOnAction(__ -> Controllers.dialog(new AddonChangelog(version, selfPage.repository))); + + JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); + versionPageBtn.setDisable(true); + loadVersionPageUrl(version, selfPage.repository, versionPageBtn); + + HBox additionalBox = new HBox(changelogButton, versionPageBtn); + box.getChildren().add(additionalBox); + SpinnerPane spinnerPane = new SpinnerPane(); ScrollPane scrollPane = new ScrollPane(); ComponentList dependenciesList = new ComponentList(); @@ -509,13 +519,6 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP this.setBody(box); - JFXHyperlink changelogButton = new JFXHyperlink(i18n("mods.changelog")); - changelogButton.setOnAction(__ -> Controllers.dialog(new AddonChangelog(version, selfPage.repository))); - - 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")); @@ -542,13 +545,13 @@ 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(saveAsButton, cancelButton); } else { - this.setActions(versionPageBtn, changelogButton, downloadButton, saveAsButton, cancelButton); + this.setActions(downloadButton, saveAsButton, cancelButton); } 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.prefHeightProperty().bind(BindingMapping.of(Controllers.getStage().heightProperty()).map(w -> w.doubleValue() * 0.8)); onEscPressed(this, cancelButton::fire); } From 21960521034b18a4a2a4e2a6690903a9ea5cca6c Mon Sep 17 00:00:00 2001 From: ToobLac Date: Wed, 20 May 2026 22:50:10 +0800 Subject: [PATCH 095/123] update --- .../hmcl/ui/versions/AddonUpdatesPage.java | 32 ++++++++----------- .../hmcl/ui/versions/DownloadPage.java | 12 +++---- .../jackhuang/hmcl/mod/LocalAddonFile.java | 1 + .../org/jackhuang/hmcl/mod/LocalModFile.java | 2 +- .../resourcepack/ResourcePackZipFile.java | 2 +- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index 47397ee6b85..21d3e6a0315 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -31,8 +31,6 @@ 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.LocalAddonFile; import org.jackhuang.hmcl.mod.LocalAddonManager; import org.jackhuang.hmcl.mod.RemoteMod; @@ -43,8 +41,6 @@ import org.jackhuang.hmcl.ui.FXUtils; 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.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.CSVTable; @@ -102,19 +98,19 @@ public AddonUpdatesPage(LocalAddonManager localAddonManager, List sourceColumn = new TableColumn<>(i18n("mods.check_updates.source")); setupCellValueFactory(sourceColumn, AddonUpdateObject::sourceProperty); - TableColumn changelogColumn = new TableColumn<>(i18n("mods.changelog")); + TableColumn changelogColumn = new TableColumn<>(i18n("mods.changelog")); { var oldCellFactory = changelogColumn.getCellFactory(); changelogColumn.setCellFactory(param -> { - TableCell cell = oldCellFactory.call(param); + TableCell cell = oldCellFactory.call(param); cell.getStyleClass().add("addon-changelog-table-cell"); cell.setOnMouseClicked(event -> { - List items = cell.getTableColumn().getTableView().getItems(); + List items = cell.getTableColumn().getTableView().getItems(); if (cell.getIndex() >= items.size() || cell.getIndex() < 0) { return; } - ModUpdateObject object = items.get(cell.getIndex()); - Controllers.dialog(new ModChangelog(object)); + AddonUpdateObject object = items.get(cell.getIndex()); + Controllers.dialog(new AddonChangelog(object)); }); return cell; }); @@ -305,10 +301,10 @@ public void setSource(String source) { } } - private static final class ModChangelog extends JFXDialogLayout { + private static final class AddonChangelog extends JFXDialogLayout { - public ModChangelog(ModUpdateObject object) { - RemoteMod.Version targetVersion = object.data.getCandidate(); + public AddonChangelog(AddonUpdateObject object) { + RemoteMod.Version targetVersion = object.data.targetVersion(); this.setHeading(new HBox(new Label(i18n("mods.changelog") + " - " + targetVersion.getName()))); @@ -346,18 +342,18 @@ public ModChangelog(ModUpdateObject object) { onEscPressed(this, closeButton::fire); } - private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) { + private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) { spinnerPane.setLoading(true); Task.supplyAsync(() -> { if (object.changelog != null) { return object.changelog; } - RemoteMod.Version version = object.data.getCandidate(); - return StringUtils.convertToHtml(object.data.getRepository().getModChangelog(version.getModid(), version.getVersionId())); + RemoteMod.Version version = object.data.targetVersion(); + return StringUtils.convertToHtml(object.data.repository().getModChangelog(version.getModid(), version.getVersionId())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { object.changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); - scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, object.data.getRepository().getBaseUrl())); + scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, object.data.repository().getBaseUrl())); FXUtils.smoothScrolling(scrollPane); spinnerPane.setFailedReason(null); } else { @@ -367,8 +363,8 @@ private void loadChangelog(ModUpdateObject object, SpinnerPane spinnerPane, Scro }).start(); } - private void loadVersionPageUrl(ModUpdateObject object, JFXHyperlink button) { - Task.supplyAsync(() -> object.data.getRepository().getVersionPageUrl(object.data.getCandidate())) + private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) { + Task.supplyAsync(() -> object.data.repository().getVersionPageUrl(object.data.targetVersion())) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null && StringUtils.isNotBlank(result)) { button.setOnAction(__ -> Controllers.openUriInBrowser(result)); 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 8783aaa2423..4f886e01ce7 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 @@ -279,18 +279,18 @@ protected DownloadPageSkin(DownloadPage control) { String gameVersion = repository.getGameVersion(game).orElse(null); if (gameVersion != null && control.versions.containsKey(gameVersion)) { - List modVersions = control.versions.get(gameVersion); - if (modVersions != null && !modVersions.isEmpty()) { + List addonVersions = control.versions.get(gameVersion); + if (addonVersions != null && !addonVersions.isEmpty()) { Set targetLoaders = LibraryAnalyzer.analyze(game, gameVersion).getModLoaders(); resolve: - for (RemoteMod.Version modVersion : modVersions) { + for (RemoteMod.Version addonVersion : addonVersions) { if (getSkinnable().type == RemoteModRepository.Type.MOD) { - for (ModLoaderType loader : modVersion.getLoaders()) { + for (ModLoaderType loader : addonVersion.getLoaders()) { if (targetLoaders.contains(loader)) { list.getContent().addAll( ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)), - new AddonItem(control.addon, modVersion, control) + new AddonItem(control.addon, addonVersion, control) ); break resolve; } @@ -298,7 +298,7 @@ protected DownloadPageSkin(DownloadPage control) { } else { list.getContent().addAll( ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)), - new ModItem(control.addon, modVersion, control) + new AddonItem(control.addon, addonVersion, control) ); break; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java index 15183a74d3e..9e13133fc28 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java @@ -56,6 +56,7 @@ public boolean isDisabled() { public abstract AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteMod.Type type) throws IOException; public record AddonUpdate( + RemoteModRepository repository, LocalAddonFile localAddonFile, RemoteMod.Version currentVersion, RemoteMod.Version targetVersion, 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 235e5b36f9a..60fabd67e30 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -203,7 +203,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe .sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed()) .toList(); if (remoteVersions.isEmpty()) return null; - return new AddonUpdate(this, currentVersion.get(), remoteVersions.get(0), true); + return new AddonUpdate(repository, this, currentVersion.get(), remoteVersions.get(0), true); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java index 838702bf235..69f6b19d205 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java @@ -101,7 +101,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe .sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed()) .toList(); if (remoteVersions.isEmpty()) return null; - return new AddonUpdate(this, currentVersion.get(), remoteVersions.get(0), false); + return new AddonUpdate(repository, this, currentVersion.get(), remoteVersions.get(0), false); } } From a2522033de72908b02c8aaef66481167cb5ac108 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Thu, 21 May 2026 22:43:42 +0800 Subject: [PATCH 096/123] 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 4cd41658b4c..13316522801 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -370,14 +370,14 @@ private void appendTable(Node table) { TableColumn, javafx.scene.Node> c = new TableColumn<>(); Element e = head.get(i); if (e != null) { - var box = new VBox(new HTMLRenderer(Controllers::openUriInBrowser).appendNode(e).render()); + var box = new VBox(new HTMLRenderer(Controllers::openUriInBrowser).appendNode(e).mergeLineBreaks().render()); box.setAlignment(Pos.CENTER_LEFT); c.setGraphic(box); } c.setCellValueFactory(param -> { Element el = param.getValue().get(finalI); if (el == null) return new SimpleObjectProperty<>(); - return new SimpleObjectProperty<>(new HTMLRenderer(Controllers::openUriInBrowser).appendNode(el).render()); + return new SimpleObjectProperty<>(new HTMLRenderer(Controllers::openUriInBrowser).appendNode(el).mergeLineBreaks().render()); }); tableView.getColumns().add(c); } From 148ab1a771bdc2f754bfe11aeb19dcadc1a4d6fd Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sat, 30 May 2026 20:54:54 +0800 Subject: [PATCH 097/123] 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 13316522801..8dd0500aaa3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -370,14 +370,14 @@ private void appendTable(Node table) { TableColumn, javafx.scene.Node> c = new TableColumn<>(); Element e = head.get(i); if (e != null) { - var box = new VBox(new HTMLRenderer(Controllers::openUriInBrowser).appendNode(e).mergeLineBreaks().render()); + var box = new VBox(new HTMLRenderer(onClickHyperlink).appendNode(e).mergeLineBreaks().render()); box.setAlignment(Pos.CENTER_LEFT); c.setGraphic(box); } c.setCellValueFactory(param -> { Element el = param.getValue().get(finalI); if (el == null) return new SimpleObjectProperty<>(); - return new SimpleObjectProperty<>(new HTMLRenderer(Controllers::openUriInBrowser).appendNode(el).mergeLineBreaks().render()); + return new SimpleObjectProperty<>(new HTMLRenderer(onClickHyperlink).appendNode(el).mergeLineBreaks().render()); }); tableView.getColumns().add(c); } From 08343b48b3b72f9c3c5c792b295af0fe1e26ca3c Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sat, 30 May 2026 21:52:06 +0800 Subject: [PATCH 098/123] fix cell height Co-authored-by: CiiLu <109708109+ciilu@users.noreply.github.com> --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 5 ++--- 1 file changed, 2 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 8dd0500aaa3..78b3dc925d2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -25,6 +25,7 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; @@ -363,8 +364,6 @@ private void appendTable(Node table) { rows.add(Lang.copyWithSize(foot, columnCount, null)); TableView> tableView = new TableView<>(FXCollections.observableList(rows)); - tableView.setFixedCellSize(25); - tableView.setPrefHeight(25 * (rows.size() + 1) + 5); for (int i = 0; i < columnCount; i++) { int finalI = i; TableColumn, javafx.scene.Node> c = new TableColumn<>(); @@ -377,7 +376,7 @@ private void appendTable(Node table) { c.setCellValueFactory(param -> { Element el = param.getValue().get(finalI); if (el == null) return new SimpleObjectProperty<>(); - return new SimpleObjectProperty<>(new HTMLRenderer(onClickHyperlink).appendNode(el).mergeLineBreaks().render()); + return new SimpleObjectProperty<>(new Pane(new HTMLRenderer(onClickHyperlink).appendNode(el).mergeLineBreaks().render())); }); tableView.getColumns().add(c); } From 0b825b3df6cd5b9716e4436a73ee8eb8dcc916dd Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 31 May 2026 18:39:20 +0800 Subject: [PATCH 099/123] fix table height --- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 54 +++++++++++++++++++ .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 4 +- 2 files changed, 56 insertions(+), 2 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 55aff5872f6..d71bab2a661 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -32,6 +32,7 @@ import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableValue; import javafx.beans.value.WeakChangeListener; +import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javafx.event.Event; import javafx.event.EventDispatcher; @@ -44,6 +45,7 @@ import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.*; +import javafx.scene.control.skin.TableViewSkin; import javafx.scene.control.skin.VirtualFlow; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -1711,4 +1713,56 @@ public static TextFlow renderAddonChangelog(String changelogHtml, String baseUri textFlow.getStyleClass().add("addon-changelog"); return textFlow; } + + /// Be cautious when dealing with large amounts of data + public static TableView autoSizeTable(ObservableList items) { + return new AutoSizeTableView<>(items); + } + + private static final class AutoSizeTableView extends TableView { + + public AutoSizeTableView() { + super(); + } + + public AutoSizeTableView(ObservableList items) { + super(items); + } + + @Override + protected Skin createDefaultSkin() { + return new AutoSizeSkin<>(this); + } + + private static final class AutoSizeSkin extends TableViewSkin { + + private final VirtualFlow> flow; + + public AutoSizeSkin(TableView control) { + super(control); + this.flow = getVirtualFlow(); + } + + @Override + protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + double fixedCellSize = getSkinnable().getFixedCellSize(); + double w = width - rightInset - leftInset; + int itemCount = getItemCount(); + double headerHeight = getTableHeaderRow().prefHeight(w); + + double prefHeight = topInset + bottomInset + headerHeight; + if (fixedCellSize > 0 || itemCount == 0) return prefHeight + fixedCellSize * itemCount; + int i = 0; + TableRow r; + while (!(r = flow.getCell(i)).isEmpty() && r.getItem() != null) { + prefHeight += r.prefHeight(w); + i++; + } + return prefHeight; + } + + } + + } + } 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 78b3dc925d2..b9eec1d95f9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -363,7 +363,7 @@ private void appendTable(Node table) { if (hasFoot) rows.add(Lang.copyWithSize(foot, columnCount, null)); - TableView> tableView = new TableView<>(FXCollections.observableList(rows)); + TableView> tableView = FXUtils.autoSizeTable(FXCollections.observableList(rows)); for (int i = 0; i < columnCount; i++) { int finalI = i; TableColumn, javafx.scene.Node> c = new TableColumn<>(); @@ -375,7 +375,7 @@ private void appendTable(Node table) { } c.setCellValueFactory(param -> { Element el = param.getValue().get(finalI); - if (el == null) return new SimpleObjectProperty<>(); + if (el == null) return null; return new SimpleObjectProperty<>(new Pane(new HTMLRenderer(onClickHyperlink).appendNode(el).mergeLineBreaks().render())); }); tableView.getColumns().add(c); From caa8798fbd0e63f8b204e3fcd337bca98058c21b Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sat, 13 Jun 2026 21:46:24 +0800 Subject: [PATCH 100/123] apply suggestions --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 21 ++++++++----------- .../hmcl/ui/versions/AddonUpdatesPage.java | 12 ++++------- .../hmcl/ui/versions/DownloadPage.java | 7 +++---- 3 files changed, 16 insertions(+), 24 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 b9eec1d95f9..8813044fe34 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -48,11 +48,6 @@ */ public final class HTMLRenderer { - private static final Map cssPropertyMapping = Map.of( - "color", "-fx-fill", - "font-size", "-fx-font-size" - ); - private static URI resolveLink(Node linkNode) { String href = linkNode.absUrl("href"); if (href.isEmpty()) @@ -132,7 +127,7 @@ private void updateStyle() { hyperlink = null; fxStyle = null; - var declarations = new SimpleCssDeclarations(cssPropertyMapping); + var declarations = new SimpleCssDeclarations(); for (Node node : stack) { String nodeName = node.nodeName(); switch (nodeName) { @@ -529,12 +524,12 @@ public AutoLineBreak(String text) { private static final class SimpleCssDeclarations { - private final Map declarations = new HashMap<>(); - private final Map mapping; + private static final Map cssPropertyMapping = Map.of( + "color", "-fx-fill", + "font-size", "-fx-font-size" + ); - public SimpleCssDeclarations(Map mapping) { - this.mapping = Map.copyOf(mapping); - } + private final Map declarations = new HashMap<>(); public void add(String inlineCss) { if (StringUtils.isBlank(inlineCss)) return; @@ -543,7 +538,9 @@ public void add(String inlineCss) { if (declaration.length != 2) return; declaration[0] = declaration[0].trim(); declaration[1] = declaration[1].trim(); - declarations.put(mapping.getOrDefault(declaration[0], declaration[0]), declaration[1]); + if (cssPropertyMapping.containsKey(declaration[0])) { + declarations.put(cssPropertyMapping.get(declaration[0]), declaration[1]); + } // Ignore unsupported fields }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index 21d3e6a0315..44659c9b17a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -44,7 +44,6 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.CSVTable; -import org.jackhuang.hmcl.util.javafx.BindingMapping; import java.io.IOException; import java.nio.file.Path; @@ -105,11 +104,8 @@ public AddonUpdatesPage(LocalAddonManager localAddonManager, List cell = oldCellFactory.call(param); cell.getStyleClass().add("addon-changelog-table-cell"); cell.setOnMouseClicked(event -> { - List items = cell.getTableColumn().getTableView().getItems(); - if (cell.getIndex() >= items.size() || cell.getIndex() < 0) { - return; - } - AddonUpdateObject object = items.get(cell.getIndex()); + AddonUpdateObject object; + if (cell.isEmpty() || cell.getTableRow() == null || (object = cell.getTableRow().getItem()) == null) return; Controllers.dialog(new AddonChangelog(object)); }); return cell; @@ -336,8 +332,8 @@ public AddonChangelog(AddonUpdateObject object) { 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); } 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 4f886e01ce7..cd8c8573c52 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 @@ -49,7 +49,6 @@ import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; @@ -568,8 +567,8 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP this.setActions(downloadButton, saveAsButton, cancelButton); } - 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.8)); + this.prefWidthProperty().bind(Controllers.getStage().widthProperty().multiply(0.7)); + this.prefHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.8)); onEscPressed(this, cancelButton::fire); } @@ -662,7 +661,7 @@ public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo) { setActions(closeButton); this.prefWidthProperty().bind(Controllers.getStage().widthProperty().multiply(0.7)); - this.prefHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.7)); + this.prefHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.8)); onEscPressed(this, closeButton::fire); } From 7524e09f356fcc7a25841d0aacd3b534e0303e29 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sat, 13 Jun 2026 21:49:55 +0800 Subject: [PATCH 101/123] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 5 +++-- 1 file changed, 3 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 8813044fe34..84bd7ed573f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -538,8 +538,9 @@ public void add(String inlineCss) { if (declaration.length != 2) return; declaration[0] = declaration[0].trim(); declaration[1] = declaration[1].trim(); - if (cssPropertyMapping.containsKey(declaration[0])) { - declarations.put(cssPropertyMapping.get(declaration[0]), declaration[1]); + String mappedKey = cssPropertyMapping.get(declaration[0]); + if (mappedKey != null) { + declarations.put(mappedKey, declaration[1]); } // Ignore unsupported fields }); } From 1acd344c01b432d6edc3465bce0955c985ec5feb Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 14 Jun 2026 12:26:30 +0800 Subject: [PATCH 102/123] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java | 2 +- .../main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 11 +++++------ 2 files changed, 6 insertions(+), 7 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 d71bab2a661..3c51e5c79ac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -1715,7 +1715,7 @@ public static TextFlow renderAddonChangelog(String changelogHtml, String baseUri } /// Be cautious when dealing with large amounts of data - public static TableView autoSizeTable(ObservableList items) { + public static TableView newAutoSizeTable(ObservableList items) { return new AutoSizeTableView<>(items); } 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 84bd7ed573f..558db1e4823 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -358,7 +358,7 @@ private void appendTable(Node table) { if (hasFoot) rows.add(Lang.copyWithSize(foot, columnCount, null)); - TableView> tableView = FXUtils.autoSizeTable(FXCollections.observableList(rows)); + TableView> tableView = FXUtils.newAutoSizeTable(FXCollections.observableList(rows)); for (int i = 0; i < columnCount; i++) { int finalI = i; TableColumn, javafx.scene.Node> c = new TableColumn<>(); @@ -503,14 +503,13 @@ public TextFlow render() { textFlow.getChildren().setAll(children); for (javafx.scene.Node node : children) { if (node instanceof ImageView img) { - InvalidationListener listener = __ -> - img.setFitWidth(Math.min(textFlow.getWidth() - 20D, img.getImage() == null ? 0D : img.getImage().getWidth())); + InvalidationListener listener = __ -> img.setFitWidth(Math.min(textFlow.getWidth() * 0.8, img.getImage() == null ? 0D : img.getImage().getWidth())); textFlow.widthProperty().addListener(listener); img.imageProperty().addListener(listener); } else if (node instanceof TableView table) { - table.prefWidthProperty().bind(textFlow.widthProperty().add(-20D)); + table.prefWidthProperty().bind(textFlow.widthProperty().multiply(0.8)); } else if (node instanceof TextFlow codeFlow) { - codeFlow.maxWidthProperty().bind(textFlow.widthProperty().add(-20D)); + codeFlow.maxWidthProperty().bind(textFlow.widthProperty().multiply(0.8)); } } return textFlow; @@ -538,7 +537,7 @@ public void add(String inlineCss) { if (declaration.length != 2) return; declaration[0] = declaration[0].trim(); declaration[1] = declaration[1].trim(); - String mappedKey = cssPropertyMapping.get(declaration[0]); + String mappedKey = cssPropertyMapping.get(declaration[0].toLowerCase(Locale.ROOT)); if (mappedKey != null) { declarations.put(mappedKey, declaration[1]); } // Ignore unsupported fields From 0490b1c008251dace584831eed991c09e9ae8a40 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 14 Jun 2026 12:56:23 +0800 Subject: [PATCH 103/123] 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 558db1e4823..5c275440002 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -248,7 +248,7 @@ private void appendImage(Node node) { return; } if (res == null) { - LOG.warning("Failed to load image: " + src, new AssertionError("Image loading task returned null")); + LOG.warning("Failed to load image: " + src, new AssertionError("Image loading task returned null, which should never happen")); } imageView.setImage(res); }).start(); From de166c362c12cbc834a70372c94947f0920681fc Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 14 Jun 2026 13:04:38 +0800 Subject: [PATCH 104/123] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 4 ++++ 1 file changed, 4 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 5c275440002..9f7411797ba 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -110,6 +110,8 @@ public static String normaliseWhitespace(String str) { private final Consumer onClickHyperlink; + private boolean rendered; + public HTMLRenderer(Consumer onClickHyperlink) { this.onClickHyperlink = onClickHyperlink; } @@ -498,6 +500,7 @@ public HTMLRenderer mergeLineBreaks() { } public TextFlow render() { + if (rendered) throw new IllegalStateException("Should not render twice"); TextFlow textFlow = new TextFlow(); textFlow.getStyleClass().add("html"); textFlow.getChildren().setAll(children); @@ -512,6 +515,7 @@ public TextFlow render() { codeFlow.maxWidthProperty().bind(textFlow.widthProperty().multiply(0.8)); } } + rendered = true; return textFlow; } From b570429fdee9d3fe3d27830c98b01e9dd7a04f89 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 14 Jun 2026 13:07:11 +0800 Subject: [PATCH 105/123] update --- .../java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java | 2 ++ .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 3 +++ 2 files changed, 5 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index 44659c9b17a..476f1dc254f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -365,6 +365,8 @@ private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) { if (exception == null && StringUtils.isNotBlank(result)) { button.setOnAction(__ -> Controllers.openUriInBrowser(result)); button.setDisable(false); + } else { + LOG.warning("Failed to load addon version page url", exception); } }) .start(); 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 cd8c8573c52..b7785996744 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 @@ -59,6 +59,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class DownloadPage extends Control implements DecoratorPage { @@ -625,6 +626,8 @@ private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository r if (exception == null && StringUtils.isNotBlank(result)) { button.setOnAction(__ -> Controllers.openUriInBrowser(result)); button.setDisable(false); + } else { + LOG.warning("Failed to load addon version page url", exception); } }) .start(); From edb7b8a9f48b08adc7293b77eef7ffa846dfa3c6 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 14 Jun 2026 13:34:50 +0800 Subject: [PATCH 106/123] update & renaming --- .../hmcl/ui/versions/AddonCheckUpdatesTask.java | 4 ++-- .../hmcl/ui/versions/AddonUpdatesPage.java | 12 +++++++++--- .../org/jackhuang/hmcl/mod/LocalAddonFile.java | 9 +++++++-- .../org/jackhuang/hmcl/mod/LocalModFile.java | 6 +++--- .../java/org/jackhuang/hmcl/mod/RemoteMod.java | 6 +++--- .../org/jackhuang/hmcl/mod/curse/CurseAddon.java | 16 ++++++++++++---- .../mod/curse/CurseForgeRemoteModRepository.java | 8 ++++---- .../modrinth/ModrinthRemoteModRepository.java | 4 ++-- .../hmcl/resourcepack/ResourcePackFile.java | 2 ++ .../hmcl/resourcepack/ResourcePackFolder.java | 4 ---- .../hmcl/resourcepack/ResourcePackZipFile.java | 6 +++--- 11 files changed, 47 insertions(+), 30 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java index 54dcdc5b628..27e92240ead 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java @@ -39,10 +39,10 @@ public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersi dependents = addons.stream().map(addon -> Task.supplyAsync(Schedulers.io(), () -> { LocalAddonFile.AddonUpdate candidate = null; - for (RemoteMod.Type type : RemoteMod.Type.values()) { + for (RemoteMod.Source source : RemoteMod.Source.values()) { LocalAddonFile.AddonUpdate update = null; try { - update = addon.checkUpdates(downloadProvider, gameVersion, type); + update = addon.checkUpdates(downloadProvider, gameVersion, source); } catch (IOException e) { LOG.warning(String.format("Cannot check update for addon %s.", addon.getFileName()), e); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index 476f1dc254f..b6142a13cb3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -34,6 +34,7 @@ import org.jackhuang.hmcl.mod.LocalAddonFile; import org.jackhuang.hmcl.mod.LocalAddonManager; 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; @@ -340,16 +341,18 @@ public AddonChangelog(AddonUpdateObject object) { private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) { spinnerPane.setLoading(true); + RemoteModRepository repo = object.data.source().getRepoForType(object.data.repoType()); Task.supplyAsync(() -> { if (object.changelog != null) { return object.changelog; } RemoteMod.Version version = object.data.targetVersion(); - return StringUtils.convertToHtml(object.data.repository().getModChangelog(version.getModid(), version.getVersionId())); + if (repo == null) return null; + return StringUtils.convertToHtml(repo.getModChangelog(version.getModid(), version.getVersionId())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { object.changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); - scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, object.data.repository().getBaseUrl())); + scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, repo == null ? "" : repo.getBaseUrl())); FXUtils.smoothScrolling(scrollPane); spinnerPane.setFailedReason(null); } else { @@ -360,7 +363,10 @@ private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, Sc } private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) { - Task.supplyAsync(() -> object.data.repository().getVersionPageUrl(object.data.targetVersion())) + Task.supplyAsync(() -> { + RemoteModRepository repo = object.data.source().getRepoForType(object.data.repoType()); + return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion()); + }) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null && StringUtils.isNotBlank(result)) { button.setOnAction(__ -> Controllers.openUriInBrowser(result)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java index 9e13133fc28..ecb646dce98 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java @@ -20,6 +20,7 @@ import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -53,10 +54,14 @@ public boolean isDisabled() { public abstract void delete() throws IOException; @Nullable - public abstract AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteMod.Type type) throws IOException; + public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteMod.Source source) throws IOException { + return null; + } + @NotNullByDefault public record AddonUpdate( - RemoteModRepository repository, + RemoteMod.Source source, + RemoteModRepository.Type repoType, LocalAddonFile localAddonFile, RemoteMod.Version currentVersion, RemoteMod.Version targetVersion, 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 60fabd67e30..0a5ac22e462 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -191,8 +191,8 @@ public void delete() throws IOException { } @Override - public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteMod.Type type) throws IOException { - RemoteModRepository repository = type.getRepoForType(RemoteModRepository.Type.MOD); + public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteMod.Source source) throws IOException { + RemoteModRepository repository = source.getRepoForType(RemoteModRepository.Type.MOD); if (repository == null) return null; Optional currentVersion = repository.getRemoteVersionByLocalFile(file); if (currentVersion.isEmpty()) return null; @@ -203,7 +203,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe .sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed()) .toList(); if (remoteVersions.isEmpty()) return null; - return new AddonUpdate(repository, this, currentVersion.get(), remoteVersions.get(0), true); + return new AddonUpdate(source, RemoteModRepository.Type.MOD, this, currentVersion.get(), remoteVersions.get(0), true); } @Override 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 e715052b436..8114e29ec4c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -194,7 +194,7 @@ public int hashCode() { } } - public enum Type { + public enum Source { CURSEFORGE( CurseForgeRemoteModRepository.MODS, CurseForgeRemoteModRepository.RESOURCE_PACKS, @@ -231,7 +231,7 @@ public RemoteModRepository getRepoForType(RemoteModRepository.Type type) { }; } - Type( + Source( RemoteModRepository modRepo, RemoteModRepository resourcePackRepo, RemoteModRepository shaderPackRepo, @@ -255,7 +255,7 @@ public interface IMod { } public interface IVersion { - Type getType(); + Source getType(); } public static class Version { 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 1391826d9f3..97298c5069b 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 @@ -212,7 +212,7 @@ public Stream loadVersions(RemoteModRepository modRepository, return modRepository.getRemoteVersionsById(downloadProvider, Integer.toString(id)); } - public RemoteMod toMod(RemoteModRepository.Type type) { + public RemoteMod toMod() { String iconUrl = ""; if (logo != null) { if (StringUtils.isNotBlank(logo.getThumbnailUrl())) @@ -221,6 +221,14 @@ else if (StringUtils.isNotBlank(logo.getUrl())) iconUrl = logo.getUrl(); } + RemoteModRepository.Type repoType = switch (classId) { + case CurseForgeRemoteModRepository.SECTION_MODPACK -> RemoteModRepository.Type.MODPACK; + case CurseForgeRemoteModRepository.SECTION_RESOURCE_PACK -> RemoteModRepository.Type.RESOURCE_PACK; + case CurseForgeRemoteModRepository.SECTION_WORLD -> RemoteModRepository.Type.WORLD; + case CurseForgeRemoteModRepository.SECTION_CUSTOMIZATION -> RemoteModRepository.Type.CUSTOMIZATION; + case CurseForgeRemoteModRepository.SECTION_SHADER -> RemoteModRepository.Type.SHADER_PACK; + default -> RemoteModRepository.Type.MOD; + }; return new RemoteMod( slug, "", @@ -230,7 +238,7 @@ else if (StringUtils.isNotBlank(logo.getUrl())) links.websiteUrl, iconUrl, this, - type + repoType ); } @@ -558,8 +566,8 @@ public long getFileFingerprint() { } @Override - public RemoteMod.Type getType() { - return RemoteMod.Type.CURSEFORGE; + public RemoteMod.Source getType() { + return RemoteMod.Source.CURSEFORGE; } public RemoteMod.Version toVersion() { 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 4f00458c604..5e7fa79281c 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 @@ -153,7 +153,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion response = withApiKey(HttpRequest.GET(candidate.toString())) .getJson(Response.typeOf(listTypeOf(CurseAddon.class))); if (searchFilter.isEmpty()) { - return new SearchResult(response.getData().stream().map(addon -> addon.toMod(type)), calculateTotalPages(response, pageSize)); + return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); } break; } catch (IOException e) { @@ -182,7 +182,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator(); - return new SearchResult(response.getData().stream().map(addon -> addon.toMod(type)).map(remoteMod -> { + return new SearchResult(response.getData().stream().map(CurseAddon::toMod).map(remoteMod -> { String lowerCaseResult = remoteMod.getTitle().toLowerCase(Locale.ROOT); int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult); @@ -193,7 +193,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } return pair(remoteMod, diff); - }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(addon -> addon.toMod(type)), calculateTotalPages(response, pageSize)); + }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); } finally { SEMAPHORE.release(); } @@ -242,7 +242,7 @@ public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws try { Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id)) .getJson(Response.typeOf(CurseAddon.class)); - return response.data.toMod(type); + return response.data.toMod(); } finally { SEMAPHORE.release(); } 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 efbadabab96..8a0965d25bd 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 @@ -659,8 +659,8 @@ public List getFiles() { } @Override - public RemoteMod.Type getType() { - return RemoteMod.Type.MODRINTH; + public RemoteMod.Source getType() { + return RemoteMod.Source.MODRINTH; } public Optional toVersion() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java index fb70dd8f6f8..0c16c81c7b0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java @@ -18,7 +18,9 @@ package org.jackhuang.hmcl.resourcepack; import javafx.scene.image.Image; +import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.mod.LocalAddonFile; +import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java index 7deeaeec2e1..a4897c0c362 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java @@ -78,8 +78,4 @@ public void delete() throws IOException { FileUtils.deleteDirectory(file); } - @Override - public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteMod.Type type) { - return null; - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java index 69f6b19d205..500e92008c2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java @@ -90,8 +90,8 @@ public void delete() throws IOException { } @Override - public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteMod.Type type) throws IOException { - RemoteModRepository repository = type.getRepoForType(RemoteModRepository.Type.RESOURCE_PACK); + public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteMod.Source source) throws IOException { + RemoteModRepository repository = source.getRepoForType(RemoteModRepository.Type.RESOURCE_PACK); if (repository == null) return null; Optional currentVersion = repository.getRemoteVersionByLocalFile(file); if (currentVersion.isEmpty()) return null; @@ -101,7 +101,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe .sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed()) .toList(); if (remoteVersions.isEmpty()) return null; - return new AddonUpdate(repository, this, currentVersion.get(), remoteVersions.get(0), false); + return new AddonUpdate(source, RemoteModRepository.Type.RESOURCE_PACK, this, currentVersion.get(), remoteVersions.get(0), false); } } From d8151316c8c9dced065dc69841994d16cd0ee3b9 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 14 Jun 2026 13:36:36 +0800 Subject: [PATCH 107/123] update --- .../org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java | 6 ++---- .../org/jackhuang/hmcl/resourcepack/ResourcePackFile.java | 2 -- .../org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java | 2 -- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index b6142a13cb3..a1a5db6fc31 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -366,16 +366,14 @@ private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) { Task.supplyAsync(() -> { RemoteModRepository repo = object.data.source().getRepoForType(object.data.repoType()); return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion()); - }) - .whenComplete(Schedulers.javafx(), (result, exception) -> { + }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null && StringUtils.isNotBlank(result)) { button.setOnAction(__ -> Controllers.openUriInBrowser(result)); button.setDisable(false); } else { LOG.warning("Failed to load addon version page url", exception); } - }) - .start(); + }).start(); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java index 0c16c81c7b0..fb70dd8f6f8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java @@ -18,9 +18,7 @@ package org.jackhuang.hmcl.resourcepack; import javafx.scene.image.Image; -import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.mod.LocalAddonFile; -import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java index a4897c0c362..63d1a6579c7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java @@ -18,8 +18,6 @@ package org.jackhuang.hmcl.resourcepack; import javafx.scene.image.Image; -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; From 57b3822787b632bdbccb40772c61f836db6393cd Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 14 Jun 2026 13:37:31 +0800 Subject: [PATCH 108/123] update --- .../hmcl/ui/versions/AddonUpdatesPage.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index a1a5db6fc31..368041b9007 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -364,16 +364,16 @@ private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, Sc private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) { Task.supplyAsync(() -> { - RemoteModRepository repo = object.data.source().getRepoForType(object.data.repoType()); - return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion()); + RemoteModRepository repo = object.data.source().getRepoForType(object.data.repoType()); + return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion()); }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null && StringUtils.isNotBlank(result)) { - button.setOnAction(__ -> Controllers.openUriInBrowser(result)); - button.setDisable(false); - } else { - LOG.warning("Failed to load addon version page url", exception); - } - }).start(); + if (exception == null && StringUtils.isNotBlank(result)) { + button.setOnAction(__ -> Controllers.openUriInBrowser(result)); + button.setDisable(false); + } else { + LOG.warning("Failed to load addon version page url", exception); + } + }).start(); } } From 5d5031d3ed11a2dfa3d59aec6a9ae31ff34346dc Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 14 Jun 2026 19:55:14 +0800 Subject: [PATCH 109/123] =?UTF-8?q?=E6=9B=B4=E6=96=B0markdown=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/util/StringUtils.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 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 3136e832d49..21a41e2a871 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -24,7 +24,7 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.jsoup.Jsoup; -import org.jsoup.nodes.Node; +import org.jsoup.nodes.Document; import org.jsoup.safety.Safelist; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -631,8 +631,6 @@ public static List deserializeStringList(String json) { private static final Safelist all = Safelist.relaxed() .addAttributes("a", "rel", "target"); - private static final Set mdParsableTags = Set.of("#text", "img"); - private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().extensions(List.of( InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create() )).build(); @@ -641,13 +639,24 @@ public static List deserializeStringList(String json) { AutolinkExtension.create(), InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create() )).build(); - public static String convertToHtml(String md) { - if (md == null) return null; - if (isBlank(md)) return ""; - if (md.startsWith("") || md.startsWith("") || md.startsWith("") - || (Jsoup.isValid(md, all) && !Jsoup.parse(md).body().childNodes().stream().map(Node::normalName).allMatch(mdParsableTags::contains))) - return md; - return HTML_RENDERER.render(MD_PARSER.parse(md)); + public static String convertToHtml(String str) { + if (str == null) return null; + if (isBlank(str)) return ""; + + /* + Firstly, we check if the string could be seen as HTML. + + If so, we convert it to HTML document, then re-print it to a string to normalize spaces + (otherwise the parser may parse the HTML code as code blocks, which is not intended). + This will (hopefully) not break Markdown formats. + + If not, we assume that the string is pure Markdown. + */ + String either = Jsoup.isValid(str, all) + ? Jsoup.parse(str).outputSettings(new Document.OutputSettings().prettyPrint(false)).body().html() + : str; + + return HTML_RENDERER.render(MD_PARSER.parse(either)); } public static class LevCalculator { From 52601fb7590495c84b26454b9639be3d85164192 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Fri, 19 Jun 2026 12:41:58 +0800 Subject: [PATCH 110/123] 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 9f7411797ba..ddcd084df18 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -40,7 +40,7 @@ import java.util.*; import java.util.function.Consumer; -import static org.jackhuang.hmcl.setting.ConfigHolder.config; +import static org.jackhuang.hmcl.setting.SettingsManager.settings; import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** @@ -186,7 +186,7 @@ private void applyStyle(Text text) { if (code) { text.getStyleClass().add("html-code"); - styleBuilder.append("-fx-font-family: \"%s\";".formatted(Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT))); + styleBuilder.append("-fx-font-family: \"%s\";".formatted(Lang.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT))); } if (headerLevel != null) From e82810f8575d28f7993b69eb86c11934de120693 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Fri, 19 Jun 2026 21:29:29 +0800 Subject: [PATCH 111/123] update --- .../src/main/java/org/jackhuang/hmcl/ui/FXUtils.java | 12 ++++++------ .../java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 3 ++- 2 files changed, 8 insertions(+), 7 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 3c51e5c79ac..829746965d7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -1715,17 +1715,17 @@ public static TextFlow renderAddonChangelog(String changelogHtml, String baseUri } /// Be cautious when dealing with large amounts of data - public static TableView newAutoSizeTable(ObservableList items) { - return new AutoSizeTableView<>(items); + public static TableView newAutoHeightTable(ObservableList items) { + return new AutoHeightTableView<>(items); } - private static final class AutoSizeTableView extends TableView { + private static final class AutoHeightTableView extends TableView { - public AutoSizeTableView() { + public AutoHeightTableView() { super(); } - public AutoSizeTableView(ObservableList items) { + public AutoHeightTableView(ObservableList items) { super(items); } @@ -1754,7 +1754,7 @@ protected double computePrefHeight(double width, double topInset, double rightIn if (fixedCellSize > 0 || itemCount == 0) return prefHeight + fixedCellSize * itemCount; int i = 0; TableRow r; - while (!(r = flow.getCell(i)).isEmpty() && r.getItem() != null) { + while (i < itemCount && !(r = flow.getCell(i)).isEmpty() && r.getItem() != null) { prefHeight += r.prefHeight(w); i++; } 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 ddcd084df18..10c7f897388 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -360,7 +360,8 @@ private void appendTable(Node table) { if (hasFoot) rows.add(Lang.copyWithSize(foot, columnCount, null)); - TableView> tableView = FXUtils.newAutoSizeTable(FXCollections.observableList(rows)); + TableView> tableView = FXUtils.newAutoHeightTable(FXCollections.observableList(rows)); + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); for (int i = 0; i < columnCount; i++) { int finalI = i; TableColumn, javafx.scene.Node> c = new TableColumn<>(); From a52b0f69061b26c05bcf7ac88152bb014199e73e Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 21 Jun 2026 15:54:50 +0800 Subject: [PATCH 112/123] update --- .../hmcl/game/LocalizedRemoteModRepository.java | 4 ++-- .../jackhuang/hmcl/ui/versions/AddonUpdatesPage.java | 3 ++- .../org/jackhuang/hmcl/ui/versions/DownloadPage.java | 12 ++++++------ .../org/jackhuang/hmcl/mod/RemoteModRepository.java | 2 +- .../mod/curse/CurseForgeRemoteModRepository.java | 2 +- .../mod/modrinth/ModrinthRemoteModRepository.java | 2 +- 6 files changed, 13 insertions(+), 12 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 0268da64941..4f17b230bbd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -148,8 +148,8 @@ public Stream getRemoteVersionsById(DownloadProvider download } @Override - public String getModChangelog(String modId, String versionId) throws IOException { - return getBackedRemoteModRepository().getModChangelog(modId, versionId); + public String getModChangelog(DownloadProvider downloadProvider, String modId, String versionId) throws IOException { + return getBackedRemoteModRepository().getModChangelog(downloadProvider, modId, versionId); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index 368041b9007..8363b9f088b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -35,6 +35,7 @@ import org.jackhuang.hmcl.mod.LocalAddonManager; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -348,7 +349,7 @@ private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, Sc } RemoteMod.Version version = object.data.targetVersion(); if (repo == null) return null; - return StringUtils.convertToHtml(repo.getModChangelog(version.getModid(), version.getVersionId())); + return StringUtils.convertToHtml(repo.getModChangelog(DownloadProviders.getDownloadProvider(), version.getModid(), version.getVersionId())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { object.changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); 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 d84c0cceb4e..ef9f4ef8438 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 @@ -510,7 +510,7 @@ public AddonVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfP box.getChildren().setAll(addonItem); JFXHyperlink changelogButton = new JFXHyperlink(i18n("mods.changelog")); - changelogButton.setOnAction(__ -> Controllers.dialog(new AddonChangelog(version, selfPage.repository))); + changelogButton.setOnAction(__ -> Controllers.dialog(new AddonChangelog(version, selfPage.repository, selfPage.page.getDownloadProvider()))); JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); versionPageBtn.setDisable(true); @@ -636,7 +636,7 @@ private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository r private static final class AddonChangelog extends JFXDialogLayout { - public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo) { + public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo, DownloadProvider provider) { setHeading(new HBox(new Label(i18n("mods.changelog") + " - " + version.getName()))); VBox box = new VBox(8); @@ -648,8 +648,8 @@ public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo) { scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); FXUtils.setOverflowHidden(scrollPane, 8); - loadChangelog(version, repo, spinnerPane, scrollPane); - spinnerPane.setOnFailedAction(e -> loadChangelog(version, repo, spinnerPane, scrollPane)); + loadChangelog(version, repo, provider, spinnerPane, scrollPane); + spinnerPane.setOnFailedAction(e -> loadChangelog(version, repo, provider, spinnerPane, scrollPane)); spinnerPane.setContent(scrollPane); box.getChildren().add(spinnerPane); @@ -669,10 +669,10 @@ public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo) { onEscPressed(this, closeButton::fire); } - private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, SpinnerPane spinnerPane, ScrollPane scrollPane) { + private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, DownloadProvider provider, SpinnerPane spinnerPane, ScrollPane scrollPane) { spinnerPane.setLoading(true); Task.supplyAsync(() -> - StringUtils.convertToHtml(repo.getModChangelog(version.getModid(), version.getVersionId())) + StringUtils.convertToHtml(repo.getModChangelog(provider, version.getModid(), version.getVersionId())) ).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { String changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); 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 084967cd575..c6b8116c6b3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -104,7 +104,7 @@ default RemoteMod resolveDependency(DownloadProvider downloadProvider, String id Stream getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException; - String getModChangelog(String modId, String versionId) throws IOException; + String getModChangelog(DownloadProvider downloadProvider, String modId, String versionId) throws IOException; String getVersionPageUrl(RemoteMod.Version version) 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 5e7fa79281c..86d2b6b759e 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 @@ -274,7 +274,7 @@ public Stream getRemoteVersionsById(DownloadProvider download } @Override - public String getModChangelog(String modId, String versionId) throws IOException { + public String getModChangelog(DownloadProvider downloadProvider, 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, versionId))) 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 8a0965d25bd..fc683cdbd02 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 @@ -301,7 +301,7 @@ public Stream getRemoteVersionsById(DownloadProvider download } @Override - public String getModChangelog(String modId, String versionId) throws IOException { + public String getModChangelog(DownloadProvider downloadProvider, String modId, String versionId) throws IOException { SEMAPHORE.acquireUninterruptibly(); try { ProjectVersion version = HttpRequest.GET(PREFIX + "/v2/version/" + versionId).getJson(ProjectVersion.class); From 8f71e2e037f9e55a169c1748c1d0816ba8efdddd Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 21 Jun 2026 16:14:45 +0800 Subject: [PATCH 113/123] update --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 32 +++++++++++++++---- 1 file changed, 26 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 10c7f897388..79103100426 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -26,12 +26,14 @@ import javafx.scene.control.TableView; import javafx.scene.image.ImageView; import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; 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.util.Lang; import org.jackhuang.hmcl.util.StringUtils; +import org.jetbrains.annotations.Nullable; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; @@ -254,16 +256,19 @@ private void appendImage(Node node) { } imageView.setImage(res); }).start(); + imageView.setPreserveRatio(true); + + ImagePane imagePane = new ImagePane(imageView, alt); if (hyperlink != null) { URI target = resolveLink(hyperlink); if (target != null) { - FXUtils.onClicked(imageView, () -> onClickHyperlink.accept(target)); - imageView.setCursor(Cursor.HAND); + FXUtils.onClicked(imagePane, () -> onClickHyperlink.accept(target)); + imagePane.setCursor(Cursor.HAND); } } - imageView.setPreserveRatio(true); - children.add(imageView); + + children.add(imagePane); return; } catch (Throwable e) { LOG.warning("Failed to load image: " + src, e); @@ -506,8 +511,9 @@ public TextFlow render() { textFlow.getStyleClass().add("html"); textFlow.getChildren().setAll(children); for (javafx.scene.Node node : children) { - if (node instanceof ImageView img) { - InvalidationListener listener = __ -> img.setFitWidth(Math.min(textFlow.getWidth() * 0.8, img.getImage() == null ? 0D : img.getImage().getWidth())); + if (node instanceof ImagePane imgPane) { + ImageView img = imgPane.imageView; + InvalidationListener listener = __ -> img.setFitWidth(Math.min(textFlow.getWidth() * 0.9, img.getImage() == null ? 0D : img.getImage().getWidth())); textFlow.widthProperty().addListener(listener); img.imageProperty().addListener(listener); } else if (node instanceof TableView table) { @@ -526,6 +532,20 @@ public AutoLineBreak(String text) { } } + private static final class ImagePane extends StackPane { + + public final ImageView imageView; + + public ImagePane(ImageView imageView, @Nullable String alt) { + super(); + this.imageView = imageView; + + if (StringUtils.isNotBlank(alt)) getChildren().add(new Text(alt)); + getChildren().add(imageView); + } + + } + private static final class SimpleCssDeclarations { private static final Map cssPropertyMapping = Map.of( From f862fc8d9ea0abcc91013d0d83e9698e6364a755 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 21 Jun 2026 16:20:48 +0800 Subject: [PATCH 114/123] update --- .../game/LocalizedRemoteModRepository.java | 4 +-- .../hmcl/ui/versions/AddonUpdatesPage.java | 2 +- .../hmcl/ui/versions/DownloadPage.java | 2 +- .../hmcl/mod/RemoteModRepository.java | 2 +- .../curse/CurseForgeRemoteModRepository.java | 4 +-- .../modrinth/ModrinthRemoteModRepository.java | 25 ++++++++++++++++--- 6 files changed, 29 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 4f17b230bbd..eb217839d66 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -148,8 +148,8 @@ public Stream getRemoteVersionsById(DownloadProvider download } @Override - public String getModChangelog(DownloadProvider downloadProvider, String modId, String versionId) throws IOException { - return getBackedRemoteModRepository().getModChangelog(downloadProvider, modId, versionId); + public String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException { + return getBackedRemoteModRepository().getAddonChangelog(downloadProvider, addonId, versionId); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index 8363b9f088b..644134739ae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -349,7 +349,7 @@ private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, Sc } RemoteMod.Version version = object.data.targetVersion(); if (repo == null) return null; - return StringUtils.convertToHtml(repo.getModChangelog(DownloadProviders.getDownloadProvider(), version.getModid(), version.getVersionId())); + return StringUtils.convertToHtml(repo.getAddonChangelog(DownloadProviders.getDownloadProvider(), version.getModid(), version.getVersionId())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { object.changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); 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 ef9f4ef8438..b9fdf6c25d6 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 @@ -672,7 +672,7 @@ public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo, Downl private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, DownloadProvider provider, SpinnerPane spinnerPane, ScrollPane scrollPane) { spinnerPane.setLoading(true); Task.supplyAsync(() -> - StringUtils.convertToHtml(repo.getModChangelog(provider, version.getModid(), version.getVersionId())) + StringUtils.convertToHtml(repo.getAddonChangelog(provider, version.getModid(), version.getVersionId())) ).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { String changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); 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 c6b8116c6b3..71ed266e1d1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -104,7 +104,7 @@ default RemoteMod resolveDependency(DownloadProvider downloadProvider, String id Stream getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException; - String getModChangelog(DownloadProvider downloadProvider, String modId, String versionId) throws IOException; + String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException; String getVersionPageUrl(RemoteMod.Version version) 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 c23e9a3f73b..cbdc9f2d798 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 @@ -275,10 +275,10 @@ public Stream getRemoteVersionsById(DownloadProvider download } @Override - public String getModChangelog(DownloadProvider downloadProvider, String modId, String versionId) throws IOException { + public String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException { SEMAPHORE.acquireUninterruptibly(); try { - Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s/changelog", PREFIX, modId, versionId))) + Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s/changelog", PREFIX, addonId, 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 fc683cdbd02..a4c88b4449d 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 @@ -301,11 +301,30 @@ public Stream getRemoteVersionsById(DownloadProvider download } @Override - public String getModChangelog(DownloadProvider downloadProvider, String modId, String versionId) throws IOException { + public String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException { SEMAPHORE.acquireUninterruptibly(); try { - ProjectVersion version = HttpRequest.GET(PREFIX + "/v2/version/" + versionId).getJson(ProjectVersion.class); - return version.getChangelog(); + List candidates = downloadProvider.injectURLWithCandidates(PREFIX + "/v2/version/" + versionId); + IOException exception = null; + + for (URI uri : candidates) { + try { + ProjectVersion version = HttpRequest.GET(uri.toString()).getJson(ProjectVersion.class); + return version.getChangelog(); + } catch (IOException e) { + IOException wrapper = new IOException("Failed to get addon changelog: " + uri, e); + if (candidates.size() == 1) { + exception = wrapper; + } else { + if (exception == null) { + exception = new IOException("Failed to get addon changelog"); + } + exception.addSuppressed(wrapper); + } + } + } + + throw exception != null ? exception : new IOException("No candidates found"); } finally { SEMAPHORE.release(); } From f68f782355e7f5ffff1cdb3072952e75cdc577a3 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Tue, 23 Jun 2026 22:14:38 +0800 Subject: [PATCH 115/123] complete merge --- .../game/LocalizedRemoteAddonRepository.java | 2 +- .../hmcl/ui/versions/AddonUpdatesPage.java | 16 +- .../hmcl/ui/versions/DownloadPage.java | 14 +- .../resources/assets/lang/I18N.properties | 4 +- .../resources/assets/lang/I18N_lzh.properties | 3 +- .../resources/assets/lang/I18N_zh.properties | 4 +- .../assets/lang/I18N_zh_CN.properties | 4 +- .../jackhuang/hmcl/addon/LocalAddonFile.java | 4 +- .../org/jackhuang/hmcl/addon/RemoteAddon.java | 2 +- .../hmcl/addon/RemoteAddonRepository.java | 2 +- .../hmcl/addon/mod/LocalModFile.java | 2 +- .../CurseForgeRemoteAddonRepository.java | 74 +- .../ModrinthRemoteAddonRepository.java | 49 +- .../resourcepack/ResourcePackZipFile.java | 2 +- .../org/jackhuang/hmcl/mod/RemoteMod.java | 370 ------- .../jackhuang/hmcl/mod/curse/CurseAddon.java | 749 -------------- .../curse/CurseForgeRemoteModRepository.java | 481 --------- .../modrinth/ModrinthRemoteModRepository.java | 964 ------------------ 18 files changed, 143 insertions(+), 2603 deletions(-) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java index 1fc93542345..601c3fe5d40 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java @@ -153,7 +153,7 @@ public String getAddonChangelog(DownloadProvider downloadProvider, String addonI } @Override - public String getVersionPageUrl(RemoteMod.Version version) throws IOException { + public String getVersionPageUrl(RemoteAddon.Version version) throws IOException { return getBackedRemoteModRepository().getVersionPageUrl(version); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index 1edc6c0872c..f33e7f0f181 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -99,7 +99,7 @@ public AddonUpdatesPage(LocalAddonManager localAddonManager, List sourceColumn = new TableColumn<>(i18n("addon.check_update.source")); setupCellValueFactory(sourceColumn, AddonUpdateObject::sourceProperty); - TableColumn changelogColumn = new TableColumn<>(i18n("mods.changelog")); + TableColumn changelogColumn = new TableColumn<>(i18n("addon.changelog")); { var oldCellFactory = changelogColumn.getCellFactory(); changelogColumn.setCellFactory(param -> { @@ -302,9 +302,9 @@ public void setSource(String source) { private static final class AddonChangelog extends JFXDialogLayout { public AddonChangelog(AddonUpdateObject object) { - RemoteMod.Version targetVersion = object.data.targetVersion(); + RemoteAddon.Version targetVersion = object.data.targetVersion(); - this.setHeading(new HBox(new Label(i18n("mods.changelog") + " - " + targetVersion.getName()))); + this.setHeading(new HBox(new Label(i18n("addon.changelog") + " - " + targetVersion.name()))); VBox box = new VBox(8); box.setPadding(new Insets(8)); @@ -342,17 +342,17 @@ public AddonChangelog(AddonUpdateObject object) { private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) { spinnerPane.setLoading(true); - RemoteModRepository repo = object.data.source().getRepoForType(object.data.repoType()); + RemoteAddonRepository repo = object.data.source().getRepoForType(object.data.repoType()); Task.supplyAsync(() -> { if (object.changelog != null) { return object.changelog; } - RemoteMod.Version version = object.data.targetVersion(); + RemoteAddon.Version version = object.data.targetVersion(); if (repo == null) return null; - return StringUtils.convertToHtml(repo.getAddonChangelog(DownloadProviders.getDownloadProvider(), version.getModid(), version.getVersionId())); + return StringUtils.convertToHtml(repo.getAddonChangelog(DownloadProviders.getDownloadProvider(), version.modid(), version.versionId())); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - object.changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); + object.changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty"); scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, repo == null ? "" : repo.getBaseUrl())); FXUtils.smoothScrolling(scrollPane); spinnerPane.setFailedReason(null); @@ -365,7 +365,7 @@ private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, Sc private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) { Task.supplyAsync(() -> { - RemoteModRepository repo = object.data.source().getRepoForType(object.data.repoType()); + RemoteAddonRepository repo = object.data.source().getRepoForType(object.data.repoType()); return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion()); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null && StringUtils.isNotBlank(result)) { 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 625fee76364..ac2172b51c2 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 @@ -509,7 +509,7 @@ public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage s addonItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again box.getChildren().setAll(addonItem); - JFXHyperlink changelogButton = new JFXHyperlink(i18n("mods.changelog")); + JFXHyperlink changelogButton = new JFXHyperlink(i18n("addon.changelog")); changelogButton.setOnAction(__ -> Controllers.dialog(new AddonChangelog(version, selfPage.repository, selfPage.page.getDownloadProvider()))); JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); @@ -620,7 +620,7 @@ private void loadDependencies(RemoteAddon.Version version, DownloadPage selfPage }).start(); } - private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository repo, JFXHyperlink button) { + private void loadVersionPageUrl(RemoteAddon.Version version, RemoteAddonRepository repo, JFXHyperlink button) { Task.supplyAsync(() -> repo.getVersionPageUrl(version)) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null && StringUtils.isNotBlank(result)) { @@ -636,8 +636,8 @@ private void loadVersionPageUrl(RemoteMod.Version version, RemoteModRepository r private static final class AddonChangelog extends JFXDialogLayout { - public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo, DownloadProvider provider) { - setHeading(new HBox(new Label(i18n("mods.changelog") + " - " + version.getName()))); + public AddonChangelog(RemoteAddon.Version version, RemoteAddonRepository repo, DownloadProvider provider) { + setHeading(new HBox(new Label(i18n("addon.changelog") + " - " + version.name()))); VBox box = new VBox(8); box.setPadding(new Insets(8)); @@ -669,13 +669,13 @@ public AddonChangelog(RemoteMod.Version version, RemoteModRepository repo, Downl onEscPressed(this, closeButton::fire); } - private void loadChangelog(RemoteMod.Version version, RemoteModRepository repo, DownloadProvider provider, SpinnerPane spinnerPane, ScrollPane scrollPane) { + private void loadChangelog(RemoteAddon.Version version, RemoteAddonRepository repo, DownloadProvider provider, SpinnerPane spinnerPane, ScrollPane scrollPane) { spinnerPane.setLoading(true); Task.supplyAsync(() -> - StringUtils.convertToHtml(repo.getAddonChangelog(provider, version.getModid(), version.getVersionId())) + StringUtils.convertToHtml(repo.getAddonChangelog(provider, version.modid(), version.versionId())) ).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - String changelog = StringUtils.isNotBlank(result) ? result : i18n("mods.changelog.empty"); + String changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty"); scrollPane.setContent(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); FXUtils.smoothScrolling(scrollPane); spinnerPane.setFailedReason(null); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 01883e36833..eb5ff7aef9c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -175,8 +175,8 @@ account.username=Username addon.broken_dependency.title=Broken dependency addon.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source. addon.category=Category -mods.changelog=Changelog -mods.changelog.empty=Currently no changelog +addon.changelog=Changelog +addon.changelog.empty=Currently no changelog addon.channel.alpha=Alpha addon.channel.beta=Beta addon.channel.release=Release diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 8e8a3b94954..13c3221211a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -152,7 +152,7 @@ account.username=戶名 addon.broken_dependency.title=所依之壞者 addon.broken_dependency.desc=夫改囊素存於改囊庫,今闕矣,宜易他源。 addon.category=類 -mods.changelog=迭更誌 +addon.changelog=迭更誌 addon.channel.alpha=預版 addon.channel.beta=試版 addon.channel.release=當版 @@ -165,7 +165,6 @@ addon.check_update.failed_download=有引案未成 addon.check_update.file=案 addon.check_update.source=源 addon.check_update.target_version=將至之版 -mods.check_updates.update_mod=迭更改囊 - %1s addon.curseforge=CurseForge addon.modrinth=Modrinth diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index bf92cdecd0e..1af0c0c2707 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -173,8 +173,8 @@ account.username=使用者名稱 addon.broken_dependency.title=損壞的相依內容 addon.broken_dependency.desc=該相依內容曾經存在於附加內容下載源中,但現在已被刪除,請嘗試其他下載源。 addon.category=類別 -mods.changelog=更新日誌 -mods.changelog.empty=暫無更新日誌 +addon.changelog=更新日誌 +addon.changelog.empty=暫無更新日誌 addon.channel.alpha=Alpha addon.channel.beta=Beta addon.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 9b74173ab01..27fab7d11e6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -175,8 +175,8 @@ account.username=用户名 addon.broken_dependency.title=损坏的前置内容 addon.broken_dependency.desc=该前置内容曾经在该附加内容下载源上存在过,但现在被删除了。换个下载源试试吧。 addon.category=类别 -mods.changelog=更新日志 -mods.changelog.empty=暂无更新日志 +addon.changelog=更新日志 +addon.changelog.empty=暂无更新日志 addon.channel.alpha=快照版本 addon.channel.beta=测试版本 addon.channel.release=稳定版本 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java index 709defff691..5b093d6f29b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java @@ -60,8 +60,8 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe @NotNullByDefault public record AddonUpdate( - RemoteMod.Source source, - RemoteModRepository.Type repoType, + RemoteAddon.Source source, + RemoteAddonRepository.Type repoType, LocalAddonFile localAddonFile, RemoteAddon.Version currentVersion, RemoteAddon.Version targetVersion, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddon.java index e4b700a5284..db7791d5c86 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddon.java @@ -202,7 +202,7 @@ public interface IVersion { Source getType(); } - public record Version(IVersion self, String modid, String name, String version, String changelog, + public record Version(IVersion self, String versionId, String modid, String name, String version, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java index 7b36c3128eb..d0a06c41648 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java @@ -106,7 +106,7 @@ default RemoteAddon resolveDependency(DownloadProvider downloadProvider, String String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException; - String getVersionPageUrl(RemoteMod.Version version) throws IOException; + String getVersionPageUrl(RemoteAddon.Version version) throws IOException; Stream getCategories() throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java index 826720821e0..ceb2e711e0d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java @@ -207,7 +207,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe .sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed()) .toList(); if (remoteVersions.isEmpty()) return null; - return new AddonUpdate(source, RemoteModRepository.Type.MOD, this, currentVersion.get(), remoteVersions.get(0), true); + return new AddonUpdate(source, RemoteAddonRepository.Type.MOD, this, currentVersion.get(), remoteVersions.get(0), true); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java index 3d12959d30f..1db08ebeb7b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java @@ -52,6 +52,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonRepository { private static final String PREFIX = "https://api.curseforge.com"; + private static final String BASE = "https://www.curseforge.com"; private static final Semaphore SEMAPHORE = new Semaphore(16); public static final String API_KEY = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", "")); @@ -82,6 +83,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 return switch (sort) { @@ -138,7 +149,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion response = withApiKey(HttpRequest.GET(candidate.toString())) .getJson(Response.typeOf(listTypeOf(CurseAddon.class))); if (searchFilter.isEmpty()) { - return new SearchResult(response.data().stream().map(addon -> addon.toMod(type)), calculateTotalPages(response, pageSize)); + return new SearchResult(response.data().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); } break; } catch (IOException e) { @@ -167,7 +178,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator(); - return new SearchResult(response.data().stream().map(addon -> addon.toMod(type)).map(remoteMod -> { + return new SearchResult(response.data().stream().map(CurseAddon::toMod).map(remoteMod -> { String lowerCaseResult = remoteMod.title().toLowerCase(Locale.ROOT); int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult); @@ -178,7 +189,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } return pair(remoteMod, diff); - }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.data().stream().map(addon -> addon.toMod(type)), calculateTotalPages(response, pageSize)); + }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.data().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); } finally { SEMAPHORE.release(); } @@ -227,7 +238,7 @@ public RemoteAddon getModById(DownloadProvider downloadProvider, String id) thro try { Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id)) .getJson(Response.typeOf(CurseAddon.class)); - return response.data.toMod(type); + return response.data.toMod(); } finally { SEMAPHORE.release(); } @@ -258,6 +269,44 @@ public Stream getRemoteVersionsById(DownloadProvider downlo } } + @Override + public String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException { + SEMAPHORE.acquireUninterruptibly(); + try { + Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s/changelog", PREFIX, addonId, versionId))) + .getJson(Response.typeOf(String.class)); + return response.data(); + } finally { + SEMAPHORE.release(); + } + } + + @Override + public String getVersionPageUrl(RemoteAddon.Version version) throws IOException { + SEMAPHORE.acquireUninterruptibly(); + try { + Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + version.modid())) + .getJson(Response.typeOf(CurseAddon.class)); + var addon = response.data(); + var classId = addon.classId(); + 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_SHADER -> "shaders"; + default -> throw new IllegalArgumentException("Unsupported CurseForge class id [%d]".formatted(classId)); + }; + return "%s/minecraft/%s/%s/files/%s".formatted(BASE, clazz, addon.slug(), version.versionId()); + } finally { + SEMAPHORE.release(); + } + } + @Override public Stream getCategories() throws IOException { SEMAPHORE.acquireUninterruptibly(); @@ -295,6 +344,7 @@ private List reorganizeCategories(List categories, int rootI 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_WORLD = 17; public static final int SECTION_MODPACK = 4471; public static final int SECTION_SHADER = 6552; @@ -379,7 +429,7 @@ public Stream loadVersions(RemoteAddonRepository modReposit return modRepository.getRemoteVersionsById(downloadProvider, Integer.toString(id)); } - public RemoteAddon toMod(Type type) { + public RemoteAddon toMod() { String iconUrl = ""; if (logo != null) { if (StringUtils.isNotBlank(logo.thumbnailUrl())) @@ -388,6 +438,14 @@ else if (StringUtils.isNotBlank(logo.url())) iconUrl = logo.url(); } + RemoteAddonRepository.Type repoType = switch (classId) { + case SECTION_MODPACK -> RemoteAddonRepository.Type.MODPACK; + case SECTION_RESOURCE_PACK -> RemoteAddonRepository.Type.RESOURCE_PACK; + case SECTION_WORLD -> RemoteAddonRepository.Type.WORLD; + case SECTION_CUSTOMIZATION -> RemoteAddonRepository.Type.CUSTOMIZATION; + case SECTION_SHADER -> RemoteAddonRepository.Type.SHADER_PACK; + default -> RemoteAddonRepository.Type.MOD; + }; return new RemoteAddon( slug, "", @@ -397,7 +455,7 @@ else if (StringUtils.isNotBlank(logo.url())) links.websiteUrl, iconUrl, this, - type + repoType ); } @@ -468,10 +526,10 @@ public RemoteAddon.Version toVersion() { return new RemoteAddon.Version( this, - Integer.toString(modId), + Integer.toString(id()), + Integer.toString(modId()), displayName(), fileName(), - null, fileDate(), versionType, new RemoteAddon.File(Collections.emptyMap(), downloadUrl(), fileName()), diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java index c4deb57d091..20e9e1f1024 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java @@ -91,6 +91,8 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor private static final String PREFIX = "https://api.modrinth.com"; + private static final String BASE = "https://modrinth.com"; + private final String projectType; private final RemoteAddonRepository.Type type; @@ -110,6 +112,16 @@ public Type getType() { return this.type; } + @Override + public String getApiBaseUrl() { + return PREFIX; + } + + @Override + public String getBaseUrl() { + return BASE; + } + private static String convertSortType(SortType sortType) { return switch (sortType) { case DATE_CREATED -> "newest"; @@ -281,6 +293,41 @@ public Stream getRemoteVersionsById(DownloadProvider downlo } } + @Override + public String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException { + SEMAPHORE.acquireUninterruptibly(); + try { + List candidates = downloadProvider.injectURLWithCandidates(PREFIX + "/v2/version/" + versionId); + IOException exception = null; + + for (URI uri : candidates) { + try { + ProjectVersion version = HttpRequest.GET(uri.toString()).getJson(ProjectVersion.class); + return version.changelog(); + } catch (IOException e) { + IOException wrapper = new IOException("Failed to get addon changelog: " + uri, e); + if (candidates.size() == 1) { + exception = wrapper; + } else { + if (exception == null) { + exception = new IOException("Failed to get addon changelog"); + } + exception.addSuppressed(wrapper); + } + } + } + + throw exception != null ? exception : new IOException("No candidates found"); + } finally { + SEMAPHORE.release(); + } + } + + @Override + public String getVersionPageUrl(RemoteAddon.Version version) { + return "%s/mod/%s/version/%s".formatted(BASE, version.modid(), version.versionId()); // Modrinth will help us redirect + } + @Override public Stream getCategories() throws IOException { SEMAPHORE.acquireUninterruptibly(); @@ -398,10 +445,10 @@ public Optional toVersion() { return Optional.of(new RemoteAddon.Version( this, + id, projectId, name, versionNumber, - changelog, datePublished, type, files.get(0).toFile(), diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java index f1846fb9fa9..8439f7e242e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java @@ -101,7 +101,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe .sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed()) .toList(); if (remoteVersions.isEmpty()) return null; - return new AddonUpdate(source, RemoteModRepository.Type.RESOURCE_PACK, this, currentVersion.get(), remoteVersions.get(0), false); + return new AddonUpdate(source, RemoteAddonRepository.Type.RESOURCE_PACK, this, currentVersion.get(), remoteVersions.get(0), false); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java deleted file mode 100644 index 8114e29ec4c..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ /dev/null @@ -1,370 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.mod; - -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; -import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; -import org.jackhuang.hmcl.task.FileDownloadTask; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -public final class RemoteMod { - - public static final RemoteMod BROKEN = new RemoteMod("", "", "RemoteMod.BROKEN", "", Collections.emptyList(), "", "", new RemoteMod.IMod() { - @Override - public List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { - throw new IOException(); - } - - @Override - public Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { - throw new IOException(); - } - }, RemoteModRepository.Type.MOD); - - private final String slug; - private final String author; - private final String title; - private final String description; - private final List categories; - private final String pageUrl; - private final String iconUrl; - private final IMod data; - private final RemoteModRepository.Type repoType; - - public RemoteMod(String slug, String author, String title, String description, List categories, String pageUrl, String iconUrl, IMod data, RemoteModRepository.Type repoType) { - this.slug = slug; - this.author = author; - this.title = title; - this.description = description; - this.categories = categories; - this.pageUrl = pageUrl; - this.iconUrl = iconUrl; - this.data = data; - this.repoType = repoType; - } - - public String getSlug() { - return slug; - } - - public String getAuthor() { - return author; - } - - public String getTitle() { - return title; - } - - public String getDescription() { - return description; - } - - public List getCategories() { - return categories; - } - - public String getPageUrl() { - return pageUrl; - } - - public String getIconUrl() { - return iconUrl; - } - - public IMod getData() { - return data; - } - - public RemoteModRepository.Type getRepositoryType() { - return repoType; - } - - public enum VersionType { - Release, - Beta, - Alpha - } - - public enum DependencyType { - REQUIRED, - OPTIONAL, - TOOL, - INCLUDE, - EMBEDDED, - INCOMPATIBLE, - BROKEN - } - - public static final class Dependency { - private static Dependency BROKEN_DEPENDENCY = null; - - private final DependencyType type; - - private final RemoteModRepository remoteModRepository; - - private final String id; - - private transient RemoteMod remoteMod = null; - - private Dependency(DependencyType type, RemoteModRepository remoteModRepository, String modid) { - this.type = type; - this.remoteModRepository = remoteModRepository; - this.id = modid; - } - - public static Dependency ofGeneral(DependencyType type, RemoteModRepository remoteModRepository, String modid) { - if (type == DependencyType.BROKEN) { - return ofBroken(); - } else { - return new Dependency(type, remoteModRepository, modid); - } - } - - public static Dependency ofBroken() { - if (BROKEN_DEPENDENCY == null) { - BROKEN_DEPENDENCY = new Dependency(DependencyType.BROKEN, null, null); - } - return BROKEN_DEPENDENCY; - } - - public DependencyType getType() { - return this.type; - } - - public RemoteModRepository getRemoteModRepository() { - return this.remoteModRepository; - } - - public String getId() { - return this.id; - } - - public RemoteMod load(DownloadProvider downloadProvider) throws IOException { - if (this.remoteMod == null) { - if (this.type == DependencyType.BROKEN) { - this.remoteMod = RemoteMod.BROKEN; - } else { - this.remoteMod = this.remoteModRepository.resolveDependency(downloadProvider, this.id); - } - } - return this.remoteMod; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Dependency that = (Dependency) o; - - if (type != that.type) return false; - if (!remoteModRepository.equals(that.remoteModRepository)) return false; - return id.equals(that.id); - } - - @Override - public int hashCode() { - int result = type.hashCode(); - result = 31 * result + remoteModRepository.hashCode(); - result = 31 * result + id.hashCode(); - return result; - } - } - - public enum Source { - CURSEFORGE( - CurseForgeRemoteModRepository.MODS, - CurseForgeRemoteModRepository.RESOURCE_PACKS, - CurseForgeRemoteModRepository.SHADERS, - CurseForgeRemoteModRepository.WORLDS, - CurseForgeRemoteModRepository.MODPACKS, - CurseForgeRemoteModRepository.CUSTOMIZATIONS - ), - MODRINTH( - ModrinthRemoteModRepository.MODS, - ModrinthRemoteModRepository.RESOURCE_PACKS, - ModrinthRemoteModRepository.SHADER_PACKS, - null, - ModrinthRemoteModRepository.MODPACKS, - null - ); - - public final RemoteModRepository modRepo; - public final RemoteModRepository resourcePackRepo; - public final RemoteModRepository shaderPackRepo; - public final RemoteModRepository worldRepo; - public final RemoteModRepository modpackRepo; - public final RemoteModRepository customizationRepo; - - @Nullable - public RemoteModRepository getRepoForType(RemoteModRepository.Type type) { - return switch (type) { - case MOD -> modRepo; - case RESOURCE_PACK -> resourcePackRepo; - case SHADER_PACK -> shaderPackRepo; - case WORLD -> worldRepo; - case MODPACK -> modpackRepo; - case CUSTOMIZATION -> customizationRepo; - }; - } - - Source( - RemoteModRepository modRepo, - RemoteModRepository resourcePackRepo, - RemoteModRepository shaderPackRepo, - RemoteModRepository worldRepo, - RemoteModRepository modpackRepo, - RemoteModRepository customizationRepo - ) { - this.modRepo = modRepo; - this.resourcePackRepo = resourcePackRepo; - this.shaderPackRepo = shaderPackRepo; - this.worldRepo = worldRepo; - this.modpackRepo = modpackRepo; - this.customizationRepo = customizationRepo; - } - } - - public interface IMod { - List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException; - - Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException; - } - - public interface IVersion { - Source getType(); - } - - public static class Version { - private final IVersion self; - private final String versionId; - private final String modid; - private final String name; - private final String version; - private final Instant datePublished; - private final VersionType versionType; - private final File file; - private final List dependencies; - private final List gameVersions; - private final List loaders; - - public Version(IVersion self, String versionId, String modid, String name, String version, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { - this.versionId = versionId; - this.self = self; - this.modid = modid; - this.name = name; - this.version = version; - this.datePublished = datePublished; - this.versionType = versionType; - this.file = file; - this.dependencies = dependencies; - this.gameVersions = gameVersions; - this.loaders = loaders; - } - - public IVersion getSelf() { - return self; - } - - public String getVersionId() { - return versionId; - } - - public String getModid() { - return modid; - } - - public String getName() { - return name; - } - - public String getVersion() { - return version; - } - - public Instant getDatePublished() { - return datePublished; - } - - public VersionType getVersionType() { - return versionType; - } - - public File getFile() { - return file; - } - - public List getDependencies() { - return dependencies; - } - - public List getGameVersions() { - return gameVersions; - } - - public List getLoaders() { - return loaders; - } - } - - public static class File { - private final Map hashes; - private final String url; - private final String filename; - - public File(Map hashes, String url, String filename) { - this.hashes = hashes; - this.url = url; - this.filename = filename; - } - - public Map getHashes() { - return hashes; - } - - public FileDownloadTask.IntegrityCheck getIntegrityCheck() { - if (hashes.containsKey("md5")) { - return new FileDownloadTask.IntegrityCheck("MD5", hashes.get("md5")); - } else if (hashes.containsKey("sha1")) { - return new FileDownloadTask.IntegrityCheck("SHA-1", hashes.get("sha1")); - } else if (hashes.containsKey("sha256")) { - return new FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha256")); - } else if (hashes.containsKey("sha512")) { - return new FileDownloadTask.IntegrityCheck("SHA-512", hashes.get("sha512")); - } else { - return null; - } - } - - public String getUrl() { - return url; - } - - public String getFilename() { - return filename; - } - } -} 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 deleted file mode 100644 index 97298c5069b..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ /dev/null @@ -1,749 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.mod.curse; - -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.mod.ModLoaderType; -import org.jackhuang.hmcl.mod.RemoteMod; -import org.jackhuang.hmcl.mod.RemoteModRepository; -import org.jackhuang.hmcl.util.Immutable; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.Pair; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.time.Instant; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@Immutable -public class CurseAddon implements RemoteMod.IMod { - public static final Map RELATION_TYPE = Lang.mapOf( - Pair.pair(1, RemoteMod.DependencyType.EMBEDDED), - Pair.pair(2, RemoteMod.DependencyType.OPTIONAL), - Pair.pair(3, RemoteMod.DependencyType.REQUIRED), - Pair.pair(4, RemoteMod.DependencyType.TOOL), - Pair.pair(5, RemoteMod.DependencyType.INCOMPATIBLE), - Pair.pair(6, RemoteMod.DependencyType.INCLUDE) - ); - - private final int id; - private final int gameId; - private final String name; - private final String slug; - private final Links links; - private final String summary; - private final int status; - private final int downloadCount; - private final boolean isFeatured; - private final int primaryCategoryId; - private final List categories; - private final int classId; - private final List authors; - private final Logo logo; - private final int mainFileId; - private final List latestFiles; - private final List latestFileIndices; - private final Instant dateCreated; - private final Instant dateModified; - private final Instant dateReleased; - private final boolean allowModDistribution; - private final int gamePopularityRank; - private final boolean isAvailable; - private final int thumbsUpCount; - - public CurseAddon(int id, int gameId, String name, String slug, Links links, String summary, int status, int downloadCount, boolean isFeatured, int primaryCategoryId, List categories, int classId, List authors, Logo logo, int mainFileId, List latestFiles, List latestFileIndices, Instant dateCreated, Instant dateModified, Instant dateReleased, boolean allowModDistribution, int gamePopularityRank, boolean isAvailable, int thumbsUpCount) { - this.id = id; - this.gameId = gameId; - this.name = name; - this.slug = slug; - this.links = links; - this.summary = summary; - this.status = status; - this.downloadCount = downloadCount; - this.isFeatured = isFeatured; - this.primaryCategoryId = primaryCategoryId; - this.categories = categories; - this.classId = classId; - this.authors = authors; - this.logo = logo; - this.mainFileId = mainFileId; - this.latestFiles = latestFiles; - this.latestFileIndices = latestFileIndices; - this.dateCreated = dateCreated; - this.dateModified = dateModified; - this.dateReleased = dateReleased; - this.allowModDistribution = allowModDistribution; - this.gamePopularityRank = gamePopularityRank; - this.isAvailable = isAvailable; - this.thumbsUpCount = thumbsUpCount; - } - - public int getId() { - return id; - } - - public int getGameId() { - return gameId; - } - - public String getName() { - return name; - } - - public String getSlug() { - return slug; - } - - public Links getLinks() { - return links; - } - - public String getSummary() { - return summary; - } - - public int getStatus() { - return status; - } - - public int getDownloadCount() { - return downloadCount; - } - - public boolean isFeatured() { - return isFeatured; - } - - public int getPrimaryCategoryId() { - return primaryCategoryId; - } - - public List getCategories() { - return categories; - } - - public int getClassId() { - return classId; - } - - public List getAuthors() { - return authors; - } - - public Logo getLogo() { - return logo; - } - - public int getMainFileId() { - return mainFileId; - } - - public List getLatestFiles() { - return latestFiles; - } - - public List getLatestFileIndices() { - return latestFileIndices; - } - - public Instant getDateCreated() { - return dateCreated; - } - - public Instant getDateModified() { - return dateModified; - } - - public Instant getDateReleased() { - return dateReleased; - } - - public boolean isAllowModDistribution() { - return allowModDistribution; - } - - public int getGamePopularityRank() { - return gamePopularityRank; - } - - public boolean isAvailable() { - return isAvailable; - } - - public int getThumbsUpCount() { - return thumbsUpCount; - } - - @Override - public List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { - Set dependencies = latestFiles.stream() - .flatMap(latestFile -> latestFile.getDependencies().stream()) - .filter(dep -> dep.getRelationType() == 3) - .map(Dependency::getModId) - .collect(Collectors.toSet()); - List mods = new ArrayList<>(); - for (int dependencyId : dependencies) { - mods.add(modRepository.getModById(downloadProvider, Integer.toString(dependencyId))); - } - return mods; - } - - @Override - public Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { - return modRepository.getRemoteVersionsById(downloadProvider, Integer.toString(id)); - } - - public RemoteMod toMod() { - String iconUrl = ""; - if (logo != null) { - if (StringUtils.isNotBlank(logo.getThumbnailUrl())) - iconUrl = logo.getThumbnailUrl(); - else if (StringUtils.isNotBlank(logo.getUrl())) - iconUrl = logo.getUrl(); - } - - RemoteModRepository.Type repoType = switch (classId) { - case CurseForgeRemoteModRepository.SECTION_MODPACK -> RemoteModRepository.Type.MODPACK; - case CurseForgeRemoteModRepository.SECTION_RESOURCE_PACK -> RemoteModRepository.Type.RESOURCE_PACK; - case CurseForgeRemoteModRepository.SECTION_WORLD -> RemoteModRepository.Type.WORLD; - case CurseForgeRemoteModRepository.SECTION_CUSTOMIZATION -> RemoteModRepository.Type.CUSTOMIZATION; - case CurseForgeRemoteModRepository.SECTION_SHADER -> RemoteModRepository.Type.SHADER_PACK; - default -> RemoteModRepository.Type.MOD; - }; - return new RemoteMod( - slug, - "", - name, - summary, - categories.stream().map(category -> Integer.toString(category.getId())).collect(Collectors.toList()), - links.websiteUrl, - iconUrl, - this, - repoType - ); - } - - @Immutable - public static class Links { - private final String websiteUrl; - private final String wikiUrl; - private final String issuesUrl; - private final String sourceUrl; - - public Links(String websiteUrl, String wikiUrl, String issuesUrl, String sourceUrl) { - this.websiteUrl = websiteUrl; - this.wikiUrl = wikiUrl; - this.issuesUrl = issuesUrl; - this.sourceUrl = sourceUrl; - } - - public String getWebsiteUrl() { - return websiteUrl; - } - - public String getWikiUrl() { - return wikiUrl; - } - - @Nullable - public String getIssuesUrl() { - return issuesUrl; - } - - @Nullable - public String getSourceUrl() { - return sourceUrl; - } - } - - @Immutable - public static class Author { - private final int id; - private final String name; - private final String url; - - public Author(int id, String name, String url) { - this.id = id; - this.name = name; - this.url = url; - } - - public int getId() { - return id; - } - - public String getName() { - return name; - } - - public String getUrl() { - return url; - } - } - - @Immutable - public static class Logo { - private final int id; - private final int modId; - private final String title; - private final String description; - private final String thumbnailUrl; - private final String url; - - public Logo(int id, int modId, String title, String description, String thumbnailUrl, String url) { - this.id = id; - this.modId = modId; - this.title = title; - this.description = description; - this.thumbnailUrl = thumbnailUrl; - this.url = url; - } - - public int getId() { - return id; - } - - public int getModId() { - return modId; - } - - public String getTitle() { - return title; - } - - public String getDescription() { - return description; - } - - public String getThumbnailUrl() { - return thumbnailUrl; - } - - public String getUrl() { - return url; - } - } - - @Immutable - public static class Attachment { - private final int id; - private final int projectId; - private final String description; - private final boolean isDefault; - private final String thumbnailUrl; - private final String title; - private final String url; - private final int status; - - public Attachment(int id, int projectId, String description, boolean isDefault, String thumbnailUrl, String title, String url, int status) { - this.id = id; - this.projectId = projectId; - this.description = description; - this.isDefault = isDefault; - this.thumbnailUrl = thumbnailUrl; - this.title = title; - this.url = url; - this.status = status; - } - - public int getId() { - return id; - } - - public int getProjectId() { - return projectId; - } - - public String getDescription() { - return description; - } - - public boolean isDefault() { - return isDefault; - } - - public String getThumbnailUrl() { - return thumbnailUrl; - } - - public String getTitle() { - return title; - } - - public String getUrl() { - return url; - } - - public int getStatus() { - return status; - } - } - - @Immutable - public static class Dependency { - private final int modId; - private final int relationType; - - public Dependency() { - this(0, 1); - } - - public Dependency(int modId, int relationType) { - this.modId = modId; - this.relationType = relationType; - } - - public int getModId() { - return modId; - } - - public int getRelationType() { - return relationType; - } - } - - /** - * @see Schema - */ - @Immutable - public static class LatestFileHash { - private final String value; - private final int algo; - - public LatestFileHash(String value, int algo) { - this.value = value; - this.algo = algo; - } - - public String getValue() { - return value; - } - - public int getAlgo() { - return algo; - } - } - - /** - * @see Schema - */ - @Immutable - public static class LatestFile implements RemoteMod.IVersion { - private final int id; - private final int gameId; - private final int modId; - private final boolean isAvailable; - private final String displayName; - private final String fileName; - private final int releaseType; - private final int fileStatus; - private final List hashes; - private final Instant fileDate; - private final int fileLength; - private final int downloadCount; - private final String downloadUrl; - private final List gameVersions; - private final List dependencies; - private final int alternateFileId; - private final boolean isServerPack; - private final long fileFingerprint; - - public LatestFile(int id, int gameId, int modId, boolean isAvailable, String displayName, String fileName, int releaseType, int fileStatus, List hashes, Instant fileDate, int fileLength, int downloadCount, String downloadUrl, List gameVersions, List dependencies, int alternateFileId, boolean isServerPack, long fileFingerprint) { - this.id = id; - this.gameId = gameId; - this.modId = modId; - this.isAvailable = isAvailable; - this.displayName = displayName; - this.fileName = fileName; - this.releaseType = releaseType; - this.fileStatus = fileStatus; - this.hashes = hashes; - this.fileDate = fileDate; - this.fileLength = fileLength; - this.downloadCount = downloadCount; - this.downloadUrl = downloadUrl; - this.gameVersions = gameVersions; - this.dependencies = dependencies; - this.alternateFileId = alternateFileId; - this.isServerPack = isServerPack; - this.fileFingerprint = fileFingerprint; - } - - public int getId() { - return id; - } - - public int getGameId() { - return gameId; - } - - public int getModId() { - return modId; - } - - public boolean isAvailable() { - return isAvailable; - } - - public String getDisplayName() { - return displayName; - } - - public String getFileName() { - return fileName; - } - - public int getReleaseType() { - return releaseType; - } - - public int getFileStatus() { - return fileStatus; - } - - public List getHashes() { - return hashes; - } - - public Instant getFileDate() { - return fileDate; - } - - public int getFileLength() { - return fileLength; - } - - public int getDownloadCount() { - return downloadCount; - } - - public String getDownloadUrl() { - if (downloadUrl == null) { - // This addon is not allowed for distribution, and downloadUrl will be null. - // We try to find its download url. - return String.format("https://edge.forgecdn.net/files/%d/%d/%s", id / 1000, id % 1000, fileName); - } - return downloadUrl; - } - - public List getGameVersions() { - return gameVersions; - } - - public List getDependencies() { - return dependencies; - } - - public int getAlternateFileId() { - return alternateFileId; - } - - public boolean isServerPack() { - return isServerPack; - } - - public long getFileFingerprint() { - return fileFingerprint; - } - - @Override - public RemoteMod.Source getType() { - return RemoteMod.Source.CURSEFORGE; - } - - public RemoteMod.Version toVersion() { - RemoteMod.VersionType versionType; - switch (getReleaseType()) { - case 1: - versionType = RemoteMod.VersionType.Release; - break; - case 2: - versionType = RemoteMod.VersionType.Beta; - break; - case 3: - versionType = RemoteMod.VersionType.Alpha; - break; - default: - versionType = RemoteMod.VersionType.Release; - break; - } - - return new RemoteMod.Version( - this, - Integer.toString(getId()), - Integer.toString(modId), - getDisplayName(), - getFileName(), - getFileDate(), - versionType, - new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), - dependencies.stream().map(dependency -> { - if (!RELATION_TYPE.containsKey(dependency.getRelationType())) { - throw new IllegalStateException("Broken datas."); - } - return RemoteMod.Dependency.ofGeneral(RELATION_TYPE.get(dependency.getRelationType()), CurseForgeRemoteModRepository.MODS, Integer.toString(dependency.getModId())); - }).distinct().filter(Objects::nonNull).collect(Collectors.toList()), - gameVersions.stream().filter(GameVersionNumber::isKnown).toList(), - gameVersions.stream().flatMap(version -> { - if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC); - else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE); - else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT); - else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGE); - else return Stream.empty(); - }).collect(Collectors.toList()) - ); - } - } - - /** - * @see Schema - */ - @Immutable - public static class LatestFileIndex { - private final String gameVersion; - private final int fileId; - private final String filename; - private final int releaseType; - private final int gameVersionTypeId; - private final int modLoader; - - public LatestFileIndex(String gameVersion, int fileId, String filename, int releaseType, int gameVersionTypeId, int modLoader) { - this.gameVersion = gameVersion; - this.fileId = fileId; - this.filename = filename; - this.releaseType = releaseType; - this.gameVersionTypeId = gameVersionTypeId; - this.modLoader = modLoader; - } - - public String getGameVersion() { - return gameVersion; - } - - public int getFileId() { - return fileId; - } - - public String getFilename() { - return filename; - } - - public int getReleaseType() { - return releaseType; - } - - @Nullable - public int getGameVersionTypeId() { - return gameVersionTypeId; - } - - public int getModLoader() { - return modLoader; - } - } - - @Immutable - public static class Category { - private final int id; - private final int gameId; - private final String name; - private final String slug; - private final String url; - private final String iconUrl; - private final Instant dateModified; - private final boolean isClass; - private final int classId; - private final int parentCategoryId; - - private transient final List subcategories; - - public Category() { - this(0, 0, "", "", "", "", Instant.now(), false, 0, 0); - } - - public Category(int id, int gameId, String name, String slug, String url, String iconUrl, Instant dateModified, boolean isClass, int classId, int parentCategoryId) { - this.id = id; - this.gameId = gameId; - this.name = name; - this.slug = slug; - this.url = url; - this.iconUrl = iconUrl; - this.dateModified = dateModified; - this.isClass = isClass; - this.classId = classId; - this.parentCategoryId = parentCategoryId; - - this.subcategories = new ArrayList<>(); - } - - public int getId() { - return id; - } - - public int getGameId() { - return gameId; - } - - public String getName() { - return name; - } - - public String getSlug() { - return slug; - } - - public String getUrl() { - return url; - } - - public String getIconUrl() { - return iconUrl; - } - - public Instant getDateModified() { - return dateModified; - } - - public boolean isClass() { - return isClass; - } - - public int getClassId() { - return classId; - } - - public int getParentCategoryId() { - return parentCategoryId; - } - - public List getSubcategories() { - return subcategories; - } - - public RemoteModRepository.Category toCategory() { - return new RemoteModRepository.Category( - this, - Integer.toString(id), - getSubcategories().stream().map(Category::toCategory).collect(Collectors.toList())); - } - } -} 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 deleted file mode 100644 index cbdc9f2d798..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ /dev/null @@ -1,481 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.mod.curse; - -import com.google.gson.reflect.TypeToken; -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.mod.RemoteMod; -import org.jackhuang.hmcl.mod.RemoteModRepository; -import org.jackhuang.hmcl.util.MurmurHash2; -import org.jackhuang.hmcl.util.Pair; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.io.HttpRequest; -import org.jackhuang.hmcl.util.io.JarUtils; -import org.jackhuang.hmcl.util.io.NetworkUtils; -import org.jetbrains.annotations.Nullable; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -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; -import static org.jackhuang.hmcl.util.Pair.pair; -import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -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 Semaphore SEMAPHORE = new Semaphore(16); - - public static final String API_KEY = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", "")); - - private static final int WORD_PERFECT_MATCH_WEIGHT = 5; - - private static R withApiKey(R request) { - if (request.getUrl().startsWith(PREFIX) && !API_KEY.isEmpty()) { - request.header("X-API-KEY", API_KEY); - } - return request; - } - - public static boolean isAvailable() { - return !API_KEY.isEmpty(); - } - - private final Type type; - private final int section; - - public CurseForgeRemoteModRepository(Type type, int section) { - this.type = type; - this.section = section; - } - - @Override - 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) { - case DATE_CREATED: - return 1; - case POPULARITY: - return 2; - case LAST_UPDATED: - return 3; - case NAME: - return 4; - case AUTHOR: - return 5; - case TOTAL_DOWNLOADS: - return 6; - default: - return 8; - } - } - - private String toSortOrder(SortOrder sortOrder) { - // https://docs.curseforge.com/#tocS_SortOrder - switch (sortOrder) { - case ASC: - return "asc"; - case DESC: - return "desc"; - } - return "asc"; - } - - private int calculateTotalPages(Response> response, int pageSize) { - return (int) Math.ceil((double) Math.min(response.pagination.totalCount, 10000) / pageSize); - } - - @Override - public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - int categoryId = 0; - if (category != null && category.self() instanceof CurseAddon.Category) { - categoryId = ((CurseAddon.Category) category.self()).getId(); - } - - var query = new LinkedHashMap(); - query.put("gameId", "432"); - query.put("classId", Integer.toString(section)); - if (categoryId != 0) - query.put("categoryId", Integer.toString(categoryId)); - query.put("gameVersion", gameVersion); - query.put("searchFilter", searchFilter); - query.put("sortField", Integer.toString(toModsSearchSortField(sortType))); - query.put("sortOrder", toSortOrder(sortOrder)); - query.put("index", Integer.toString(pageOffset * pageSize)); - query.put("pageSize", Integer.toString(pageSize)); - - Response> response = null; - - IOException exception = null; - List candidates = downloadProvider.injectURLWithCandidates(NetworkUtils.withQuery(PREFIX + "/v1/mods/search", query)); - for (URI candidate : candidates) { - LOG.info("Fetching " + candidate); - try { - response = withApiKey(HttpRequest.GET(candidate.toString())) - .getJson(Response.typeOf(listTypeOf(CurseAddon.class))); - if (searchFilter.isEmpty()) { - return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); - } - break; - } catch (IOException e) { - LOG.warning("Failed to search addons: " + candidate, e); - if (candidates.size() == 1) { - exception = e; - } else { - if (exception == null) { - exception = new IOException("Failed to search addons"); - } - exception.addSuppressed(e); - } - } - } - - if (response == null) { - throw exception != null ? exception : new IOException("No candidates found"); - } - - // https://github.com/HMCL-dev/HMCL/issues/1549 - String lowerCaseSearchFilter = searchFilter.toLowerCase(Locale.ROOT); - Map searchFilterWords = new HashMap<>(); - for (String s : StringUtils.tokenize(lowerCaseSearchFilter)) { - searchFilterWords.put(s, searchFilterWords.getOrDefault(s, 0) + 1); - } - - StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator(); - - return new SearchResult(response.getData().stream().map(CurseAddon::toMod).map(remoteMod -> { - String lowerCaseResult = remoteMod.getTitle().toLowerCase(Locale.ROOT); - int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult); - - for (String s : StringUtils.tokenize(lowerCaseResult)) { - if (searchFilterWords.containsKey(s)) { - diff -= WORD_PERFECT_MATCH_WEIGHT * searchFilterWords.get(s) * s.length(); - } - } - - return pair(remoteMod, diff); - }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public Optional getRemoteVersionByLocalFile(Path file) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (InputStream stream = Files.newInputStream(file)) { - byte[] buf = new byte[1024]; - int len; - while ((len = stream.read(buf, 0, buf.length)) != -1) { - for (int i = 0; i < len; i++) { - byte b = buf[i]; - if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) { - baos.write(b); - } - } - } - } - - long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1)); - if (hash == 811513880) { // Workaround for https://github.com/HMCL-dev/HMCL/issues/4597 - return Optional.empty(); - } - - SEMAPHORE.acquireUninterruptibly(); - try { - Response response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432")) - .json(mapOf(pair("fingerprints", Collections.singletonList(hash)))) - .getJson(Response.typeOf(FingerprintMatchesResult.class)); - - if (response.getData().getExactMatches() == null || response.getData().getExactMatches().isEmpty()) { - return Optional.empty(); - } - - return Optional.of(response.getData().getExactMatches().get(0).getFile().toVersion()); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id)) - .getJson(Response.typeOf(CurseAddon.class)); - return response.data.toMod(); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public RemoteMod.File getModFile(String modId, String fileId) throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId))) - .getJson(Response.typeOf(CurseAddon.LatestFile.class)); - return response.getData().toVersion().getFile(); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public Stream getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - Response> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files", - pair("pageSize", "10000"))) - .getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class))); - return response.getData().stream().map(CurseAddon.LatestFile::toVersion); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s/changelog", PREFIX, addonId, versionId))) - .getJson(Response.typeOf(String.class)); - return response.getData(); - } finally { - SEMAPHORE.release(); - } - } - - @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_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()); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public Stream getCategories() throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - Response> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432"))) - .getJson(Response.typeOf(listTypeOf(CurseAddon.Category.class))); - return reorganizeCategories(categories.getData(), section).stream().map(CurseAddon.Category::toCategory); - } finally { - SEMAPHORE.release(); - } - } - - private List reorganizeCategories(List categories, int rootId) { - List result = new ArrayList<>(); - - Map categoryMap = new HashMap<>(); - for (CurseAddon.Category category : categories) { - categoryMap.put(category.getId(), category); - } - for (CurseAddon.Category category : categories) { - if (category.getParentCategoryId() == rootId) { - result.add(category); - } else { - CurseAddon.Category parentCategory = categoryMap.get(category.getParentCategoryId()); - if (parentCategory == null) { - // Category list is not correct, so we ignore this item. - continue; - } - parentCategory.getSubcategories().add(category); - } - } - return result; - } - - 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_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; - public static final int SECTION_UNKNOWN2 = 4979; - public static final int SECTION_UNKNOWN3 = 4984; - - public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MOD, SECTION_MOD); - public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MODPACK, SECTION_MODPACK); - public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.RESOURCE_PACK, SECTION_RESOURCE_PACK); - public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.WORLD, SECTION_WORLD); - public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.CUSTOMIZATION, SECTION_CUSTOMIZATION); - public static final CurseForgeRemoteModRepository SHADERS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.SHADER_PACK, SECTION_SHADER); - - public static class Pagination { - private final int index; - private final int pageSize; - private final int resultCount; - private final int totalCount; - - public Pagination(int index, int pageSize, int resultCount, int totalCount) { - this.index = index; - this.pageSize = pageSize; - this.resultCount = resultCount; - this.totalCount = totalCount; - } - - public int getIndex() { - return index; - } - - public int getPageSize() { - return pageSize; - } - - public int getResultCount() { - return resultCount; - } - - public int getTotalCount() { - return totalCount; - } - } - - public static class Response { - - @SuppressWarnings("unchecked") - public static TypeToken> typeOf(Class responseType) { - return (TypeToken>) TypeToken.getParameterized(Response.class, responseType); - } - - @SuppressWarnings("unchecked") - public static TypeToken> typeOf(TypeToken responseType) { - return (TypeToken>) TypeToken.getParameterized(Response.class, responseType.getType()); - } - - private final T data; - private final Pagination pagination; - - public Response(T data, Pagination pagination) { - this.data = data; - this.pagination = pagination; - } - - public T getData() { - return data; - } - - public Pagination getPagination() { - return pagination; - } - } - - /** - * @see Schema - */ - private static class FingerprintMatchesResult { - private final boolean isCacheBuilt; - private final List exactMatches; - private final List exactFingerprints; - - public FingerprintMatchesResult(boolean isCacheBuilt, List exactMatches, List exactFingerprints) { - this.isCacheBuilt = isCacheBuilt; - this.exactMatches = exactMatches; - this.exactFingerprints = exactFingerprints; - } - - public boolean isCacheBuilt() { - return isCacheBuilt; - } - - public List getExactMatches() { - return exactMatches; - } - - public List getExactFingerprints() { - return exactFingerprints; - } - } - - /** - * @see Schema - */ - private static class FingerprintMatch { - private final int id; - private final CurseAddon.LatestFile file; - private final List latestFiles; - - public FingerprintMatch(int id, CurseAddon.LatestFile file, List latestFiles) { - this.id = id; - this.file = file; - this.latestFiles = latestFiles; - } - - public int getId() { - return id; - } - - public CurseAddon.LatestFile getFile() { - return file; - } - - public List getLatestFiles() { - return latestFiles; - } - } -} 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 deleted file mode 100644 index a4c88b4449d..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ /dev/null @@ -1,964 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.mod.modrinth; - -import com.google.gson.annotations.SerializedName; -import com.google.gson.reflect.TypeToken; -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.mod.ModLoaderType; -import org.jackhuang.hmcl.mod.RemoteMod; -import org.jackhuang.hmcl.mod.RemoteModRepository; -import org.jackhuang.hmcl.util.*; -import org.jackhuang.hmcl.util.gson.JsonUtils; -import org.jackhuang.hmcl.util.io.HttpRequest; -import org.jackhuang.hmcl.util.io.NetworkUtils; -import org.jackhuang.hmcl.util.io.ResponseCodeException; -import org.jetbrains.annotations.Nullable; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.URI; -import java.nio.file.NoSuchFileException; -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; - -import static org.jackhuang.hmcl.util.Lang.mapOf; -import static org.jackhuang.hmcl.util.Pair.pair; -import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class ModrinthRemoteModRepository implements RemoteModRepository { - public static final ModrinthRemoteModRepository MODS = new ModrinthRemoteModRepository("mod"); - public static final ModrinthRemoteModRepository MODPACKS = new ModrinthRemoteModRepository("modpack"); - public static final ModrinthRemoteModRepository RESOURCE_PACKS = new ModrinthRemoteModRepository("resourcepack"); - public static final ModrinthRemoteModRepository SHADER_PACKS = new ModrinthRemoteModRepository("shader"); - - private static final Comparator TAG_COMPARATOR = PriorityComparator.of( - List.of("babric", - "bta-babric", - "bukkit", - "bungeecord", - "canvas", - "datapack", - "fabric", - "folia", - "forge", - "geyser", - "iris", - "java-agent", - "legacy-fabric", - "liteloader", - "minecraft", - "modloader", - "mrpack", - "neoforge", - "nilloader", - "optifine", - "ornith", - "paper", - "purpur", - "quilt", - "rift", - "spigot", - "sponge", - "vanilla", - "velocity", - "waterfall"), - Comparator.naturalOrder(), - false - ); - - private static final Semaphore SEMAPHORE = new Semaphore(16); - - private static final String PREFIX = "https://api.modrinth.com"; - - private static final String BASE = "https://modrinth.com"; - - private final String projectType; - - private final RemoteModRepository.Type type; - - private ModrinthRemoteModRepository(String projectType) { - this.projectType = projectType; - this.type = switch (projectType) { - case "modpack" -> Type.MODPACK; - case "resourcepack" -> Type.RESOURCE_PACK; - case "shader" -> Type.SHADER_PACK; - default -> Type.MOD; - }; - } - - @Override - public Type getType() { - return this.type; - } - - @Override - public String getApiBaseUrl() { - return PREFIX; - } - - @Override - public String getBaseUrl() { - return BASE; - } - - private static String convertSortType(SortType sortType) { - switch (sortType) { - case DATE_CREATED: - return "newest"; - case POPULARITY: - case NAME: - case AUTHOR: - return "relevance"; - case LAST_UPDATED: - return "updated"; - case TOTAL_DOWNLOADS: - return "downloads"; - default: - throw new IllegalArgumentException("Unsupported sort type " + sortType); - } - } - - static List sortDisplayCategories(List displayCategories) { - return displayCategories != null && !displayCategories.isEmpty() - ? displayCategories.stream().sorted(TAG_COMPARATOR).toList() - : List.of(); - } - - @Override - public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - List> facets = new ArrayList<>(); - facets.add(Collections.singletonList("project_type:" + projectType)); - if (StringUtils.isNotBlank(gameVersion)) { - facets.add(Collections.singletonList("versions:" + gameVersion)); - } - if (category != null && StringUtils.isNotBlank(category.id())) { - facets.add(Collections.singletonList("categories:" + category.id())); - } - Map query = mapOf( - pair("query", searchFilter), - pair("facets", JsonUtils.UGLY_GSON.toJson(facets)), - pair("offset", Integer.toString(pageOffset * pageSize)), - pair("limit", Integer.toString(pageSize)), - pair("index", convertSortType(sort)) - ); - - List candidates = downloadProvider.injectURLWithCandidates(NetworkUtils.withQuery(PREFIX + "/v2/search", query)); - IOException exception = null; - for (URI candidate : candidates) { - try { - LOG.info("Fetching " + candidate); - Response response = HttpRequest.GET(candidate.toString()) - .getJson(Response.typeOf(ProjectSearchResult.class)); - return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize)); - } catch (IOException e) { - LOG.warning("Failed to search addons: " + candidate, e); - - IOException wrapper = new IOException("Failed to search addons: " + candidate, e); - if (candidates.size() == 1) { - exception = wrapper; - } else { - if (exception == null) { - exception = new IOException("Failed to search addons"); - } - exception.addSuppressed(wrapper); - } - } - } - - throw exception != null ? exception : new IOException("No candidates found"); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public Optional getRemoteVersionByLocalFile(Path file) throws IOException { - String sha1 = DigestUtils.digestToString("SHA-1", file); - - SEMAPHORE.acquireUninterruptibly(); - try { - ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1, - pair("algorithm", "sha1")) - .getJson(ProjectVersion.class); - return mod.toVersion(); - } catch (ResponseCodeException e) { - if (e.getResponseCode() == 404) { - return Optional.empty(); - } else { - throw e; - } - } catch (NoSuchFileException e) { - return Optional.empty(); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - id = StringUtils.removePrefix(id, "local-"); - List candidates = downloadProvider.injectURLWithCandidates(PREFIX + "/v2/project/" + id); - IOException exception = null; - - for (URI candidate : candidates) { - try { - Project project = HttpRequest.GET(candidate.toString()).getJson(Project.class); - return project.toMod(); - } catch (IOException e) { - IOException wrapper = new IOException("Failed to get mod: " + candidate, e); - if (candidates.size() == 1) { - exception = wrapper; - } else { - if (exception == null) { - exception = new IOException("Failed to get mod"); - } - exception.addSuppressed(wrapper); - } - } - } - - throw exception != null ? exception : new IOException("No candidates found"); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public RemoteMod resolveDependency(DownloadProvider downloadProvider, String id) throws IOException { - try { - return getModById(downloadProvider, id); - } catch (ResponseCodeException e) { - if (e.getResponseCode() == 502 || e.getResponseCode() == 404) { - return RemoteMod.BROKEN; - } - throw e; - } catch (FileNotFoundException e) { - return RemoteMod.BROKEN; - } - } - - @Override - public RemoteMod.File getModFile(String modId, String fileId) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public Stream getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - id = StringUtils.removePrefix(id, "local-"); - - List candidates = downloadProvider.injectURLWithCandidates(PREFIX + "/v2/project/" + id + "/version?include_changelog=false"); - IOException exception = null; - - for (URI candidate : candidates) { - try { - List versions = HttpRequest.GET(candidate.toString()) - .getJson(listTypeOf(ProjectVersion.class)); - return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream); - } catch (IOException e) { - IOException wrapper = new IOException("Failed to get remote versions: " + candidate, e); - if (candidates.size() == 1) { - exception = wrapper; - } else { - if (exception == null) { - exception = new IOException("Failed to get remote versions"); - } - exception.addSuppressed(wrapper); - } - } - } - - throw exception != null ? exception : new IOException("No candidates found"); - } finally { - SEMAPHORE.release(); - } - } - - @Override - public String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException { - SEMAPHORE.acquireUninterruptibly(); - try { - List candidates = downloadProvider.injectURLWithCandidates(PREFIX + "/v2/version/" + versionId); - IOException exception = null; - - for (URI uri : candidates) { - try { - ProjectVersion version = HttpRequest.GET(uri.toString()).getJson(ProjectVersion.class); - return version.getChangelog(); - } catch (IOException e) { - IOException wrapper = new IOException("Failed to get addon changelog: " + uri, e); - if (candidates.size() == 1) { - exception = wrapper; - } else { - if (exception == null) { - exception = new IOException("Failed to get addon changelog"); - } - exception.addSuppressed(wrapper); - } - } - } - - throw exception != null ? exception : new IOException("No candidates found"); - } finally { - SEMAPHORE.release(); - } - } - - @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(); - try { - List categories = HttpRequest.GET(PREFIX + "/v2/tag/category").getJson(listTypeOf(Category.class)); - return categories.stream() - .filter(category -> category.getProjectType().equals(projectType)) - .map(Category::toCategory); - } finally { - SEMAPHORE.release(); - } - } - - public static class Category { - private final String icon; - - private final String name; - - @SerializedName("project_type") - private final String projectType; - - public Category() { - this("", "", ""); - } - - public Category(String icon, String name, String projectType) { - this.icon = icon; - this.name = name; - this.projectType = projectType; - } - - public String getIcon() { - return icon; - } - - public String getName() { - return name; - } - - public String getProjectType() { - return projectType; - } - - public RemoteModRepository.Category toCategory() { - return new RemoteModRepository.Category( - this, - name, - Collections.emptyList()); - } - } - - public static class Project implements RemoteMod.IMod { - private final String slug; - - private final String title; - - private final String description; - - private final List categories; - - /** - * A long body describing project in detail. - */ - private final String body; - - @SerializedName("project_type") - private final String projectType; - - private final int downloads; - - @SerializedName("icon_url") - private final String iconUrl; - - private final String id; - - private final String team; - - private final Instant published; - - private final Instant updated; - - private final List versions; - - public Project(String slug, String title, String description, List categories, String body, String projectType, int downloads, String iconUrl, String id, String team, Instant published, Instant updated, List versions) { - this.slug = slug; - this.title = title; - this.description = description; - this.categories = categories; - this.body = body; - this.projectType = projectType; - this.downloads = downloads; - this.iconUrl = iconUrl; - this.id = id; - this.team = team; - this.published = published; - this.updated = updated; - this.versions = versions; - } - - public String getSlug() { - return slug; - } - - public String getTitle() { - return title; - } - - public String getDescription() { - return description; - } - - public List getCategories() { - return categories; - } - - public String getBody() { - return body; - } - - public String getProjectType() { - return projectType; - } - - public int getDownloads() { - return downloads; - } - - public String getIconUrl() { - return iconUrl; - } - - public String getId() { - return id; - } - - public String getTeam() { - return team; - } - - public Instant getPublished() { - return published; - } - - public Instant getUpdated() { - return updated; - } - - public List getVersions() { - return versions; - } - - @Override - public List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { - Set dependencies = modRepository.getRemoteVersionsById(downloadProvider, getId()) - .flatMap(version -> version.getDependencies().stream()) - .collect(Collectors.toSet()); - List mods = new ArrayList<>(); - for (RemoteMod.Dependency dependency : dependencies) { - mods.add(dependency.load(downloadProvider)); - } - return mods; - } - - @Override - public Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { - return modRepository.getRemoteVersionsById(downloadProvider, getId()); - } - - public RemoteMod toMod() { - RemoteModRepository.Type type = switch (projectType) { - case "modpack" -> RemoteModRepository.Type.MODPACK; - case "resourcepack" -> RemoteModRepository.Type.RESOURCE_PACK; - case "shader" -> RemoteModRepository.Type.SHADER_PACK; - default -> RemoteModRepository.Type.MOD; - }; - return new RemoteMod( - slug, - "", - title, - description, - categories, - String.format("https://modrinth.com/%s/%s", projectType, id), - iconUrl, - this, - type - ); - } - } - - @Immutable - public static class Dependency { - @SerializedName("version_id") - private final String versionId; - - @SerializedName("project_id") - private final String projectId; - - @SerializedName("dependency_type") - private final String dependencyType; - - public Dependency(String versionId, String projectId, String dependencyType) { - this.versionId = versionId; - this.projectId = projectId; - this.dependencyType = dependencyType; - } - - public String getVersionId() { - return versionId; - } - - public String getProjectId() { - return projectId; - } - - public String getDependencyType() { - return dependencyType; - } - } - - public static class ProjectVersion implements RemoteMod.IVersion { - private static final Map DEPENDENCY_TYPE = Lang.mapOf( - Pair.pair("required", RemoteMod.DependencyType.REQUIRED), - Pair.pair("optional", RemoteMod.DependencyType.OPTIONAL), - Pair.pair("embedded", RemoteMod.DependencyType.EMBEDDED), - Pair.pair("incompatible", RemoteMod.DependencyType.INCOMPATIBLE) - ); - - private final String name; - - @SerializedName("version_number") - private final String versionNumber; - - private final String changelog; - - private final List dependencies; - - @SerializedName("game_versions") - private final List gameVersions; - - @SerializedName("version_type") - private final String versionType; - - private final List loaders; - - private final boolean featured; - - private final String id; - - @SerializedName("project_id") - private final String projectId; - - @SerializedName("author_id") - private final String authorId; - - @SerializedName("date_published") - private final Instant datePublished; - - private final int downloads; - - @SerializedName("changelog_url") - private final String changelogUrl; - - private final List files; - - public ProjectVersion(String name, String versionNumber, String changelog, List dependencies, List gameVersions, String versionType, List loaders, boolean featured, String id, String projectId, String authorId, Instant datePublished, int downloads, String changelogUrl, List files) { - this.name = name; - this.versionNumber = versionNumber; - this.changelog = changelog; - this.dependencies = dependencies; - this.gameVersions = gameVersions; - this.versionType = versionType; - this.loaders = loaders; - this.featured = featured; - this.id = id; - this.projectId = projectId; - this.authorId = authorId; - this.datePublished = datePublished; - this.downloads = downloads; - this.changelogUrl = changelogUrl; - this.files = files; - } - - public String getName() { - return name; - } - - public String getVersionNumber() { - return versionNumber; - } - - public String getChangelog() { - return changelog; - } - - public List getDependencies() { - return dependencies; - } - - public List getGameVersions() { - return gameVersions; - } - - public String getVersionType() { - return versionType; - } - - public List getLoaders() { - return loaders; - } - - public boolean isFeatured() { - return featured; - } - - public String getId() { - return id; - } - - public String getProjectId() { - return projectId; - } - - public String getAuthorId() { - return authorId; - } - - public Instant getDatePublished() { - return datePublished; - } - - public int getDownloads() { - return downloads; - } - - public String getChangelogUrl() { - return changelogUrl; - } - - public List getFiles() { - return files; - } - - @Override - public RemoteMod.Source getType() { - return RemoteMod.Source.MODRINTH; - } - - public Optional toVersion() { - RemoteMod.VersionType type; - if ("release".equals(versionType)) { - type = RemoteMod.VersionType.Release; - } else if ("beta".equals(versionType)) { - type = RemoteMod.VersionType.Beta; - } else if ("alpha".equals(versionType)) { - type = RemoteMod.VersionType.Alpha; - } else { - type = RemoteMod.VersionType.Release; - } - - if (files.size() == 0) { - return Optional.empty(); - } - - return Optional.of(new RemoteMod.Version( - this, - getId(), - projectId, - name, - versionNumber, - datePublished, - type, - files.get(0).toFile(), - dependencies.stream().map(dependency -> { - if (dependency.projectId == null) { - return RemoteMod.Dependency.ofBroken(); - } - - if (!DEPENDENCY_TYPE.containsKey(dependency.dependencyType)) { - throw new IllegalStateException("Broken datas"); - } - - return RemoteMod.Dependency.ofGeneral(DEPENDENCY_TYPE.get(dependency.dependencyType), MODS, dependency.projectId); - }).filter(Objects::nonNull).collect(Collectors.toList()), - gameVersions, - loaders.stream().flatMap(loader -> { - if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC); - else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE); - else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGE); - else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT); - else if ("liteloader".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LITE_LOADER); - else return Stream.empty(); - }).collect(Collectors.toList()) - )); - } - } - - public static class ProjectVersionFile { - private final Map hashes; - private final String url; - private final String filename; - private final boolean primary; - private final int size; - - public ProjectVersionFile(Map hashes, String url, String filename, boolean primary, int size) { - this.hashes = hashes; - this.url = url; - this.filename = filename; - this.primary = primary; - this.size = size; - } - - public Map getHashes() { - return hashes; - } - - public String getUrl() { - return url; - } - - public String getFilename() { - return filename; - } - - public boolean isPrimary() { - return primary; - } - - public int getSize() { - return size; - } - - public RemoteMod.File toFile() { - return new RemoteMod.File(hashes, url, filename); - } - } - - public static class ProjectSearchResult implements RemoteMod.IMod { - private final String slug; - - private final String title; - - private final String description; - - private final List categories; - - @SerializedName("display_categories") - private final List displayCategories; - - @SerializedName("project_type") - private final String projectType; - - private final int downloads; - - @SerializedName("icon_url") - private final String iconUrl; - - @SerializedName("project_id") - private final String projectId; - - private final String author; - - private final List versions; - - @SerializedName("date_created") - private final Instant dateCreated; - - @SerializedName("date_modified") - private final Instant dateModified; - - @SerializedName("latest_version") - private final String latestVersion; - - public ProjectSearchResult(String slug, String title, String description, List categories, List displayCategories, String projectType, int downloads, String iconUrl, String projectId, String author, List versions, Instant dateCreated, Instant dateModified, String latestVersion) { - this.slug = slug; - this.title = title; - this.description = description; - this.categories = categories; - this.displayCategories = displayCategories; - this.projectType = projectType; - this.downloads = downloads; - this.iconUrl = iconUrl; - this.projectId = projectId; - this.author = author; - this.versions = versions; - this.dateCreated = dateCreated; - this.dateModified = dateModified; - this.latestVersion = latestVersion; - } - - public String getSlug() { - return slug; - } - - public String getTitle() { - return title; - } - - public String getDescription() { - return description; - } - - public List getCategories() { - return categories; - } - - public List getDisplayCategories() { - return displayCategories; - } - - public String getProjectType() { - return projectType; - } - - public int getDownloads() { - return downloads; - } - - public String getIconUrl() { - return iconUrl; - } - - public String getProjectId() { - return projectId; - } - - public String getAuthor() { - return author; - } - - public List getVersions() { - return versions; - } - - public Instant getDateCreated() { - return dateCreated; - } - - public Instant getDateModified() { - return dateModified; - } - - public String getLatestVersion() { - return latestVersion; - } - - @Override - public List loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { - Set dependencies = modRepository.getRemoteVersionsById(downloadProvider, getProjectId()) - .flatMap(version -> version.getDependencies().stream()) - .collect(Collectors.toSet()); - List mods = new ArrayList<>(); - for (RemoteMod.Dependency dependency : dependencies) { - mods.add(dependency.load(downloadProvider)); - } - return mods; - } - - @Override - public Stream loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException { - return modRepository.getRemoteVersionsById(downloadProvider, getProjectId()); - } - - public RemoteMod toMod() { - RemoteModRepository.Type type = switch (projectType) { - case "modpack" -> RemoteModRepository.Type.MODPACK; - case "resourcepack" -> RemoteModRepository.Type.RESOURCE_PACK; - case "shader" -> RemoteModRepository.Type.SHADER_PACK; - default -> RemoteModRepository.Type.MOD; - }; - return new RemoteMod( - slug, - author, - title, - description, - sortDisplayCategories(displayCategories), - String.format("https://modrinth.com/%s/%s", projectType, projectId), - iconUrl, - this, - type - ); - } - } - - public static class Response { - - @SuppressWarnings("unchecked") - public static TypeToken> typeOf(Class responseType) { - return (TypeToken>) TypeToken.getParameterized(Response.class, responseType); - } - - private final int offset; - - private final int limit; - - @SerializedName("total_hits") - private final int totalHits; - - private final List hits; - - public Response() { - this(0, 0, Collections.emptyList()); - } - - public Response(int offset, int limit, List hits) { - this.offset = offset; - this.limit = limit; - this.totalHits = hits.size(); - this.hits = hits; - } - - public int getOffset() { - return offset; - } - - public int getLimit() { - return limit; - } - - public int getTotalHits() { - return totalHits; - } - - public List getHits() { - return hits; - } - } -} From 602266577d4f8731d067d98a32d5a55c0af64da6 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sat, 27 Jun 2026 17:39:03 +0800 Subject: [PATCH 116/123] update --- .../org/jackhuang/hmcl/ui/HTMLRenderer.java | 19 ++++++++++++++----- 1 file changed, 14 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 79103100426..6349575a091 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.collections.FXCollections; import javafx.geometry.Pos; import javafx.scene.Cursor; +import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.image.ImageView; @@ -369,18 +370,26 @@ private void appendTable(Node table) { tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); for (int i = 0; i < columnCount; i++) { int finalI = i; - TableColumn, javafx.scene.Node> c = new TableColumn<>(); + TableColumn, Element> c = new TableColumn<>(); Element e = head.get(i); if (e != null) { var box = new VBox(new HTMLRenderer(onClickHyperlink).appendNode(e).mergeLineBreaks().render()); box.setAlignment(Pos.CENTER_LEFT); c.setGraphic(box); } - c.setCellValueFactory(param -> { - Element el = param.getValue().get(finalI); - if (el == null) return null; - return new SimpleObjectProperty<>(new Pane(new HTMLRenderer(onClickHyperlink).appendNode(el).mergeLineBreaks().render())); + c.setCellFactory(param -> new TableCell<>() { + @Override + protected void updateItem(Element item, boolean empty) { + if (item == getItem()) return; + super.updateItem(item, empty); + if (item == null) { + setGraphic(null); + } else { + setGraphic(new Pane(new HTMLRenderer(onClickHyperlink).appendNode(item).mergeLineBreaks().render())); + } + } }); + c.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().get(finalI))); tableView.getColumns().add(c); } From 453db58dd3f5341bdc64418fa81cf7987a4cd9c9 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sat, 27 Jun 2026 17:41:00 +0800 Subject: [PATCH 117/123] update --- .../main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 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 6349575a091..b6b8f2f7011 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -80,18 +80,21 @@ 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); + int i = 0; + while (i < len) { + int c = str.codePointAt(i); if (isWhitespace(c)) { // Ignore   - if (lastWasWhite) + if (lastWasWhite) { + i += Character.charCount(c); continue; + } accum.append(' '); lastWasWhite = true; } else if (!isInvisibleChar(c)) { accum.appendCodePoint(c); lastWasWhite = false; } + i += Character.charCount(c); } return accum.toString(); } From 1f269fc6919e33f449f41b685576d10232df8b70 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sat, 4 Jul 2026 18:55:33 +0800 Subject: [PATCH 118/123] update --- .../hmcl/addon/repository/ModrinthRemoteAddonRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java index 20e9e1f1024..f2ecfc4a0bc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java @@ -325,7 +325,7 @@ public String getAddonChangelog(DownloadProvider downloadProvider, String addonI @Override public String getVersionPageUrl(RemoteAddon.Version version) { - return "%s/mod/%s/version/%s".formatted(BASE, version.modid(), version.versionId()); // Modrinth will help us redirect + return "%s/project/%s/version/%s".formatted(BASE, version.modid(), version.versionId()); // Modrinth will help us redirect } @Override From 1729a92ba3fd6ba0bc9ec0890bef8091b7df6646 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 5 Jul 2026 11:47:29 +0800 Subject: [PATCH 119/123] workaround for jei --- .../hmcl/ui/versions/AddonUpdatesPage.java | 5 +- .../hmcl/ui/versions/DownloadPage.java | 5 +- .../org/jackhuang/hmcl/util/StringUtils.java | 52 ++++++++++++------- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index f33e7f0f181..bbda42d6406 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -349,7 +349,10 @@ private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, Sc } RemoteAddon.Version version = object.data.targetVersion(); if (repo == null) return null; - return StringUtils.convertToHtml(repo.getAddonChangelog(DownloadProviders.getDownloadProvider(), version.modid(), version.versionId())); + return StringUtils.convertToHtml( + repo.getAddonChangelog(DownloadProviders.getDownloadProvider(), version.modid(), version.versionId()), + "238222".equals(object.data.targetVersion().modid()) + ); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { object.changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty"); 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 7c5362c0893..f1199b705fe 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 @@ -676,7 +676,10 @@ public AddonChangelog(RemoteAddon.Version version, RemoteAddonRepository repo, D private void loadChangelog(RemoteAddon.Version version, RemoteAddonRepository repo, DownloadProvider provider, SpinnerPane spinnerPane, ScrollPane scrollPane) { spinnerPane.setLoading(true); Task.supplyAsync(() -> - StringUtils.convertToHtml(repo.getAddonChangelog(provider, version.modid(), version.versionId())) + StringUtils.convertToHtml( + repo.getAddonChangelog(provider, version.modid(), version.versionId()), + "238222".equals(version.modid()) // FUCK YOU JEI, FUCK YOU CURSEFORGE + ) ).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { String changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.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 21a41e2a871..31ed72270db 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -21,11 +21,13 @@ import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.ext.ins.InsExtension; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.Node; import org.commonmark.parser.Parser; +import org.commonmark.renderer.NodeRenderer; +import org.commonmark.renderer.html.HtmlNodeRendererContext; import org.commonmark.renderer.html.HtmlRenderer; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.safety.Safelist; +import org.commonmark.renderer.html.HtmlWriter; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jetbrains.annotations.Contract; @@ -628,35 +630,49 @@ public static List deserializeStringList(String json) { return JsonUtils.fromNonNullJson(json, JsonUtils.listTypeOf(String.class)); } - private static final Safelist all = Safelist.relaxed() - .addAttributes("a", "rel", "target"); - private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().extensions(List.of( InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create() )).build(); + private static final HtmlRenderer HTML_RENDERER_RAW_INDENTED_BLOCK = HtmlRenderer.builder() + .extensions(List.of(InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create())) + .nodeRendererFactory(IndentedBlockRawRenderer::new) + .build(); + private static final Parser MD_PARSER = Parser.builder().extensions(List.of( AutolinkExtension.create(), InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create() )).build(); - public static String convertToHtml(String str) { + public static String convertToHtml(String str, boolean rawIndentedBlocks) { if (str == null) return null; if (isBlank(str)) return ""; - /* - Firstly, we check if the string could be seen as HTML. + if (rawIndentedBlocks) + return HTML_RENDERER_RAW_INDENTED_BLOCK.render(MD_PARSER.parse(str)); + return HTML_RENDERER.render(MD_PARSER.parse(str)); + } - If so, we convert it to HTML document, then re-print it to a string to normalize spaces - (otherwise the parser may parse the HTML code as code blocks, which is not intended). - This will (hopefully) not break Markdown formats. + private static final class IndentedBlockRawRenderer implements NodeRenderer { - If not, we assume that the string is pure Markdown. - */ - String either = Jsoup.isValid(str, all) - ? Jsoup.parse(str).outputSettings(new Document.OutputSettings().prettyPrint(false)).body().html() - : str; + private final HtmlWriter html; + + public IndentedBlockRawRenderer(HtmlNodeRendererContext context) { + this.html = context.getWriter(); + } + + @Override + public Set> getNodeTypes() { + return Set.of(IndentedCodeBlock.class); + } + + @Override + public void render(Node node) { + IndentedCodeBlock block = (IndentedCodeBlock) node; + html.line(); + html.raw(block.getLiteral()); + html.line(); + } - return HTML_RENDERER.render(MD_PARSER.parse(either)); } public static class LevCalculator { From 221347a4717b4d9a5d63e90dce12eeb60d1c2f78 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sat, 25 Jul 2026 21:34:27 +0800 Subject: [PATCH 120/123] update --- .../main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 1 - 1 file changed, 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 4c26cf81f1f..ba715c8394b 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 @@ -46,7 +46,6 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; From 49b74dab53a296d48b113f0f463653c98772f478 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 26 Jul 2026 10:39:19 +0800 Subject: [PATCH 121/123] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java | 4 ++-- HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 74e69ed396d..9301a02c15b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -678,12 +678,12 @@ public static void onHyperlinkAction(String href) { } } - public static void openUriInBrowser(URI uri) { + public static void openUriInBrowser(@Nullable URI uri) { if (uri == null) return; openUriInBrowser(uri.toString()); } - public static void openUriInBrowser(String uri) { + public static void openUriInBrowser(@Nullable String uri) { if (uri == null) return; var dialog = new MessageDialogPane.Builder( i18n("web.open_in_browser", uri), 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 b6b8f2f7011..0b2a76a7832 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java @@ -35,6 +35,7 @@ import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; @@ -560,6 +561,7 @@ public ImagePane(ImageView imageView, @Nullable String alt) { private static final class SimpleCssDeclarations { + @Unmodifiable private static final Map cssPropertyMapping = Map.of( "color", "-fx-fill", "font-size", "-fx-font-size" From 18af8886efff48b989e3dadb63c54ebb37386c07 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 26 Jul 2026 10:42:23 +0800 Subject: [PATCH 122/123] update --- .../java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java | 4 ++-- .../java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index bbda42d6406..cca31710abb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -334,8 +334,8 @@ public AddonChangelog(AddonUpdateObject object) { setActions(versionPageBtn, closeButton); - this.prefWidthProperty().bind(Controllers.getStage().widthProperty().multiply(0.7)); - this.prefHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.7)); + this.prefWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7)); + this.prefHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.7)); onEscPressed(this, closeButton::fire); } 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 ba715c8394b..4cc6a850605 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 @@ -669,8 +669,8 @@ public AddonChangelog(RemoteAddon.Version version, RemoteAddonRepository repo, D setActions(closeButton); - this.prefWidthProperty().bind(Controllers.getStage().widthProperty().multiply(0.7)); - this.prefHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.8)); + this.prefWidthProperty().bind(Controllers.windowWidthProperty().multiply(0.7)); + this.prefHeightProperty().bind(Controllers.windowHeightProperty().multiply(0.8)); onEscPressed(this, closeButton::fire); } From a20defd7923acdeafdeb51cbc736d9033fcae2e5 Mon Sep 17 00:00:00 2001 From: ToobLac Date: Sun, 26 Jul 2026 14:31:54 +0800 Subject: [PATCH 123/123] update --- .../jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java | 3 ++- .../java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java | 3 +++ .../hmcl/addon/repository/CurseForgeRemoteAddonRepository.java | 3 ++- .../hmcl/addon/repository/ModrinthRemoteAddonRepository.java | 3 ++- .../src/main/java/org/jackhuang/hmcl/util/StringUtils.java | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java index 601c3fe5d40..df364515ec8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java @@ -23,6 +23,7 @@ import org.jackhuang.hmcl.ui.versions.ModTranslations; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.nio.file.Path; @@ -153,7 +154,7 @@ public String getAddonChangelog(DownloadProvider downloadProvider, String addonI } @Override - public String getVersionPageUrl(RemoteAddon.Version version) throws IOException { + public @NotNull String getVersionPageUrl(RemoteAddon.Version version) throws IOException { return getBackedRemoteModRepository().getVersionPageUrl(version); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java index d0a06c41648..36c720d4bd5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/RemoteAddonRepository.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.addon; import org.jackhuang.hmcl.download.DownloadProvider; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -104,8 +105,10 @@ default RemoteAddon resolveDependency(DownloadProvider downloadProvider, String Stream getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException; + @Nullable String getAddonChangelog(DownloadProvider downloadProvider, String addonId, String versionId) throws IOException; + @NotNull String getVersionPageUrl(RemoteAddon.Version version) throws IOException; Stream getCategories() throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java index 1db08ebeb7b..472d1588baf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java @@ -30,6 +30,7 @@ import org.jackhuang.hmcl.util.io.JarUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayOutputStream; @@ -282,7 +283,7 @@ public String getAddonChangelog(DownloadProvider downloadProvider, String addonI } @Override - public String getVersionPageUrl(RemoteAddon.Version version) throws IOException { + public @NotNull String getVersionPageUrl(RemoteAddon.Version version) throws IOException { SEMAPHORE.acquireUninterruptibly(); try { Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + version.modid())) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java index 771c6ad7bf3..3dc9cce86ee 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java @@ -28,6 +28,7 @@ import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.ResponseCodeException; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.FileNotFoundException; @@ -324,7 +325,7 @@ public String getAddonChangelog(DownloadProvider downloadProvider, String addonI } @Override - public String getVersionPageUrl(RemoteAddon.Version version) { + public @NotNull String getVersionPageUrl(RemoteAddon.Version version) { return "%s/project/%s/version/%s".formatted(BASE, version.modid(), version.versionId()); // Modrinth will help us redirect } 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 823de90bb1b..ca1c8670cf7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -651,7 +651,7 @@ public static List deserializeStringList(String json) { AutolinkExtension.create(), InsExtension.create(), StrikethroughExtension.create(), TablesExtension.create() )).build(); - public static String convertToHtml(String str, boolean rawIndentedBlocks) { + public static @Nullable String convertToHtml(String str, boolean rawIndentedBlocks) { if (str == null) return null; if (isBlank(str)) return "";