使用 Jsoup 解析并渲染 HTML 页面 (#3321)
This commit is contained in:
@@ -52,7 +52,6 @@ import javafx.util.StringConverter;
|
|||||||
import org.glavo.png.PNGType;
|
import org.glavo.png.PNGType;
|
||||||
import org.glavo.png.PNGWriter;
|
import org.glavo.png.PNGWriter;
|
||||||
import org.glavo.png.javafx.PNGJavaFXUtils;
|
import org.glavo.png.javafx.PNGJavaFXUtils;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||||
@@ -70,12 +69,9 @@ import org.w3c.dom.NodeList;
|
|||||||
import org.xml.sax.InputSource;
|
import org.xml.sax.InputSource;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import javax.swing.event.HyperlinkEvent;
|
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
import java.awt.*;
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
@@ -527,52 +523,6 @@ public final class FXUtils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showWebDialog(String title, String content) {
|
|
||||||
showWebDialog(title, content, 800, 480);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void showWebDialog(String title, String content, int width, int height) {
|
|
||||||
try {
|
|
||||||
WebStage stage = new WebStage(width, height);
|
|
||||||
stage.getWebView().getEngine().loadContent(content);
|
|
||||||
stage.setTitle(title);
|
|
||||||
stage.showAndWait();
|
|
||||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
|
||||||
LOG.warning("WebView is missing or initialization failed, use JEditorPane replaced", e);
|
|
||||||
|
|
||||||
SwingUtils.initLookAndFeel();
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
|
||||||
final JFrame frame = new JFrame(title);
|
|
||||||
frame.setSize(width, height);
|
|
||||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
|
||||||
frame.setLocationByPlatform(true);
|
|
||||||
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
|
|
||||||
frame.setLayout(new BorderLayout());
|
|
||||||
|
|
||||||
final JProgressBar progressBar = new JProgressBar();
|
|
||||||
progressBar.setIndeterminate(true);
|
|
||||||
frame.add(progressBar, BorderLayout.PAGE_START);
|
|
||||||
|
|
||||||
Schedulers.defaultScheduler().execute(() -> {
|
|
||||||
final JEditorPane pane = new JEditorPane("text/html", content);
|
|
||||||
pane.setEditable(false);
|
|
||||||
pane.addHyperlinkListener(event -> {
|
|
||||||
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
|
|
||||||
openLink(event.getURL().toExternalForm());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
|
||||||
progressBar.setVisible(false);
|
|
||||||
frame.add(new JScrollPane(pane), BorderLayout.CENTER);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
frame.setVisible(true);
|
|
||||||
frame.toFront();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
|
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
|
||||||
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
|
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
|
||||||
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
|
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
|
||||||
|
|||||||
266
HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java
Normal file
266
HMCL/src/main/java/org/jackhuang/hmcl/ui/HTMLRenderer.java
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.ui;
|
||||||
|
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextFlow;
|
||||||
|
import org.jsoup.nodes.Node;
|
||||||
|
import org.jsoup.nodes.TextNode;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Glavo
|
||||||
|
*/
|
||||||
|
public final class HTMLRenderer {
|
||||||
|
private static URI resolveLink(Node linkNode) {
|
||||||
|
String href = linkNode.absUrl("href");
|
||||||
|
if (href.isEmpty())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URI(href);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<javafx.scene.Node> children = new ArrayList<>();
|
||||||
|
private final List<Node> stack = new ArrayList<>();
|
||||||
|
|
||||||
|
private boolean bold;
|
||||||
|
private boolean italic;
|
||||||
|
private boolean underline;
|
||||||
|
private boolean strike;
|
||||||
|
private boolean highlight;
|
||||||
|
private String headerLevel;
|
||||||
|
private Node hyperlink;
|
||||||
|
|
||||||
|
private final Consumer<URI> onClickHyperlink;
|
||||||
|
|
||||||
|
public HTMLRenderer(Consumer<URI> onClickHyperlink) {
|
||||||
|
this.onClickHyperlink = onClickHyperlink;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStyle() {
|
||||||
|
bold = false;
|
||||||
|
italic = false;
|
||||||
|
underline = false;
|
||||||
|
strike = false;
|
||||||
|
highlight = false;
|
||||||
|
headerLevel = null;
|
||||||
|
hyperlink = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushNode(Node node) {
|
||||||
|
stack.add(node);
|
||||||
|
updateStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void popNode() {
|
||||||
|
stack.remove(stack.size() - 1);
|
||||||
|
updateStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyStyle(Text text) {
|
||||||
|
if (hyperlink != null) {
|
||||||
|
URI target = resolveLink(hyperlink);
|
||||||
|
if (target != null) {
|
||||||
|
text.setOnMouseClicked(event -> onClickHyperlink.accept(target));
|
||||||
|
text.setCursor(Cursor.HAND);
|
||||||
|
}
|
||||||
|
text.getStyleClass().add("html-hyperlink");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hyperlink != null || underline)
|
||||||
|
text.setUnderline(true);
|
||||||
|
|
||||||
|
if (strike)
|
||||||
|
text.setStrikethrough(true);
|
||||||
|
|
||||||
|
if (bold || highlight)
|
||||||
|
text.getStyleClass().add("html-bold");
|
||||||
|
|
||||||
|
if (italic)
|
||||||
|
text.getStyleClass().add("html-italic");
|
||||||
|
|
||||||
|
if (headerLevel != null)
|
||||||
|
text.getStyleClass().add("html-" + headerLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendText(String text) {
|
||||||
|
Text textNode = new Text(text);
|
||||||
|
applyStyle(textNode);
|
||||||
|
children.add(textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendImage(Node node) {
|
||||||
|
String src = node.absUrl("src");
|
||||||
|
URI imageUri = null;
|
||||||
|
try {
|
||||||
|
if (!src.isEmpty())
|
||||||
|
imageUri = URI.create(src);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
String alt = node.attr("alt");
|
||||||
|
|
||||||
|
if (imageUri != null) {
|
||||||
|
URI uri = URI.create(src);
|
||||||
|
|
||||||
|
String widthAttr = node.attr("width");
|
||||||
|
String heightAttr = node.attr("height");
|
||||||
|
|
||||||
|
double width = 0;
|
||||||
|
double height = 0;
|
||||||
|
|
||||||
|
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
|
||||||
|
try {
|
||||||
|
width = Double.parseDouble(widthAttr);
|
||||||
|
height = Double.parseDouble(heightAttr);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
|
||||||
|
if (image.isError()) {
|
||||||
|
LOG.warning("Failed to load image: " + uri, image.getException());
|
||||||
|
} else {
|
||||||
|
ImageView imageView = new ImageView(image);
|
||||||
|
if (hyperlink != null) {
|
||||||
|
URI target = resolveLink(hyperlink);
|
||||||
|
if (target != null) {
|
||||||
|
imageView.setOnMouseClicked(event -> onClickHyperlink.accept(target));
|
||||||
|
imageView.setCursor(Cursor.HAND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
children.add(imageView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alt.isEmpty())
|
||||||
|
appendText(alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void appendNode(Node node) {
|
||||||
|
if (node instanceof TextNode) {
|
||||||
|
appendText(((TextNode) node).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
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":
|
||||||
|
if (!children.isEmpty())
|
||||||
|
appendText("\n\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.childNodeSize() > 0) {
|
||||||
|
pushNode(node);
|
||||||
|
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":
|
||||||
|
appendText("\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextFlow render() {
|
||||||
|
TextFlow textFlow = new TextFlow();
|
||||||
|
textFlow.getStyleClass().add("html");
|
||||||
|
textFlow.getChildren().setAll(children);
|
||||||
|
return textFlow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,58 +19,73 @@ package org.jackhuang.hmcl.ui;
|
|||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
import com.jfoenix.controls.JFXDialogLayout;
|
import com.jfoenix.controls.JFXDialogLayout;
|
||||||
import javafx.concurrent.Worker;
|
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.web.WebEngine;
|
import javafx.scene.control.ProgressIndicator;
|
||||||
import javafx.scene.web.WebView;
|
import javafx.scene.control.ScrollPane;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||||
|
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Node;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
public class UpgradeDialog extends JFXDialogLayout {
|
public final class UpgradeDialog extends JFXDialogLayout {
|
||||||
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
|
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
|
||||||
{
|
setHeading(new Label(i18n("update.changelog")));
|
||||||
setHeading(new Label(i18n("update.changelog")));
|
setBody(new ProgressIndicator());
|
||||||
}
|
|
||||||
|
|
||||||
{
|
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html#nowchange";
|
Task.supplyAsync(Schedulers.io(), () -> {
|
||||||
try {
|
Document document = Jsoup.parse(new URL(url), 30 * 1000);
|
||||||
WebView webView = new WebView();
|
Node node = document.selectFirst("#nowchange");
|
||||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
if (node == null)
|
||||||
WebEngine engine = webView.getEngine();
|
throw new IOException("Cannot find #nowchange in document");
|
||||||
engine.load(url);
|
|
||||||
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
|
HTMLRenderer renderer = new HTMLRenderer(uri -> {
|
||||||
if (newValue == Worker.State.FAILED) {
|
LOG.info("Open link: " + uri);
|
||||||
LOG.warning("Failed to load update log, trying to open it in browser");
|
FXUtils.openLink(uri.toString());
|
||||||
FXUtils.openLink(url);
|
});
|
||||||
setBody();
|
|
||||||
}
|
do {
|
||||||
});
|
renderer.appendNode(node);
|
||||||
setBody(webView);
|
node = node.nextSibling();
|
||||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
} while (node != null);
|
||||||
LOG.warning("WebView is missing or initialization failed", e);
|
|
||||||
|
return renderer.render();
|
||||||
|
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
|
if (exception == null) {
|
||||||
|
ScrollPane scrollPane = new ScrollPane(result);
|
||||||
|
scrollPane.setFitToWidth(true);
|
||||||
|
setBody(scrollPane);
|
||||||
|
} else {
|
||||||
|
LOG.warning("Failed to load update log, trying to open it in browser");
|
||||||
FXUtils.openLink(url);
|
FXUtils.openLink(url);
|
||||||
|
setBody();
|
||||||
}
|
}
|
||||||
}
|
}).start();
|
||||||
|
|
||||||
{
|
JFXHyperlink openInBrowser = new JFXHyperlink(i18n("web.view_in_browser"));
|
||||||
JFXButton updateButton = new JFXButton(i18n("update.accept"));
|
openInBrowser.setExternalLink(url);
|
||||||
updateButton.getStyleClass().add("dialog-accept");
|
|
||||||
updateButton.setOnMouseClicked(e -> updateRunnable.run());
|
|
||||||
|
|
||||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
JFXButton updateButton = new JFXButton(i18n("update.accept"));
|
||||||
cancelButton.getStyleClass().add("dialog-cancel");
|
updateButton.getStyleClass().add("dialog-accept");
|
||||||
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
|
updateButton.setOnAction(e -> updateRunnable.run());
|
||||||
|
|
||||||
setActions(updateButton, cancelButton);
|
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||||
onEscPressed(this, cancelButton::fire);
|
cancelButton.getStyleClass().add("dialog-cancel");
|
||||||
}
|
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||||
|
|
||||||
|
setActions(openInBrowser, updateButton, cancelButton);
|
||||||
|
onEscPressed(this, cancelButton::fire);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
HMCL/src/main/java/org/jackhuang/hmcl/ui/WebPage.java
Normal file
74
HMCL/src/main/java/org/jackhuang/hmcl/ui/WebPage.java
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.ui;
|
||||||
|
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.layout.Background;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
|
import org.jackhuang.hmcl.task.Task;
|
||||||
|
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||||
|
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Glavo
|
||||||
|
*/
|
||||||
|
public final class WebPage extends SpinnerPane implements DecoratorPage {
|
||||||
|
|
||||||
|
private final ObjectProperty<DecoratorPage.State> stateProperty;
|
||||||
|
|
||||||
|
public WebPage(String title, String content) {
|
||||||
|
this.stateProperty = new SimpleObjectProperty<>(DecoratorPage.State.fromTitle(title));
|
||||||
|
this.setBackground(Background.fill(Color.WHITE));
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
renderer.appendNode(document);
|
||||||
|
return renderer.render();
|
||||||
|
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
||||||
|
if (exception == null) {
|
||||||
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
|
scrollPane.setFitToWidth(true);
|
||||||
|
scrollPane.setContent(result);
|
||||||
|
setContent(scrollPane);
|
||||||
|
setFailedReason(null);
|
||||||
|
} else {
|
||||||
|
LOG.warning("Failed to load content", exception);
|
||||||
|
setFailedReason(i18n("web.failed"));
|
||||||
|
}
|
||||||
|
})).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReadOnlyObjectProperty<DecoratorPage.State> stateProperty() {
|
||||||
|
return stateProperty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Hello Minecraft! Launcher
|
|
||||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.jackhuang.hmcl.ui;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXProgressBar;
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.layout.BorderPane;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.web.WebEngine;
|
|
||||||
import javafx.scene.web.WebView;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import org.jackhuang.hmcl.Metadata;
|
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
|
||||||
|
|
||||||
public class WebStage extends Stage {
|
|
||||||
protected final StackPane pane = new StackPane();
|
|
||||||
protected final JFXProgressBar progressBar = new JFXProgressBar();
|
|
||||||
protected final WebView webView = new WebView();
|
|
||||||
protected final WebEngine webEngine = webView.getEngine();
|
|
||||||
|
|
||||||
public WebStage() {
|
|
||||||
this(800, 480);
|
|
||||||
}
|
|
||||||
|
|
||||||
public WebStage(int width, int height) {
|
|
||||||
setScene(new Scene(pane, width, height));
|
|
||||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
|
||||||
FXUtils.setIcon(this);
|
|
||||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
|
||||||
webView.setContextMenuEnabled(false);
|
|
||||||
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
|
|
||||||
|
|
||||||
progressBar.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
|
|
||||||
switch (webView.getEngine().getLoadWorker().getState()) {
|
|
||||||
case SUCCEEDED:
|
|
||||||
case FAILED:
|
|
||||||
case CANCELLED:
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}, webEngine.getLoadWorker().stateProperty()));
|
|
||||||
|
|
||||||
BorderPane borderPane = new BorderPane();
|
|
||||||
borderPane.setPickOnBounds(false);
|
|
||||||
borderPane.setTop(progressBar);
|
|
||||||
progressBar.prefWidthProperty().bind(borderPane.widthProperty());
|
|
||||||
pane.getChildren().setAll(webView, borderPane);
|
|
||||||
}
|
|
||||||
|
|
||||||
public WebView getWebView() {
|
|
||||||
return webView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.task.Schedulers;
|
|||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
|
import org.jackhuang.hmcl.ui.WebPage;
|
||||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||||
@@ -153,9 +154,8 @@ public final class LocalModpackPage extends ModpackPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void onDescribe() {
|
protected void onDescribe() {
|
||||||
if (manifest != null) {
|
if (manifest != null)
|
||||||
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
|
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final String MODPACK_FILE = "MODPACK_FILE";
|
public static final String MODPACK_FILE = "MODPACK_FILE";
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
|
|||||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.WebPage;
|
||||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||||
@@ -84,7 +84,7 @@ public final class RemoteModpackPage extends ModpackPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void onDescribe() {
|
protected void onDescribe() {
|
||||||
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
|
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final String MODPACK_SERVER_MANIFEST = "MODPACK_SERVER_MANIFEST";
|
public static final String MODPACK_SERVER_MANIFEST = "MODPACK_SERVER_MANIFEST";
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ public interface WizardPage {
|
|||||||
default void onNavigate(Map<String, Object> settings) {
|
default void onNavigate(Map<String, Object> settings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanup(Map<String, Object> settings);
|
default void cleanup(Map<String, Object> settings) {
|
||||||
|
}
|
||||||
|
|
||||||
String getTitle();
|
String getTitle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,28 +105,14 @@ public final class SelfDependencyPatcher {
|
|||||||
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies").resolve(Platform.getPlatform().toString()).resolve("openjfx");
|
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies").resolve(Platform.getPlatform().toString()).resolve("openjfx");
|
||||||
|
|
||||||
static List<DependencyDescriptor> readDependencies() {
|
static List<DependencyDescriptor> readDependencies() {
|
||||||
List<DependencyDescriptor> dependencies;
|
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
try (Reader reader = new InputStreamReader(SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE), UTF_8)) {
|
try (Reader reader = new InputStreamReader(SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE), UTF_8)) {
|
||||||
Map<String, List<DependencyDescriptor>> allDependencies =
|
Map<String, List<DependencyDescriptor>> allDependencies =
|
||||||
JsonUtils.GSON.fromJson(reader, mapTypeOf(String.class, listTypeOf(DependencyDescriptor.class)));
|
JsonUtils.GSON.fromJson(reader, mapTypeOf(String.class, listTypeOf(DependencyDescriptor.class)));
|
||||||
dependencies = allDependencies.get(Platform.getPlatform().toString());
|
return allDependencies.get(Platform.getPlatform().toString());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new UncheckedIOException(e);
|
throw new UncheckedIOException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dependencies == null) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
ClassLoader classLoader = SelfDependencyPatcher.class.getClassLoader();
|
|
||||||
Class.forName("netscape.javascript.JSObject", false, classLoader);
|
|
||||||
Class.forName("org.w3c.dom.html.HTMLDocument", false, classLoader);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LOG.warning("Disable javafx.web because JRE is incomplete", e);
|
|
||||||
dependencies.removeIf(it -> "javafx.web".equals(it.module) || "javafx.media".equals(it.module));
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String module;
|
public String module;
|
||||||
|
|||||||
@@ -1438,3 +1438,37 @@
|
|||||||
.fit-width {
|
.fit-width {
|
||||||
-fx-pref-width: 100%;
|
-fx-pref-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* HTML Renderer *
|
||||||
|
* *
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
.html {
|
||||||
|
-fx-font-size: 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.html-hyperlink {
|
||||||
|
-fx-fill: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.html-h1 {
|
||||||
|
-fx-font-size: 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.html-h2 {
|
||||||
|
-fx-font-size: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.html-h3 {
|
||||||
|
-fx-font-size: 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
.html-bold {
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.html-italic {
|
||||||
|
-fx-font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
@@ -978,6 +978,10 @@ datapack.choose_datapack=Select a datapack to import
|
|||||||
datapack.extension=Datapack
|
datapack.extension=Datapack
|
||||||
datapack.title=World %s - Datapacks
|
datapack.title=World %s - Datapacks
|
||||||
|
|
||||||
|
web.failed=Page loading failed
|
||||||
|
web.open_in_browser=Do you want to open this address in a browser:\n%s
|
||||||
|
web.view_in_browser=View in browser
|
||||||
|
|
||||||
world=Worlds
|
world=Worlds
|
||||||
world.add=Add a World (.zip)
|
world.add=Add a World (.zip)
|
||||||
world.datapack=Manage Datapacks
|
world.datapack=Manage Datapacks
|
||||||
|
|||||||
@@ -847,6 +847,10 @@ datapack.choose_datapack=選擇要匯入的資料包壓縮檔
|
|||||||
datapack.extension=資料包
|
datapack.extension=資料包
|
||||||
datapack.title=世界 %s - 資料包
|
datapack.title=世界 %s - 資料包
|
||||||
|
|
||||||
|
web.failed=加載頁面失敗
|
||||||
|
web.open_in_browser=是否要在瀏覽器中打開此連結:\n%s
|
||||||
|
web.view_in_browser=在瀏覽器中查看
|
||||||
|
|
||||||
world=世界
|
world=世界
|
||||||
world.add=加入世界
|
world.add=加入世界
|
||||||
world.datapack=管理資料包
|
world.datapack=管理資料包
|
||||||
|
|||||||
@@ -846,6 +846,10 @@ datapack.choose_datapack=选择要导入的数据包压缩包
|
|||||||
datapack.extension=数据包
|
datapack.extension=数据包
|
||||||
datapack.title=世界 %s - 数据包
|
datapack.title=世界 %s - 数据包
|
||||||
|
|
||||||
|
web.failed=加载页面失败
|
||||||
|
web.open_in_browser=是否要在浏览器中打开此链接:\n%s
|
||||||
|
web.view_in_browser=在浏览器中查看
|
||||||
|
|
||||||
world=世界
|
world=世界
|
||||||
world.add=添加世界
|
world.add=添加世界
|
||||||
world.datapack=管理数据包
|
world.datapack=管理数据包
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ dependencies {
|
|||||||
api("com.github.steveice10:opennbt:1.5")
|
api("com.github.steveice10:opennbt:1.5")
|
||||||
api("org.nanohttpd:nanohttpd:2.3.1")
|
api("org.nanohttpd:nanohttpd:2.3.1")
|
||||||
api("org.apache.commons:commons-compress:1.25.0")
|
api("org.apache.commons:commons-compress:1.25.0")
|
||||||
|
api("org.jsoup:jsoup:1.18.1")
|
||||||
compileOnlyApi("org.jetbrains:annotations:24.1.0")
|
compileOnlyApi("org.jetbrains:annotations:24.1.0")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,8 @@ data class Platform(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val classifier: String,
|
val classifier: String,
|
||||||
val groupId: String = "org.openjfx",
|
val groupId: String = "org.openjfx",
|
||||||
val version: String = jfxVersion,
|
val version: String = jfxVersion
|
||||||
val unsupportedModules: List<String> = listOf()
|
|
||||||
) {
|
) {
|
||||||
val modules: List<String> = jfxModules.filter { it !in unsupportedModules }
|
|
||||||
|
|
||||||
fun fileUrl(
|
fun fileUrl(
|
||||||
module: String, classifier: String, ext: String,
|
module: String, classifier: String, ext: String,
|
||||||
repo: String = "https://repo1.maven.org/maven2"
|
repo: String = "https://repo1.maven.org/maven2"
|
||||||
@@ -28,22 +25,22 @@ data class Platform(
|
|||||||
).toURL()
|
).toURL()
|
||||||
}
|
}
|
||||||
|
|
||||||
val jfxModules = listOf("base", "graphics", "controls", "media", "web")
|
val jfxModules = listOf("base", "graphics", "controls")
|
||||||
val jfxMirrorRepos = listOf("https://mirrors.cloud.tencent.com/nexus/repository/maven-public")
|
val jfxMirrorRepos = listOf("https://mirrors.cloud.tencent.com/nexus/repository/maven-public")
|
||||||
val jfxDependenciesFile = project("HMCL").layout.buildDirectory.file("openjfx-dependencies.json").get().asFile
|
val jfxDependenciesFile = project("HMCL").layout.buildDirectory.file("openjfx-dependencies.json").get().asFile
|
||||||
val jfxPlatforms = listOf(
|
val jfxPlatforms = listOf(
|
||||||
Platform("windows-x86", "win-x86"),
|
Platform("windows-x86", "win-x86"),
|
||||||
Platform("windows-x86_64", "win"),
|
Platform("windows-x86_64", "win"),
|
||||||
Platform("windows-arm64", "win", groupId = "org.glavo.hmcl.openjfx", version = "18.0.2+1-arm64", unsupportedModules = listOf("media", "web")),
|
Platform("windows-arm64", "win", groupId = "org.glavo.hmcl.openjfx", version = "18.0.2+1-arm64"),
|
||||||
Platform("osx-x86_64", "mac"),
|
Platform("osx-x86_64", "mac"),
|
||||||
Platform("osx-arm64", "mac-aarch64"),
|
Platform("osx-arm64", "mac-aarch64"),
|
||||||
Platform("linux-x86_64", "linux"),
|
Platform("linux-x86_64", "linux"),
|
||||||
Platform("linux-arm32", "linux-arm32-monocle", unsupportedModules = listOf("media", "web")),
|
Platform("linux-arm32", "linux-arm32-monocle"),
|
||||||
Platform("linux-arm64", "linux-aarch64"),
|
Platform("linux-arm64", "linux-aarch64"),
|
||||||
Platform("linux-loongarch64", "linux", groupId = "org.glavo.hmcl.openjfx", version = "17.0.8-loongarch64"),
|
Platform("linux-loongarch64", "linux", groupId = "org.glavo.hmcl.openjfx", version = "17.0.8-loongarch64"),
|
||||||
Platform("linux-loongarch64_ow", "linux", groupId = "org.glavo.hmcl.openjfx", version = "19-ea+10-loongson64", unsupportedModules = listOf("media", "web")),
|
Platform("linux-loongarch64_ow", "linux", groupId = "org.glavo.hmcl.openjfx", version = "19-ea+10-loongson64"),
|
||||||
Platform("linux-riscv64", "linux", groupId = "org.glavo.hmcl.openjfx", version = "19.0.2.1-riscv64", unsupportedModules = listOf("media", "web")),
|
Platform("linux-riscv64", "linux", groupId = "org.glavo.hmcl.openjfx", version = "19.0.2.1-riscv64"),
|
||||||
Platform("freebsd-x86_64", "freebsd", groupId = "org.glavo.hmcl.openjfx", version = "14.0.2.1-freebsd", unsupportedModules = listOf("media", "web")),
|
Platform("freebsd-x86_64", "freebsd", groupId = "org.glavo.hmcl.openjfx", version = "14.0.2.1-freebsd"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val jfxInClasspath =
|
val jfxInClasspath =
|
||||||
@@ -93,7 +90,7 @@ rootProject.tasks.create("generateOpenJFXDependencies") {
|
|||||||
|
|
||||||
doLast {
|
doLast {
|
||||||
val jfxDependencies = jfxPlatforms.associate { platform ->
|
val jfxDependencies = jfxPlatforms.associate { platform ->
|
||||||
platform.name to platform.modules.map { module ->
|
platform.name to jfxModules.map { module ->
|
||||||
mapOf(
|
mapOf(
|
||||||
"module" to "javafx.$module",
|
"module" to "javafx.$module",
|
||||||
"groupId" to platform.groupId,
|
"groupId" to platform.groupId,
|
||||||
@@ -117,7 +114,7 @@ rootProject.tasks.create("preTouchOpenJFXDependencies") {
|
|||||||
doLast {
|
doLast {
|
||||||
for (repo in jfxMirrorRepos) {
|
for (repo in jfxMirrorRepos) {
|
||||||
for (platform in jfxPlatforms) {
|
for (platform in jfxPlatforms) {
|
||||||
for (module in platform.modules) {
|
for (module in jfxModules) {
|
||||||
val url = platform.fileUrl(module, platform.classifier, "jar", repo = repo)
|
val url = platform.fileUrl(module, platform.classifier, "jar", repo = repo)
|
||||||
logger.quiet("Getting $url")
|
logger.quiet("Getting $url")
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user