diff --git a/.gitignore b/.gitignore index fe7d49884..3c4a8e406 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ hs_err_pid* .mine* /externalgames NVIDIA +minecraft-exported-crash-info* # gradle build /build/ diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index bcd49e1d6..0a9ba9e43 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -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); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java index 576aaad0e..26c0f1e5a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java @@ -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() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/OAuthServer.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/OAuthServer.java index 0cd9c5af6..7f309ec99 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/OAuthServer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/OAuthServer.java @@ -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"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java index ed402eb64..54944fe2d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -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 metadata; - public LoadedTexture(BufferedImage image, Map metadata) { + public LoadedTexture(Image image, Map 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 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 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 { + static final WeakHashMap hole = new WeakHashMap<>(); + + final WeakReference canvasRef; + final ObjectBinding binding; + + SkinBindingChangeListener(Canvas canvas, ObjectBinding binding) { + this.canvasRef = new WeakReference<>(canvas); + this.binding = binding; + } + + @Override + public void changed(ObservableValue observable, + LoadedTexture oldValue, LoadedTexture loadedTexture) { + Canvas canvas = canvasRef.get(); + if (canvas != null) + drawAvatar(canvas, loadedTexture.image); + } } - public static ObjectBinding 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 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); } } // ==== diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java index 7c6a5026a..d641472f1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java @@ -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 diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java index c5224c7ea..29cba156d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java @@ -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() diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index ff159d5a8..498cbdcd7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -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 Callback, ListCell> jfxListCellFactory(Function graphicBuilder) { + MutableObject lastCell = new MutableObject<>(); return view -> new JFXListCell() { @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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java index 3393001f9..ac7c93830 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index 36546176d..10f3f34ac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -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 listView = new JFXListView<>(); - private BooleanProperty autoScroll = new SimpleBooleanProperty(); - private List buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList()); - private List showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList()); - private JFXComboBox cboLines = new JFXComboBox<>(); - private BooleanProperty showCrashReport = new SimpleBooleanProperty(); + private final ListView listView = new JFXListView<>(); + private final BooleanProperty autoScroll = new SimpleBooleanProperty(); + private final List buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList()); + private final List showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList()); + private final JFXComboBox 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 { - 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 lastCell = new MutableObject<>(); listView.setCellFactory(x -> new ListCell() { { 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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SwingUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SwingUtils.java new file mode 100644 index 000000000..b1377385d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SwingUtils.java @@ -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); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java index 57d585b4b..ab0bfa0f9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java @@ -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 = new SimpleObjectProperty() { + private final ObjectProperty account = new SimpleObjectProperty() { @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 view = createImageView(null); - setLeftGraphic(view.getKey()); - imageView = view.getValue(); + canvas = new Canvas(32, 32); + setLeftGraphic(canvas); setActionButtonVisible(false); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java index 21ef0eb82..b6965a8d1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java @@ -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 = 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 imageProperty() { - return image; - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java index 03b1abf7b..c1709f9b3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java @@ -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 { 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 { 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(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java index ff6240e12..8e4287df7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java @@ -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 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(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java index 1534f5a8b..a0246c3e9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java @@ -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 extends ListCell { @@ -29,8 +30,11 @@ public abstract class MDListCell extends ListCell { private final StackPane container = new StackPane(); private final StackPane root = new StackPane(); + private final MutableObject lastCell; + + public MDListCell(JFXListView listView, MutableObject lastCell) { + this.lastCell = lastCell; - public MDListCell(JFXListView listView) { setText(null); setGraphic(null); @@ -50,6 +54,14 @@ public abstract class MDListCell extends ListCell { @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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java index 1009a2cdd..96dc823b1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -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 diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index dc0f6a075..0865d18b3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -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 settings = new HashMap<>(); @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index de0d763f0..055dd59a6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -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() { - 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 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 { + private static final EnumMap 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 lastCell; + + RemoteVersionListCell(MutableObject 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()); + } + } + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java index f67736d58..bca7f749c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java @@ -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 listView = new JFXListView<>(); spinnerPane.setContent(listView); Bindings.bindContent(listView.getItems(), feedbacks); - listView.setCellFactory(x -> new MDListCell(listView) { + MutableObject lastCell = new MutableObject<>(); + listView.setCellFactory(x -> new MDListCell(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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index 8e8f5390a..b53470944 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -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); }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SponsorPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SponsorPage.java index bd60c8ef1..ff6859719 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SponsorPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SponsorPage.java @@ -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 lastCell = new MutableObject<>(); listView.setCellFactory((listView) -> new JFXListCell() { @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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java index 865db3c8c..33e584ac4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java @@ -47,6 +47,8 @@ public class DatapackListPage extends ListPageBase items; public DatapackListPage(String worldName, Path worldDir) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index 7351997fa..f893885ce 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -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 { 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 { center.getStyleClass().add("large-spinner-pane"); center.loadingProperty().bind(skinnable.loadingProperty()); - listView.setCellFactory(x -> new ModInfoListCell(listView)); + MutableObject 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 { } } - private static Lazy menu = new Lazy<>(PopupMenu::new); - private static Lazy popup = new Lazy<>(() -> new JFXPopup(menu.get())); + private static final Lazy menu = new Lazy<>(PopupMenu::new); + private static final Lazy popup = new Lazy<>(() -> new JFXPopup(menu.get())); - class ModInfoListCell extends MDListCell { + final class ModInfoListCell extends MDListCell { JFXCheckBox checkBox = new JFXCheckBox(); TwoLineListItem content = new TwoLineListItem(); JFXButton restoreButton = new JFXButton(); @@ -287,8 +286,8 @@ class ModListPageSkin extends SkinBase { JFXButton revealButton = new JFXButton(); BooleanProperty booleanProperty; - ModInfoListCell(JFXListView listView) { - super(listView); + ModInfoListCell(JFXListView listView, MutableObject lastCell) { + super(listView, lastCell); HBox container = new HBox(8); container.setPickOnBounds(false); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java index 9ae05aa8e..8875f2f24 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java @@ -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) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 97d354f3c..f2a9e858e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -27,6 +27,7 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.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 { TwoLineListItem content = new TwoLineListItem(); - public ModUpdateCell(JFXListView listView) { - super(listView); + public ModUpdateCell(JFXListView listView, MutableObject lastCell) { + super(listView, lastCell); getContainer().getChildren().setAll(content); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 4fa0e2cab..4cd06ef8c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -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; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java index eeb48bd8b..edaa668e4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java @@ -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 dependencies = DependencyDescriptor.readDependencies(); private final List 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 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 { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java index 44e3d6237..7ae6d51a0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java @@ -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; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index 656c73a25..7fe17eb3a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -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 names = JsonUtils.fromNonNullJson(body, new TypeToken>() { + List names = JsonUtils.fromNonNullJsonFully(request.getSession().getInputStream(), new TypeToken>() { }.getType()); return ok(names.stream().distinct() .map(this::findCharacterByName) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java index d654edf3f..540764de0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java @@ -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; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java index f4c3da4c2..a1abeba25 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java @@ -68,8 +68,7 @@ public class ForgeOldInstallTask extends Task { 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()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index f3f9c4295..390bff947 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -67,7 +67,7 @@ public class DefaultGameRepository implements GameRepository { private File baseDirectory; protected Map versions; - private ConcurrentHashMap> gameVersions = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> 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 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 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java index ab9b5952c..e91379fe9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameVersion.java @@ -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 getVersionFromJson(Path versionJson) { + private static Optional 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 getVersionOfClassMinecraft(byte[] bytecode) throws IOException { + private static Optional 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 getVersionFromClassMinecraftServer(byte[] bytecode) throws IOException { + private static Optional getVersionFromClassMinecraftServer(InputStream bytecode) throws IOException { ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING); List 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 result = getVersionFromJson(versionJson); + try (ZipFile gameJar = new ZipFile(file)) { + ZipEntry versionJson = gameJar.getEntry("version.json"); + if (versionJson != null) { + Optional result = getVersionFromJson(gameJar.getInputStream(versionJson)); if (result.isPresent()) return result; } - Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class"); - if (Files.exists(minecraft)) { - Optional 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 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(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/StringArgument.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/StringArgument.java index 9181ab764..14c2f83fa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/StringArgument.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/StringArgument.java @@ -67,7 +67,7 @@ public final class StringArgument implements Argument { return argument; } - public class Serializer implements JsonSerializer { + public static final class Serializer implements JsonSerializer { @Override public JsonElement serialize(StringArgument src, Type typeOfSrc, JsonSerializationContext context) { return new JsonPrimitive(src.getArgument()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java index 5e4e7da60..f80393a55 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java @@ -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(), diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java index 90eaa4a83..2deaf7233 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java @@ -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"); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCManifest.java index 6989ef270..7773d8fa4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCManifest.java @@ -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."); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java index c4dea72ca..5ff3154f6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java @@ -239,7 +239,7 @@ public abstract class FetchTask extends Task { 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 extends Task { } } - protected class DownloadMission { - - + protected static final class DownloadMission { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Schedulers.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Schedulers.java index 9735df3d1..d40dc870b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Schedulers.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Schedulers.java @@ -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(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java index 2333b2320..9b68bc6fe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java @@ -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 index; - private Map storages = new HashMap<>(); + private final Map 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 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 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(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index 67dc43d1b..fec40f665 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -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 fromJsonFully(InputStream json, Class classOfT) throws IOException, JsonParseException { + try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) { + return GSON.fromJson(reader, classOfT); + } + } + + public static 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 fromNonNullJson(String json, Class 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 fromNonNullJsonFully(InputStream json, Class 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 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 fromMaybeMalformedJson(String json, Class classOfT) throws JsonParseException { try { return GSON.fromJson(json, classOfT); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/CompressingUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/CompressingUtils.java index 1281743d7..298182a63 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/CompressingUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/CompressingUtils.java @@ -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))); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpMultipartRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpMultipartRequest.java index 19f24c0a6..5d0658604 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpMultipartRequest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpMultipartRequest.java @@ -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); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java index 913f56531..5414a7c40 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java @@ -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); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java index 1efef6f8b..3b926719a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java @@ -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]); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java index 2f52c90bd..f16ba9ded 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java @@ -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 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); } } } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/CrashReportAnalyzerTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/CrashReportAnalyzerTest.java index db8ae1aa9..2c7c05d78 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/CrashReportAnalyzerTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/CrashReportAnalyzerTest.java @@ -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 results, CrashReportAnalyzer.Rule rule) {