Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,9 @@ public RemoteMod.File getModFile(String modId, String fileId) throws IOException
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(id);
}

@Override
public String getModChangelog(String modId, String fileId) throws IOException {
return getBackedRemoteModRepository().getModChangelog(modId, fileId);
}
}
14 changes: 14 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.Nullable;
import org.jsoup.Jsoup;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
Expand Down Expand Up @@ -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;
}
}
17 changes: 17 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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<javafx.scene.Node> children = new ArrayList<>();
private final List<Node> stack = new ArrayList<>();

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

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

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

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

for (String gameVersion : control.versions.keys().stream()
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
.collect(Collectors.toList())) {
.toList()) {
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
if (versions == null || versions.isEmpty()) {
continue;
Expand Down Expand Up @@ -447,36 +450,27 @@ 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);
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);

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);
Expand Down Expand Up @@ -522,36 +516,58 @@ 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(() -> {
Optional<String> changelog;
if (changelogCache.containsKey(version)) {
changelog = Optional.ofNullable(changelogCache.get(version));
} else if (version.getChangelog() != null) {
changelog = StringUtils.nullIfBlank(version.getChangelog());
} else {
try {
changelog = StringUtils.nullIfBlank(selfPage.repository.getModChangelog(version.getModid(), version.getVersionId()));
} catch (UnsupportedOperationException e) {
changelog = Optional.empty();
}
}

EnumMap<RemoteMod.DependencyType, List<Node>> dependencies = new EnumMap<>(RemoteMod.DependencyType.class);
for (RemoteMod.Dependency dependency : version.getDependencies()) {
if (dependency.getType() == RemoteMod.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteMod.DependencyType.BROKEN) {
continue;
}

if (!dependencies.containsKey(dependency.getType())) {
List<Node> list = new ArrayList<>();
List<Node> 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);
dependencies.get(dependency.getType()).add(dependencyModItem);
}

return dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
return new Pair<>(changelog, dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList()));
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
spinnerPane.setLoading(false);
if (exception == null) {
dependenciesList.getContent().setAll(result);
List<Node> nodes = new LinkedList<>();
result.getKey().ifPresent(s -> {
if (!HTMLRenderer.isHTML(s)) {
s = StringUtils.markdownToHTML(s);
}
changelogCache.put(version, s);
nodes.add(FXUtils.renderModChangelog(s));
});
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();
}
}
Expand Down
Loading