代码清理与修复 UI 卡顿资源占用过高的问题 (#1849)

* Lazy initialization of Swing

* Load ISRG Root X1 certificate only on Java 8

* Replace JOptionPane with JavaFX Alert

* Avoid using java.awt.Desktop

* Rewrite TexturesLoader

* Optimization SelfDependencyPatcher

* fix typo

* close #968: Use computeIfAbsent to ensure thread safety

* Optimization GameVersion::minecraftVersion

* code cleanup

* Set the initial capacity of readFullyWithoutClosing

* code cleanup

* Mark inner classes as static if possible

* Cache version icon

* Code cleanup

* Fix ListView scrolling performance issues

* DatapackListPage::items

* Replace OutputStream with FileChannel::write
This commit is contained in:
Glavo 2022-11-23 16:33:14 +08:00 committed by GitHub
parent 035469240d
commit 0f26ae5cfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 588 additions and 421 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ hs_err_pid*
.mine*
/externalgames
NVIDIA
minecraft-exported-crash-info*
# gradle build
/build/

View File

@ -40,7 +40,6 @@ import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.CookieHandler;
@ -131,7 +130,8 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
initIcon();
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
initIcon();
UpdateChecker.init();
@ -149,8 +149,7 @@ public final class Launcher extends Application {
}
private void initIcon() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Launcher.class.getResource("/assets/img/icon.png"));
AwtUtils.setAppleIcon(image);
}

View File

@ -21,12 +21,13 @@ import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -58,13 +59,11 @@ public final class Main {
// Fix title bar not displaying in GTK systems
System.setProperty("jdk.gtk.version", "2");
// Use System look and feel
initLookAndFeel();
checkDirectoryPath();
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
@ -73,15 +72,6 @@ public final class Main {
Launcher.main(args);
}
private static void initLookAndFeel() {
if (System.getProperty("swing.defaultlaf") == null) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable ignored) {
}
}
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
@ -126,7 +116,7 @@ public final class Main {
} catch (Throwable ignored) {
}
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
SwingUtils.showErrorDialog(message);
System.exit(1);
}
@ -144,7 +134,8 @@ public final class Main {
}
} catch (Throwable ignored) {
}
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
SwingUtils.showWarningDialog(message);
}
static void fixLetsEncrypt() {

View File

@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ -104,7 +103,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to load html");

View File

@ -17,9 +17,13 @@
*/
package org.jackhuang.hmcl.game;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
@ -31,18 +35,15 @@ import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@ -64,15 +65,15 @@ public final class TexturesLoader {
// ==== Texture Loading ====
public static class LoadedTexture {
private final BufferedImage image;
private final Image image;
private final Map<String, String> metadata;
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
public LoadedTexture(Image image, Map<String, String> metadata) {
this.image = requireNonNull(image);
this.metadata = requireNonNull(metadata);
}
public BufferedImage getImage() {
public Image getImage() {
return image;
}
@ -96,7 +97,7 @@ public final class TexturesLoader {
return TEXTURES_DIR.resolve(prefix).resolve(hash);
}
public static LoadedTexture loadTexture(Texture texture) throws IOException {
public static LoadedTexture loadTexture(Texture texture) throws Throwable {
if (StringUtils.isBlank(texture.getUrl())) {
throw new IOException("Texture url is empty");
}
@ -117,12 +118,13 @@ public final class TexturesLoader {
}
}
BufferedImage img;
Image img;
try (InputStream in = Files.newInputStream(file)) {
img = ImageIO.read(in);
img = new Image(in);
}
if (img == null)
throw new IOException("Texture is malformed");
if (img.isError())
throw img.getException();
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
@ -141,11 +143,16 @@ public final class TexturesLoader {
}
private static void loadDefaultSkin(String path, TextureModel model) {
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
Image skin;
try {
skin = new Image(path);
if (skin.isError())
throw skin.getException();
} catch (Throwable e) {
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
throw new ResourceNotFoundError("Cannot load default skin from " + path, e);
}
DEFAULT_SKINS.put(model, new LoadedTexture(skin, singletonMap("model", model.modelName)));
}
public static LoadedTexture getDefaultSkin(TextureModel model) {
@ -172,7 +179,7 @@ public final class TexturesLoader {
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
@ -195,7 +202,7 @@ public final class TexturesLoader {
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
@ -209,38 +216,109 @@ public final class TexturesLoader {
// ====
// ==== Avatar ====
public static BufferedImage toAvatar(BufferedImage skin, int size) {
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = avatar.createGraphics();
public static void drawAvatar(Canvas canvas, Image skin) {
canvas.getGraphicsContext2D().clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
int scale = skin.getWidth() / 64;
int size = (int) canvas.getWidth();
int scale = (int) skin.getWidth() / 64;
int faceOffset = (int) Math.round(size / 18.0);
GraphicsContext g = canvas.getGraphicsContext2D();
try {
g.setImageSmoothing(false);
drawAvatarFX(g, skin, size, scale, faceOffset);
} catch (NoSuchMethodError ignored) {
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing, fallback to Java 2D
drawAvatarJ2D(g, skin, size, scale, faceOffset);
}
}
private static void drawAvatarFX(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
g.drawImage(skin,
8 * scale, 8 * scale, 8 * scale, 8 * scale,
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
g.drawImage(skin,
40 * scale, 8 * scale, 8 * scale, 8 * scale,
0, 0, size, size);
}
private static void drawAvatarJ2D(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
BufferedImage bi = FXUtils.fromFXImage(skin);
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = avatar.createGraphics();
g2d.drawImage(bi,
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
8 * scale, 8 * scale, 16 * scale, 16 * scale,
null);
g.drawImage(skin,
g2d.drawImage(bi,
0, 0, size, size,
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
g.dispose();
return avatar;
g2d.dispose();
PixelWriter pw = g.getPixelWriter();
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
pw.setArgb(x, y, avatar.getRGB(x, y));
}
}
}
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(FXUtils::toFXImage);
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
final WeakReference<Canvas> canvasRef;
final ObjectBinding<LoadedTexture> binding;
SkinBindingChangeListener(Canvas canvas, ObjectBinding<LoadedTexture> binding) {
this.canvasRef = new WeakReference<>(canvas);
this.binding = binding;
}
@Override
public void changed(ObservableValue<? extends LoadedTexture> observable,
LoadedTexture oldValue, LoadedTexture loadedTexture) {
Canvas canvas = canvasRef.get();
if (canvas != null)
drawAvatar(canvas, loadedTexture.image);
}
}
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
return BindingMapping.of(skinBinding(account))
.map(it -> toAvatar(it.image, size))
.map(FXUtils::toFXImage);
} else {
return Bindings.createObjectBinding(
() -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
public static void fxAvatarBinding(Canvas canvas, ObjectBinding<LoadedTexture> skinBinding) {
synchronized (SkinBindingChangeListener.hole) {
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
if (oldListener != null)
oldListener.binding.removeListener(oldListener);
SkinBindingChangeListener listener = new SkinBindingChangeListener(canvas, skinBinding);
listener.changed(skinBinding, null, skinBinding.get());
skinBinding.addListener(listener);
SkinBindingChangeListener.hole.put(canvas, listener);
}
}
public static void bindAvatar(Canvas canvas, YggdrasilService service, UUID uuid) {
fxAvatarBinding(canvas, skinBinding(service, uuid));
}
public static void bindAvatar(Canvas canvas, Account account) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount)
fxAvatarBinding(canvas, skinBinding(account));
else {
unbindAvatar(canvas);
drawAvatar(canvas, getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image);
}
}
public static void unbindAvatar(Canvas canvas) {
synchronized (SkinBindingChangeListener.hole) {
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
if (oldListener != null)
oldListener.binding.removeListener(oldListener);
}
}
// ====

View File

@ -27,7 +27,8 @@ public enum VersionIconType {
CRAFT_TABLE("/assets/img/craft_table.png"),
FABRIC("/assets/img/fabric.png"),
FORGE("/assets/img/forge.png"),
FURNACE("/assets/img/furnace.png");
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png");
// Please append new items at last

View File

@ -24,6 +24,7 @@ import javafx.beans.property.*;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
@ -41,8 +42,6 @@ import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.stream.Collectors;
import static com.jfoenix.concurrency.JFXUtilities.runInFX;
/**
*
* @author huangyuhui
@ -660,9 +659,7 @@ public final class VersionSetting implements Cloneable {
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
runInFX(() -> {
setJava("Auto");
});
FXUtils.runInFX(() -> setJava("Auto"));
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()

View File

@ -34,10 +34,8 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
@ -49,6 +47,7 @@ import javafx.scene.text.TextFlow;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.util.Logging;
@ -58,6 +57,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@ -357,53 +357,77 @@ public final class FXUtils {
public static void openFolder(File file) {
if (!FileUtils.makeDirectory(file)) {
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
LOG.log(Level.SEVERE, "Unable to make directory " + file);
return;
}
String path = file.getAbsolutePath();
switch (OperatingSystem.CURRENT_OS) {
case OSX:
String openCommand;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
openCommand = "explorer.exe";
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommand = "/usr/bin/open";
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && new File("/usr/bin/xdg-open").exists())
openCommand = "/usr/bin/xdg-open";
else
openCommand = null;
thread(() -> {
if (openCommand != null) {
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path});
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e);
int exitCode = SystemUtils.callExternalProcess(openCommand, path);
// explorer.exe always return 1
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
return;
else
LOG.warning("Open " + path + " failed with code " + exitCode);
} catch (Throwable e) {
LOG.log(Level.WARNING, "Unable to open " + path + " by executing " + openCommand, e);
}
break;
default:
thread(() -> {
if (java.awt.Desktop.isDesktopSupported()) {
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
}
});
}
}
// Fallback to java.awt.Desktop::open
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
});
}
public static void showFileInExplorer(Path file) {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
String path = file.toAbsolutePath().toString();
String[] openCommands;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
openCommands = new String[]{"explorer.exe", "/select,", path};
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommands = new String[]{"/usr/bin/open", "-R", path};
else
openCommands = null;
if (openCommands != null) {
thread(() -> {
try {
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select," + file, e);
int exitCode = SystemUtils.callExternalProcess(openCommands);
// explorer.exe always return 1
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
return;
else
LOG.warning("Show " + path + " in explorer failed with code " + exitCode);
} catch (Throwable e) {
LOG.log(Level.WARNING, "Unable to show " + path + " in explorer", e);
}
break;
case OSX:
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", "-R", file.toAbsolutePath().toString()});
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing /usr/bin/open -R " + file, e);
}
break;
default:
// We do not have an universal method to show file in file manager.
// Fallback to open folder
openFolder(file.getParent().toFile());
break;
});
} else {
// We do not have a universal method to show file in file manager.
openFolder(file.getParent().toFile());
}
}
@ -426,34 +450,38 @@ public final class FXUtils {
if (link == null)
return;
if (java.awt.Desktop.isDesktopSupported()) {
thread(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
for (String browser : linuxBrowsers) {
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
if (is.read() != -1) {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
}
} catch (Throwable ignored) {
}
Logging.LOG.log(Level.WARNING, "No known browser found");
}
}
thread(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
return;
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
LOG.log(Level.WARNING, "An exception occurred while calling rundll32", e);
}
});
}
} if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
for (String browser : linuxBrowsers) {
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
if (is.read() != -1) {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
}
} catch (Throwable ignored) {
}
Logging.LOG.log(Level.WARNING, "No known browser found");
}
}
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
}
});
}
public static void showWebDialog(String title, String content) {
@ -469,12 +497,12 @@ public final class FXUtils {
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.log(Level.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);
//noinspection ConstantConditions
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
frame.setLayout(new BorderLayout());
@ -632,10 +660,17 @@ public final class FXUtils {
}
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
MutableObject<Object> lastCell = new MutableObject<>();
return view -> new JFXListCell<T>() {
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
return;
lastCell.setValue(this);
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(graphicBuilder.apply(item));
@ -688,11 +723,12 @@ public final class FXUtils {
// Based on https://stackoverflow.com/a/57552025
// Fix #874: Use it instead of SwingFXUtils.toFXImage
public static WritableImage toFXImage(BufferedImage image) {
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
PixelWriter pw = wr.getPixelWriter();
final int iw = image.getWidth();
final int ih = image.getHeight();
WritableImage wr = new WritableImage(iw, ih);
PixelWriter pw = wr.getPixelWriter();
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
pw.setArgb(x, y, image.getRGB(x, y));
@ -701,6 +737,21 @@ public final class FXUtils {
return wr;
}
public static BufferedImage fromFXImage(Image image) {
final int iw = (int) image.getWidth();
final int ih = (int) image.getHeight();
PixelReader pr = image.getPixelReader();
BufferedImage bufferedImage = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB);
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
bufferedImage.setRGB(x, y, pr.getArgb(x, y));
}
}
return bufferedImage;
}
public static void copyText(String text) {
ClipboardContent content = new ClipboardContent();
content.putString(text);

View File

@ -48,7 +48,6 @@ import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -239,15 +238,11 @@ public class GameCrashWindow extends Stage {
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.thenRunAsync(() -> {
FXUtils.showFileInExplorer(logFile);
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
}
}
}, Schedulers.javafx())
.exceptionally(e -> {
LOG.log(Level.WARNING, "Failed to export game crash info", e);

View File

@ -34,23 +34,19 @@ import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayDeque;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.stream.Collectors;
@ -155,12 +151,12 @@ public final class LogWindow extends Stage {
public class LogWindowImpl extends Control {
private ListView<Log> listView = new JFXListView<>();
private BooleanProperty autoScroll = new SimpleBooleanProperty();
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private JFXComboBox<String> cboLines = new JFXComboBox<>();
private BooleanProperty showCrashReport = new SimpleBooleanProperty();
private final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
LogWindowImpl() {
getStyleClass().add("log-window");
@ -207,13 +203,13 @@ public final class LogWindow extends Stage {
return;
}
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
}
}
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
});
FXUtils.showFileInExplorer(logFile);
});
}
@ -229,13 +225,13 @@ public final class LogWindow extends Stage {
}
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
private static PseudoClass ERROR = PseudoClass.getPseudoClass("error");
private static PseudoClass WARN = PseudoClass.getPseudoClass("warn");
private static PseudoClass INFO = PseudoClass.getPseudoClass("info");
private static PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
private static PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
private static final PseudoClass WARN = PseudoClass.getPseudoClass("warn");
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
ToggleButton button = new ToggleButton();
@ -292,6 +288,7 @@ public final class LogWindow extends Stage {
listView.scrollTo(listView.getItems().size() - 1);
});
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
MutableObject<Object> lastCell = new MutableObject<>();
listView.setCellFactory(x -> new ListCell<Log>() {
{
getStyleClass().add("log-window-list-cell");
@ -308,6 +305,12 @@ public final class LogWindow extends Stage {
@Override
protected void updateItem(Log item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
return;
lastCell.setValue(this);
pseudoClassStateChanged(EMPTY, empty);
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);

View File

@ -0,0 +1,33 @@
package org.jackhuang.hmcl.ui;
import javax.swing.*;
public final class SwingUtils {
static {
if (System.getProperty("swing.defaultlaf") == null) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable ignored) {
}
}
}
private SwingUtils() {
}
public static void initLookAndFeel() {
// Make sure the static constructor is called
}
public static void showInfoDialog(Object message) {
JOptionPane.showMessageDialog(null, message, "Info", JOptionPane.INFORMATION_MESSAGE);
}
public static void showWarningDialog(Object message) {
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
}
public static void showErrorDialog(Object message) {
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
}
}

View File

@ -22,9 +22,8 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
@ -34,20 +33,18 @@ import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static javafx.beans.binding.Bindings.createStringBinding;
import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory;
import static org.jackhuang.hmcl.setting.Accounts.getLocalizedLoginTypeName;
import static org.jackhuang.hmcl.ui.FXUtils.toFXImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
private final Canvas canvas;
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
private final ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
@Override
protected void invalidated() {
@ -55,17 +52,19 @@ public class AccountAdvancedListItem extends AdvancedListItem {
if (account == null) {
titleProperty().unbind();
subtitleProperty().unbind();
imageView.imageProperty().unbind();
tooltip.textProperty().unbind();
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageView.setImage(toFXImage(TexturesLoader.toAvatar(TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage(), 32)));
tooltip.setText(i18n("account.create"));
TexturesLoader.unbindAvatar(canvas);
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage());
} else {
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
subtitleProperty().bind(accountSubtitle(account));
imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
tooltip.textProperty().bind(accountTooltip(account));
TexturesLoader.bindAvatar(canvas, account);
}
}
};
@ -74,9 +73,8 @@ public class AccountAdvancedListItem extends AdvancedListItem {
tooltip = new Tooltip();
FXUtils.installFastTooltip(this, tooltip);
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
canvas = new Canvas(32, 32);
setLeftGraphic(canvas);
setActionButtonVisible(false);

View File

@ -20,14 +20,11 @@ package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthenticationException;
@ -39,7 +36,6 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@ -70,7 +66,6 @@ public class AccountListItem extends RadioButton {
private final Account account;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
public AccountListItem(Account account) {
this.account = account;
@ -95,8 +90,6 @@ public class AccountListItem extends RadioButton {
account.getUsername().isEmpty() ? characterName :
Bindings.concat(account.getUsername(), " - ", characterName));
}
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
}
@Override
@ -226,16 +219,4 @@ public class AccountListItem extends RadioButton {
public StringProperty subtitleProperty() {
return subtitle;
}
public Image getImage() {
return image.get();
}
public void setImage(Image image) {
this.image.set(image);
}
public ObjectProperty<Image> imageProperty() {
return image;
}
}

View File

@ -21,15 +21,16 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
@ -64,9 +65,8 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().bind(skinnable.imageProperty());
Canvas canvas = new Canvas(32, 32);
TexturesLoader.bindAvatar(canvas, skinnable.getAccount());
Label title = new Label();
title.getStyleClass().add("title");
@ -84,7 +84,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
item.getStyleClass().add("two-line-list-item");
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
center.getChildren().setAll(canvas, item);
root.setCenter(center);
HBox right = new HBox();

View File

@ -30,10 +30,10 @@ import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TextInputControl;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.CharacterSelector;
@ -652,12 +652,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
Platform.runLater(() -> {
for (GameProfile profile : profiles) {
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
FXUtils.limitSize(portraitView, 32, 32);
Canvas portraitCanvas = new Canvas(32, 32);
TexturesLoader.bindAvatar(portraitCanvas, service, profile.getId());
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
IconedItem accountItem = new IconedItem(portraitCanvas, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();

View File

@ -22,6 +22,7 @@ import javafx.css.PseudoClass;
import javafx.scene.control.ListCell;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.ui.FXUtils;
public abstract class MDListCell<T> extends ListCell<T> {
@ -29,8 +30,11 @@ public abstract class MDListCell<T> extends ListCell<T> {
private final StackPane container = new StackPane();
private final StackPane root = new StackPane();
private final MutableObject<Object> lastCell;
public MDListCell(JFXListView<T> listView, MutableObject<Object> lastCell) {
this.lastCell = lastCell;
public MDListCell(JFXListView<T> listView) {
setText(null);
setGraphic(null);
@ -50,6 +54,14 @@ public abstract class MDListCell<T> extends ListCell<T> {
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (lastCell != null) {
if (this == lastCell.getValue() && !isVisible())
return;
lastCell.setValue(this);
}
updateControl(item, empty);
if (empty) {
setGraphic(null);

View File

@ -163,7 +163,7 @@ public class DecoratorController {
config().backgroundImageUrlProperty()));
}
private Image defaultBackground = newImage("/assets/img/background.jpg");
private final Image defaultBackground = newImage("/assets/img/background.jpg");
/**
* Load background image from bg/, background.png, background.jpg

View File

@ -242,7 +242,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
tab.select(worldTab);
}
private class DownloadNavigator implements Navigation {
private static final class DownloadNavigator implements Navigation {
private final Map<String, Object> settings = new HashMap<>();
@Override

View File

@ -31,6 +31,7 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
@ -40,7 +41,10 @@ import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@ -52,9 +56,9 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ -122,86 +126,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
list.setCellFactory(listView -> new ListCell<RemoteVersion>() {
IconedTwoLineListItem content = new IconedTwoLineListItem();
RipplerContainer ripplerContainer = new RipplerContainer(content);
StackPane pane = new StackPane();
{
pane.getStyleClass().add("md-list-cell");
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
pane.getChildren().setAll(ripplerContainer);
}
@Override
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
super.updateItem(remoteVersion, empty);
if (empty) {
setGraphic(null);
return;
}
setGraphic(pane);
content.setTitle(remoteVersion.getSelfVersion());
if (remoteVersion.getReleaseDate() != null) {
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
} else {
content.setSubtitle("");
}
if (remoteVersion instanceof GameRemoteVersion) {
switch (remoteVersion.getVersionType()) {
case RELEASE:
content.getTags().setAll(i18n("version.game.release"));
content.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
break;
case SNAPSHOT:
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
break;
default:
content.getTags().setAll(i18n("version.game.old"));
content.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
break;
}
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
content.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof ForgeRemoteVersion) {
content.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof FabricRemoteVersion) {
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof FabricAPIRemoteVersion) {
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
}
}
});
MutableObject<RemoteVersionListCell> lastCell = new MutableObject<>();
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell));
list.setOnMouseClicked(e -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
@ -290,4 +216,89 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
private void onSponsor() {
HMCLService.openRedirectLink("bmclapi_sponsor");
}
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
private static final EnumMap<VersionIconType, Image> icon = new EnumMap<>(VersionIconType.class);
private static Image getIcon(VersionIconType type) {
assert Platform.isFxApplicationThread();
return icon.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl(), 32, 32, false, true));
}
final IconedTwoLineListItem content = new IconedTwoLineListItem();
final RipplerContainer ripplerContainer = new RipplerContainer(content);
final StackPane pane = new StackPane();
private final MutableObject<RemoteVersionListCell> lastCell;
RemoteVersionListCell(MutableObject<RemoteVersionListCell> lastCell) {
this.lastCell = lastCell;
}
{
pane.getStyleClass().add("md-list-cell");
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
pane.getChildren().setAll(ripplerContainer);
}
@Override
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
super.updateItem(remoteVersion, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
return;
lastCell.setValue(this);
if (empty) {
setGraphic(null);
return;
}
setGraphic(pane);
content.setTitle(remoteVersion.getSelfVersion());
if (remoteVersion.getReleaseDate() != null) {
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
} else {
content.setSubtitle(null);
}
if (remoteVersion instanceof GameRemoteVersion) {
switch (remoteVersion.getVersionType()) {
case RELEASE:
content.getTags().setAll(i18n("version.game.release"));
content.setImage(getIcon(VersionIconType.GRASS));
break;
case SNAPSHOT:
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(getIcon(VersionIconType.COMMAND));
break;
default:
content.getTags().setAll(i18n("version.game.old"));
content.setImage(getIcon(VersionIconType.CRAFT_TABLE));
break;
}
} else {
VersionIconType iconType;
if (remoteVersion instanceof LiteLoaderRemoteVersion)
iconType = VersionIconType.CHICKEN;
else if (remoteVersion instanceof OptiFineRemoteVersion)
iconType = VersionIconType.COMMAND;
else if (remoteVersion instanceof ForgeRemoteVersion)
iconType = VersionIconType.FORGE;
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
iconType = VersionIconType.FABRIC;
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
iconType = VersionIconType.QUILT;
else
iconType = null;
content.setImage(iconType != null ? getIcon(iconType) : null);
if (content.getSubtitle() == null)
content.setSubtitle(remoteVersion.getGameVersion());
else
content.getTags().setAll(remoteVersion.getGameVersion());
}
}
}
}

View File

@ -30,6 +30,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.setting.Accounts;
@ -117,7 +118,8 @@ public class FeedbackPage extends VBox implements PageAware {
JFXListView<FeedbackResponse> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), feedbacks);
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView) {
MutableObject<Object> lastCell = new MutableObject<>();
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView, lastCell) {
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton likeButton = new JFXButton();
private final JFXButton unlikeButton = new JFXButton();
@ -230,7 +232,7 @@ public class FeedbackPage extends VBox implements PageAware {
Controllers.dialog(new AddFeedbackDialog());
}
private class LoginDialog extends JFXDialogLayout {
private static final class LoginDialog extends JFXDialogLayout {
private final SpinnerPane spinnerPane = new SpinnerPane();
private final Label errorLabel = new Label();
private final BooleanProperty logging = new SimpleBooleanProperty();
@ -241,10 +243,7 @@ public class FeedbackPage extends VBox implements PageAware {
VBox vbox = new VBox(8);
setBody(vbox);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
logging
? i18n("account.hmcl.hint")
: i18n("account.hmcl.hint")));
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> i18n("account.hmcl.hint")));
hintPane.setOnMouseClicked(e -> {
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
FXUtils.copyText(OAuthServer.lastlyOpenedURL);

View File

@ -35,7 +35,6 @@ import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@ -139,12 +138,7 @@ public final class SettingsPage extends SettingsView {
}
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException ignored) {
}
}
FXUtils.showFileInExplorer(logFile);
});
}

View File

@ -27,6 +27,7 @@ import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
@ -84,10 +85,17 @@ public class SponsorPage extends StackPane {
StackPane pane = new StackPane();
pane.getStyleClass().add("card");
listView = new JFXListView<>();
MutableObject<Object> lastCell = new MutableObject<>();
listView.setCellFactory((listView) -> new JFXListCell<Sponsor>() {
@Override
public void updateItem(Sponsor item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
return;
lastCell.setValue(this);
if (!empty) {
setText(item.getName());
setGraphic(null);

View File

@ -47,6 +47,8 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
private final Path worldDir;
private final Datapack datapack;
// Strongly referencing items, preventing GC collection
@SuppressWarnings("FieldCanBeLocal")
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
public DatapackListPage(String worldName, Path worldDir) {

View File

@ -31,6 +31,7 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.setting.Theme;
@ -84,12 +85,9 @@ class ModListPageSkin extends SkinBase<ModListPage> {
toolbarNormal.getChildren().setAll(
createToolbarButton2(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
createToolbarButton2(i18n("mods.add"), SVG::plus, skinnable::add),
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, () ->
skinnable.openModFolder()),
createToolbarButton2(i18n("mods.check_updates"), SVG::update, () ->
skinnable.checkUpdates()),
createToolbarButton2(i18n("download"), SVG::downloadOutline, () ->
skinnable.download()));
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, skinnable::openModFolder),
createToolbarButton2(i18n("mods.check_updates"), SVG::update, skinnable::checkUpdates),
createToolbarButton2(i18n("download"), SVG::downloadOutline, skinnable::download));
HBox toolbarSelecting = new HBox();
toolbarSelecting.getChildren().setAll(
createToolbarButton2(i18n("button.remove"), SVG::delete, () -> {
@ -121,7 +119,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new ModInfoListCell(listView));
MutableObject<Object> lastCell = new MutableObject<>();
listView.setCellFactory(x -> new ModInfoListCell(listView, lastCell));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
Bindings.bindContent(listView.getItems(), skinnable.getItems());
@ -276,10 +275,10 @@ class ModListPageSkin extends SkinBase<ModListPage> {
}
}
private static Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
private static Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
private static final Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
class ModInfoListCell extends MDListCell<ModInfoObject> {
final class ModInfoListCell extends MDListCell<ModInfoObject> {
JFXCheckBox checkBox = new JFXCheckBox();
TwoLineListItem content = new TwoLineListItem();
JFXButton restoreButton = new JFXButton();
@ -287,8 +286,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
JFXButton revealButton = new JFXButton();
BooleanProperty booleanProperty;
ModInfoListCell(JFXListView<ModInfoObject> listView) {
super(listView);
ModInfoListCell(JFXListView<ModInfoObject> listView, MutableObject<Object> lastCell) {
super(listView, lastCell);
HBox container = new HBox(8);
container.setPickOnBounds(false);

View File

@ -23,7 +23,6 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jetbrains.annotations.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
@ -123,7 +122,7 @@ public enum ModTranslations {
return true;
}
try {
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8);
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName));
mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
return true;
} catch (Exception e) {

View File

@ -27,6 +27,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.RemoteMod;
@ -158,8 +159,8 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
public static class ModUpdateCell extends MDListCell<LocalModFile.ModUpdate> {
TwoLineListItem content = new TwoLineListItem();
public ModUpdateCell(JFXListView<LocalModFile.ModUpdate> listView) {
super(listView);
public ModUpdateCell(JFXListView<LocalModFile.ModUpdate> listView, MutableObject<Object> lastCell) {
super(listView, lastCell);
getContainer().getChildren().setAll(content);
}

View File

@ -29,12 +29,12 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.UpgradeDialog;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import javax.swing.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -65,7 +65,7 @@ public final class UpdateHandler {
performMigration();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to perform migration", e);
JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE);
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
}
return true;
}
@ -75,13 +75,13 @@ public final class UpdateHandler {
applyUpdate(Paths.get(args[1]));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to apply update", e);
JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE);
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
}
return true;
}
if (isFirstLaunchAfterUpgrade()) {
JOptionPane.showMessageDialog(null, i18n("fatal.migration_requires_manual_reboot"), "Info", JOptionPane.INFORMATION_MESSAGE);
SwingUtils.showInfoDialog(i18n("fatal.migration_requires_manual_reboot"));
return true;
}

View File

@ -43,6 +43,7 @@ package org.jackhuang.hmcl.util;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.platform.Platform;
@ -55,6 +56,7 @@ import java.io.*;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.List;
import java.util.*;
import java.util.concurrent.CancellationException;
@ -72,6 +74,8 @@ public final class SelfDependencyPatcher {
private final List<DependencyDescriptor> dependencies = DependencyDescriptor.readDependencies();
private final List<Repository> repositories;
private final Repository defaultRepository;
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
private final MessageDigest digest = DigestUtils.getDigest("SHA-1");
private SelfDependencyPatcher() throws IncompatibleVersionException {
// We can only self-patch JavaFX on specific platform.
@ -261,9 +265,10 @@ public final class SelfDependencyPatcher {
* @throws IOException When the files cannot be fetched or saved.
*/
private void fetchDependencies(List<DependencyDescriptor> dependencies) throws IOException {
SwingUtils.initLookAndFeel();
boolean isFirstTime = true;
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
Repository repository = defaultRepository;
int count = 0;
@ -360,8 +365,18 @@ public final class SelfDependencyPatcher {
return missing;
}
private static void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
ChecksumMismatchException.verifyChecksum(dependency.localPath(), "SHA-1", dependency.sha1());
private void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
digest.reset();
try (InputStream is = Files.newInputStream(dependency.localPath())) {
int read;
while ((read = is.read(buffer, 0, IOUtils.DEFAULT_BUFFER_SIZE)) > -1) {
digest.update(buffer, 0, read);
}
}
String sha1 = Hex.encodeHex(digest.digest());
if (!dependency.sha1().equalsIgnoreCase(sha1))
throw new ChecksumMismatchException("SHA-1", dependency.sha1(), sha1);
}
public static class PatchException extends Exception {

View File

@ -253,7 +253,7 @@ public class OAuth {
DEVICE,
}
public class Result {
public static final class Result {
private final String accessToken;
private final String refreshToken;
@ -282,7 +282,7 @@ public class OAuth {
@SerializedName("verification_uri")
public String verificationURI;
// Life time in seconds for device_code and user_code
// Lifetime in seconds for device_code and user_code
@SerializedName("expires_in")
public int expiresIn;

View File

@ -25,7 +25,6 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.io.HttpServer;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.IOException;
import java.security.*;
@ -80,8 +79,7 @@ public class YggdrasilServer extends HttpServer {
}
private Response profiles(Request request) throws IOException {
String body = IOUtils.readFullyAsString(request.getSession().getInputStream(), UTF_8);
List<String> names = JsonUtils.fromNonNullJson(body, new TypeToken<List<String>>() {
List<String> names = JsonUtils.fromNonNullJsonFully(request.getSession().getInputStream(), new TypeToken<List<String>>() {
}.getType());
return ok(names.stream().distinct()
.map(this::findCharacterByName)

View File

@ -240,7 +240,7 @@ public class DefaultCacheRepository extends CacheRepository {
}
}
private class LibraryIndex implements Validation {
private static final class LibraryIndex implements Validation {
private final String name;
private final String hash;
private final String type;

View File

@ -68,8 +68,7 @@ public class ForgeOldInstallTask extends Task<Version> {
InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json"));
if (stream == null)
throw new ArtifactMalformedException("Malformed forge installer file, install_profile.json does not exist.");
String json = IOUtils.readFullyAsString(stream);
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class);
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJsonFully(stream, ForgeInstallProfile.class);
// unpack the universal jar in the installer file.
Library forgeLibrary = new Library(installProfile.getInstall().getPath());

View File

@ -67,7 +67,7 @@ public class DefaultGameRepository implements GameRepository {
private File baseDirectory;
protected Map<String, Version> versions;
private ConcurrentHashMap<File, Optional<String>> gameVersions = new ConcurrentHashMap<>();
private final ConcurrentHashMap<File, Optional<String>> gameVersions = new ConcurrentHashMap<>();
public DefaultGameRepository(File baseDirectory) {
this.baseDirectory = baseDirectory;
@ -145,19 +145,13 @@ public class DefaultGameRepository implements GameRepository {
// This implementation may cause multiple flows against the same version entering
// this function, which is accepted because GameVersion::minecraftVersion should
// be consistent.
File versionJar = getVersionJar(version);
if (gameVersions.containsKey(versionJar)) {
return gameVersions.get(versionJar);
} else {
return gameVersions.computeIfAbsent(getVersionJar(version), versionJar -> {
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
if (!gameVersion.isPresent()) {
LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists());
}
gameVersions.put(versionJar, gameVersion);
return gameVersion;
}
});
}
@Override

View File

@ -19,8 +19,6 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jenkinsci.constant_pool_scanner.ConstantPool;
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
import org.jenkinsci.constant_pool_scanner.ConstantType;
@ -28,15 +26,15 @@ import org.jenkinsci.constant_pool_scanner.StringConstant;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
@ -48,9 +46,9 @@ public final class GameVersion {
private GameVersion() {
}
private static Optional<String> getVersionFromJson(Path versionJson) {
private static Optional<String> getVersionFromJson(InputStream versionJson) {
try {
Map<?, ?> version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), Map.class);
Map<?, ?> version = JsonUtils.fromNonNullJsonFully(versionJson, Map.class);
return tryCast(version.get("name"), String.class);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Failed to parse version.json", e);
@ -58,7 +56,7 @@ public final class GameVersion {
}
}
private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException {
private static Optional<String> getVersionOfClassMinecraft(InputStream bytecode) throws IOException {
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
@ -68,7 +66,7 @@ public final class GameVersion {
.findFirst();
}
private static Optional<String> getVersionFromClassMinecraftServer(byte[] bytecode) throws IOException {
private static Optional<String> getVersionFromClassMinecraftServer(InputStream bytecode) throws IOException {
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
List<String> list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
@ -94,23 +92,29 @@ public final class GameVersion {
if (file == null || !file.exists() || !file.isFile() || !file.canRead())
return Optional.empty();
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
Path versionJson = gameJar.getPath("version.json");
if (Files.exists(versionJson)) {
Optional<String> result = getVersionFromJson(versionJson);
try (ZipFile gameJar = new ZipFile(file)) {
ZipEntry versionJson = gameJar.getEntry("version.json");
if (versionJson != null) {
Optional<String> result = getVersionFromJson(gameJar.getInputStream(versionJson));
if (result.isPresent())
return result;
}
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
if (Files.exists(minecraft)) {
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
if (result.isPresent())
return result;
ZipEntry minecraft = gameJar.getEntry("net/minecraft/client/Minecraft.class");
if (minecraft != null) {
try (InputStream is = gameJar.getInputStream(minecraft)) {
Optional<String> result = getVersionOfClassMinecraft(is);
if (result.isPresent())
return result;
}
}
ZipEntry minecraftServer = gameJar.getEntry("net/minecraft/server/MinecraftServer.class");
if (minecraftServer != null) {
try (InputStream is = gameJar.getInputStream(minecraftServer)) {
return getVersionFromClassMinecraftServer(is);
}
}
Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class");
if (Files.exists(minecraftServer))
return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer));
return Optional.empty();
} catch (IOException e) {
return Optional.empty();

View File

@ -67,7 +67,7 @@ public final class StringArgument implements Argument {
return argument;
}
public class Serializer implements JsonSerializer<StringArgument> {
public static final class Serializer implements JsonSerializer<StringArgument> {
@Override
public JsonElement serialize(StringArgument src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getArgument());

View File

@ -20,7 +20,6 @@ package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.IOException;
import java.nio.file.Path;
@ -112,7 +111,7 @@ public final class LiteModMetadata {
ZipEntry entry = zipFile.getEntry("litemod.json");
if (entry == null)
throw new IOException("File " + modFile + "is not a LiteLoader mod.");
LiteModMetadata metadata = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), LiteModMetadata.class);
LiteModMetadata metadata = JsonUtils.fromJsonFully(zipFile.getInputStream(entry), LiteModMetadata.class);
if (metadata == null)
throw new IOException("Mod " + modFile + " `litemod.json` is malformed.");
return new LocalModFile(modManager, modManager.getLocalMod(metadata.getName(), ModLoaderType.LITE_LOADER), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(),

View File

@ -26,10 +26,10 @@ import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Path;
@ -66,8 +66,8 @@ public final class McbbsModpackProvider implements ModpackProvider {
config.getManifest().injectLaunchOptions(builder);
}
private static Modpack fromManifestFile(String json, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
private static Modpack fromManifestFile(InputStream json, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJsonFully(json, McbbsModpackManifest.class);
return manifest.toModpack(encoding);
}
@ -75,11 +75,11 @@ public final class McbbsModpackProvider implements ModpackProvider {
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
if (mcbbsPackMeta != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(mcbbsPackMeta)), encoding);
return fromManifestFile(zip.getInputStream(mcbbsPackMeta), encoding);
}
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
if (manifestJson != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(manifestJson)), encoding);
return fromManifestFile(zip.getInputStream(manifestJson), encoding);
}
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
}

View File

@ -22,7 +22,6 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.IOException;
import java.util.List;
@ -60,8 +59,7 @@ public final class MultiMCManifest {
ZipArchiveEntry mmcPack = zipFile.getEntry(rootEntryName + "mmc-pack.json");
if (mmcPack == null)
return null;
String json = IOUtils.readFullyAsString(zipFile.getInputStream(mmcPack));
MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class);
MultiMCManifest manifest = JsonUtils.fromNonNullJsonFully(zipFile.getInputStream(mmcPack), MultiMCManifest.class);
if (manifest.getComponents() == null)
throw new IOException("mmc-pack.json malformed.");

View File

@ -239,7 +239,7 @@ public abstract class FetchTask<T> extends Task<T> {
CACHED
}
protected class DownloadState {
protected static final class DownloadState {
private final int startPosition;
private final int endPosition;
private final int currentPosition;
@ -272,9 +272,7 @@ public abstract class FetchTask<T> extends Task<T> {
}
}
protected class DownloadMission {
protected static final class DownloadMission {
}

View File

@ -20,7 +20,6 @@ package org.jackhuang.hmcl.task;
import javafx.application.Platform;
import org.jackhuang.hmcl.util.Logging;
import javax.swing.*;
import java.util.concurrent.*;
import static org.jackhuang.hmcl.util.Lang.threadPool;
@ -61,10 +60,6 @@ public final class Schedulers {
return Platform::runLater;
}
public static Executor swing() {
return SwingUtilities::invokeLater;
}
public static Executor defaultScheduler() {
return ForkJoinPool.commonPool();
}

View File

@ -27,9 +27,9 @@ import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
@ -54,7 +54,7 @@ public class CacheRepository {
private Path cacheDirectory;
private Path indexFile;
private Map<String, ETagItem> index;
private Map<String, Storage> storages = new HashMap<>();
private final Map<String, Storage> storages = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void changeDirectory(Path commonDir) {
@ -293,9 +293,8 @@ public class CacheRepository {
ETagIndex indexOnDisk = JsonUtils.fromMaybeMalformedJson(new String(IOUtils.readFullyWithoutClosing(Channels.newInputStream(channel)), UTF_8), ETagIndex.class);
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
channel.truncate(0);
OutputStream os = Channels.newOutputStream(channel);
ETagIndex writeTo = new ETagIndex(newIndex.values());
IOUtils.write(JsonUtils.GSON.toJson(writeTo).getBytes(UTF_8), os);
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(writeTo).getBytes(UTF_8)));
this.index = newIndex;
} finally {
lock.release();
@ -303,7 +302,7 @@ public class CacheRepository {
}
}
private class ETagIndex {
private static final class ETagIndex {
private final Collection<ETagItem> eTag;
public ETagIndex() {
@ -315,7 +314,7 @@ public class CacheRepository {
}
}
private class ETagItem {
private static final class ETagItem {
private final String url;
private final String eTag;
private final String hash;
@ -429,8 +428,7 @@ public class CacheRepository {
if (indexOnDisk == null) indexOnDisk = new HashMap<>();
indexOnDisk.putAll(storage);
channel.truncate(0);
OutputStream os = Channels.newOutputStream(channel);
IOUtils.write(JsonUtils.GSON.toJson(storage).getBytes(UTF_8), os);
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(storage).getBytes(UTF_8)));
this.storage = indexOnDisk;
} finally {
lock.release();

View File

@ -23,7 +23,11 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;
import java.util.UUID;
@ -44,6 +48,18 @@ public final class JsonUtils {
private JsonUtils() {
}
public static <T> T fromJsonFully(InputStream json, Class<T> classOfT) throws IOException, JsonParseException {
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
return GSON.fromJson(reader, classOfT);
}
}
public static <T> T fromJsonFully(InputStream json, Type type) throws IOException, JsonParseException {
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
return GSON.fromJson(reader, type);
}
}
public static <T> T fromNonNullJson(String json, Class<T> classOfT) throws JsonParseException {
T parsed = GSON.fromJson(json, classOfT);
if (parsed == null)
@ -58,6 +74,24 @@ public final class JsonUtils {
return parsed;
}
public static <T> T fromNonNullJsonFully(InputStream json, Class<T> classOfT) throws IOException, JsonParseException {
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
T parsed = GSON.fromJson(reader, classOfT);
if (parsed == null)
throw new JsonParseException("Json object cannot be null.");
return parsed;
}
}
public static <T> T fromNonNullJsonFully(InputStream json, Type type) throws IOException, JsonParseException {
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
T parsed = GSON.fromJson(reader, type);
if (parsed == null)
throw new JsonParseException("Json object cannot be null.");
return parsed;
}
}
public static <T> T fromMaybeMalformedJson(String json, Class<T> classOfT) throws JsonParseException {
try {
return GSON.fromJson(json, classOfT);

View File

@ -231,7 +231,7 @@ public final class CompressingUtils {
* @return the plain text content of given file.
*/
public static String readTextZipEntry(ZipFile zipFile, String name) throws IOException {
return IOUtils.readFullyAsString(zipFile.getInputStream(zipFile.getEntry(name)), StandardCharsets.UTF_8);
return IOUtils.readFullyAsString(zipFile.getInputStream(zipFile.getEntry(name)));
}
/**
@ -244,7 +244,7 @@ public final class CompressingUtils {
*/
public static String readTextZipEntry(Path zipFile, String name, Charset encoding) throws IOException {
try (ZipFile s = openZipFile(zipFile, encoding)) {
return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8);
return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)));
}
}

View File

@ -27,10 +27,11 @@ import java.net.HttpURLConnection;
import static java.nio.charset.StandardCharsets.UTF_8;
public class HttpMultipartRequest implements Closeable {
private static final String endl = "\r\n";
private final String boundary = "*****" + System.currentTimeMillis() + "*****";
private final HttpURLConnection urlConnection;
private final ByteArrayOutputStream stream;
private final String endl = "\r\n";
public HttpMultipartRequest(HttpURLConnection urlConnection) throws IOException {
this.urlConnection = urlConnection;
@ -69,7 +70,7 @@ public class HttpMultipartRequest implements Closeable {
addLine("--" + boundary + "--");
urlConnection.setRequestProperty("Content-Length", "" + stream.size());
try (OutputStream os = urlConnection.getOutputStream()) {
IOUtils.write(stream.toByteArray(), os);
stream.writeTo(os);
}
}
}

View File

@ -31,7 +31,6 @@ import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@ -143,7 +142,7 @@ public abstract class HttpRequest {
return getStringWithRetry(() -> {
HttpURLConnection con = createConnection();
con = resolveConnection(con);
return IOUtils.readFullyAsString(con.getInputStream(), StandardCharsets.UTF_8);
return IOUtils.readFullyAsString(con.getInputStream());
}, retryTimes);
}
}

View File

@ -18,7 +18,6 @@
package org.jackhuang.hmcl.util.io;
import java.io.*;
import java.nio.charset.Charset;
/**
* This utility class consists of some util methods operating on InputStream/OutputStream.
@ -40,7 +39,7 @@ public final class IOUtils {
* @throws IOException if an I/O error occurs.
*/
public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(stream.available(), 32));
copyTo(stream, result);
return result.toByteArray();
}
@ -54,7 +53,7 @@ public final class IOUtils {
*/
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
try (InputStream is = stream) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(is.available(), 32));
copyTo(is, result);
return result;
}
@ -68,18 +67,6 @@ public final class IOUtils {
return readFully(stream).toString("UTF-8");
}
public static String readFullyAsString(InputStream stream, Charset charset) throws IOException {
return readFully(stream).toString(charset.name());
}
public static void write(String text, OutputStream outputStream) throws IOException {
write(text.getBytes(), outputStream);
}
public static void write(byte[] bytes, OutputStream outputStream) throws IOException {
copyTo(new ByteArrayInputStream(bytes), outputStream);
}
public static void copyTo(InputStream src, OutputStream dest) throws IOException {
copyTo(src, dest, new byte[DEFAULT_BUFFER_SIZE]);
}

View File

@ -21,7 +21,6 @@ import org.jackhuang.hmcl.util.Pair;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Map.Entry;
@ -176,7 +175,7 @@ public final class NetworkUtils {
public static String doGet(URL url) throws IOException {
HttpURLConnection con = createHttpConnection(url);
con = resolveConnection(con);
return IOUtils.readFullyAsString(con.getInputStream(), StandardCharsets.UTF_8);
return IOUtils.readFullyAsString(con.getInputStream());
}
public static String doPost(URL u, Map<String, String> params) throws IOException {
@ -210,13 +209,13 @@ public final class NetworkUtils {
public static String readData(HttpURLConnection con) throws IOException {
try {
try (InputStream stdout = con.getInputStream()) {
return IOUtils.readFullyAsString(stdout, UTF_8);
return IOUtils.readFullyAsString(stdout);
}
} catch (IOException e) {
try (InputStream stderr = con.getErrorStream()) {
if (stderr == null)
throw e;
return IOUtils.readFullyAsString(stderr, UTF_8);
return IOUtils.readFullyAsString(stderr);
}
}
}

View File

@ -25,7 +25,6 @@ import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class CrashReportAnalyzerTest {
@ -35,7 +34,7 @@ public class CrashReportAnalyzerTest {
if (is == null) {
throw new IllegalStateException("Resource not found: " + path);
}
return IOUtils.readFullyAsString(is, StandardCharsets.UTF_8);
return IOUtils.readFullyAsString(is);
}
private CrashReportAnalyzer.Result findResultByRule(List<CrashReportAnalyzer.Result> results, CrashReportAnalyzer.Rule rule) {