mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-04-18 18:40:34 +08:00
代码清理与修复 UI 卡顿资源占用过高的问题 (#1849)
* Lazy initialization of Swing * Load ISRG Root X1 certificate only on Java 8 * Replace JOptionPane with JavaFX Alert * Avoid using java.awt.Desktop * Rewrite TexturesLoader * Optimization SelfDependencyPatcher * fix typo * close #968: Use computeIfAbsent to ensure thread safety * Optimization GameVersion::minecraftVersion * code cleanup * Set the initial capacity of readFullyWithoutClosing * code cleanup * Mark inner classes as static if possible * Cache version icon * Code cleanup * Fix ListView scrolling performance issues * DatapackListPage::items * Replace OutputStream with FileChannel::write
This commit is contained in:
parent
035469240d
commit
0f26ae5cfc
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@ hs_err_pid*
|
||||
.mine*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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");
|
||||
|
@ -17,9 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
@ -31,18 +35,15 @@ import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -64,15 +65,15 @@ public final class TexturesLoader {
|
||||
|
||||
// ==== Texture Loading ====
|
||||
public static class LoadedTexture {
|
||||
private final BufferedImage image;
|
||||
private final Image image;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
|
||||
public LoadedTexture(Image image, Map<String, String> metadata) {
|
||||
this.image = requireNonNull(image);
|
||||
this.metadata = requireNonNull(metadata);
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
public Image getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
@ -96,7 +97,7 @@ public final class TexturesLoader {
|
||||
return TEXTURES_DIR.resolve(prefix).resolve(hash);
|
||||
}
|
||||
|
||||
public static LoadedTexture loadTexture(Texture texture) throws IOException {
|
||||
public static LoadedTexture loadTexture(Texture texture) throws Throwable {
|
||||
if (StringUtils.isBlank(texture.getUrl())) {
|
||||
throw new IOException("Texture url is empty");
|
||||
}
|
||||
@ -117,12 +118,13 @@ public final class TexturesLoader {
|
||||
}
|
||||
}
|
||||
|
||||
BufferedImage img;
|
||||
Image img;
|
||||
try (InputStream in = Files.newInputStream(file)) {
|
||||
img = ImageIO.read(in);
|
||||
img = new Image(in);
|
||||
}
|
||||
if (img == null)
|
||||
throw new IOException("Texture is malformed");
|
||||
|
||||
if (img.isError())
|
||||
throw img.getException();
|
||||
|
||||
Map<String, String> metadata = texture.getMetadata();
|
||||
if (metadata == null) {
|
||||
@ -141,11 +143,16 @@ public final class TexturesLoader {
|
||||
}
|
||||
|
||||
private static void loadDefaultSkin(String path, TextureModel model) {
|
||||
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
|
||||
Image skin;
|
||||
try {
|
||||
skin = new Image(path);
|
||||
if (skin.isError())
|
||||
throw skin.getException();
|
||||
} catch (Throwable e) {
|
||||
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
|
||||
throw new ResourceNotFoundError("Cannot load default skin from " + path, e);
|
||||
}
|
||||
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(skin, singletonMap("model", model.modelName)));
|
||||
}
|
||||
|
||||
public static LoadedTexture getDefaultSkin(TextureModel model) {
|
||||
@ -172,7 +179,7 @@ public final class TexturesLoader {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
@ -195,7 +202,7 @@ public final class TexturesLoader {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
@ -209,38 +216,109 @@ public final class TexturesLoader {
|
||||
// ====
|
||||
|
||||
// ==== Avatar ====
|
||||
public static BufferedImage toAvatar(BufferedImage skin, int size) {
|
||||
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = avatar.createGraphics();
|
||||
public static void drawAvatar(Canvas canvas, Image skin) {
|
||||
canvas.getGraphicsContext2D().clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
|
||||
int scale = skin.getWidth() / 64;
|
||||
int size = (int) canvas.getWidth();
|
||||
int scale = (int) skin.getWidth() / 64;
|
||||
int faceOffset = (int) Math.round(size / 18.0);
|
||||
|
||||
GraphicsContext g = canvas.getGraphicsContext2D();
|
||||
try {
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatarFX(g, skin, size, scale, faceOffset);
|
||||
} catch (NoSuchMethodError ignored) {
|
||||
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing, fallback to Java 2D
|
||||
drawAvatarJ2D(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawAvatarFX(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
g.drawImage(skin,
|
||||
8 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
|
||||
g.drawImage(skin,
|
||||
40 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
private static void drawAvatarJ2D(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
BufferedImage bi = FXUtils.fromFXImage(skin);
|
||||
|
||||
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = avatar.createGraphics();
|
||||
|
||||
g2d.drawImage(bi,
|
||||
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||
8 * scale, 8 * scale, 16 * scale, 16 * scale,
|
||||
null);
|
||||
g.drawImage(skin,
|
||||
g2d.drawImage(bi,
|
||||
0, 0, size, size,
|
||||
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
||||
|
||||
g.dispose();
|
||||
return avatar;
|
||||
g2d.dispose();
|
||||
|
||||
PixelWriter pw = g.getPixelWriter();
|
||||
|
||||
for (int x = 0; x < size; x++) {
|
||||
for (int y = 0; y < size; y++) {
|
||||
pw.setArgb(x, y, avatar.getRGB(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
|
||||
return BindingMapping.of(skinBinding(service, uuid))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(FXUtils::toFXImage);
|
||||
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
|
||||
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
|
||||
|
||||
final WeakReference<Canvas> canvasRef;
|
||||
final ObjectBinding<LoadedTexture> binding;
|
||||
|
||||
SkinBindingChangeListener(Canvas canvas, ObjectBinding<LoadedTexture> binding) {
|
||||
this.canvasRef = new WeakReference<>(canvas);
|
||||
this.binding = binding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends LoadedTexture> observable,
|
||||
LoadedTexture oldValue, LoadedTexture loadedTexture) {
|
||||
Canvas canvas = canvasRef.get();
|
||||
if (canvas != null)
|
||||
drawAvatar(canvas, loadedTexture.image);
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
|
||||
return BindingMapping.of(skinBinding(account))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(FXUtils::toFXImage);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
|
||||
public static void fxAvatarBinding(Canvas canvas, ObjectBinding<LoadedTexture> skinBinding) {
|
||||
synchronized (SkinBindingChangeListener.hole) {
|
||||
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||
if (oldListener != null)
|
||||
oldListener.binding.removeListener(oldListener);
|
||||
|
||||
SkinBindingChangeListener listener = new SkinBindingChangeListener(canvas, skinBinding);
|
||||
listener.changed(skinBinding, null, skinBinding.get());
|
||||
skinBinding.addListener(listener);
|
||||
|
||||
SkinBindingChangeListener.hole.put(canvas, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindAvatar(Canvas canvas, YggdrasilService service, UUID uuid) {
|
||||
fxAvatarBinding(canvas, skinBinding(service, uuid));
|
||||
}
|
||||
|
||||
public static void bindAvatar(Canvas canvas, Account account) {
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount)
|
||||
fxAvatarBinding(canvas, skinBinding(account));
|
||||
else {
|
||||
unbindAvatar(canvas);
|
||||
drawAvatar(canvas, getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unbindAvatar(Canvas canvas) {
|
||||
synchronized (SkinBindingChangeListener.hole) {
|
||||
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||
if (oldListener != null)
|
||||
oldListener.binding.removeListener(oldListener);
|
||||
}
|
||||
}
|
||||
// ====
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -34,10 +34,8 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
@ -49,6 +47,7 @@ import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
@ -58,6 +57,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
@ -357,53 +357,77 @@ public final class FXUtils {
|
||||
|
||||
public static void openFolder(File file) {
|
||||
if (!FileUtils.makeDirectory(file)) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
|
||||
LOG.log(Level.SEVERE, "Unable to make directory " + file);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = file.getAbsolutePath();
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case OSX:
|
||||
String openCommand;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommand = "explorer.exe";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
openCommand = "/usr/bin/open";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && new File("/usr/bin/xdg-open").exists())
|
||||
openCommand = "/usr/bin/xdg-open";
|
||||
else
|
||||
openCommand = null;
|
||||
|
||||
thread(() -> {
|
||||
if (openCommand != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e);
|
||||
int exitCode = SystemUtils.callExternalProcess(openCommand, path);
|
||||
|
||||
// explorer.exe always return 1
|
||||
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
|
||||
return;
|
||||
else
|
||||
LOG.warning("Open " + path + " failed with code " + exitCode);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Unable to open " + path + " by executing " + openCommand, e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
thread(() -> {
|
||||
if (java.awt.Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to java.awt.Desktop::open
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void showFileInExplorer(Path file) {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
String path = file.toAbsolutePath().toString();
|
||||
|
||||
String[] openCommands;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommands = new String[]{"explorer.exe", "/select,", path};
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
openCommands = new String[]{"/usr/bin/open", "-R", path};
|
||||
else
|
||||
openCommands = null;
|
||||
|
||||
if (openCommands != null) {
|
||||
thread(() -> {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select," + file, e);
|
||||
int exitCode = SystemUtils.callExternalProcess(openCommands);
|
||||
|
||||
// explorer.exe always return 1
|
||||
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
|
||||
return;
|
||||
else
|
||||
LOG.warning("Show " + path + " in explorer failed with code " + exitCode);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Unable to show " + path + " in explorer", e);
|
||||
}
|
||||
break;
|
||||
case OSX:
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", "-R", file.toAbsolutePath().toString()});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing /usr/bin/open -R " + file, e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// We do not have an universal method to show file in file manager.
|
||||
|
||||
// Fallback to open folder
|
||||
openFolder(file.getParent().toFile());
|
||||
break;
|
||||
});
|
||||
} else {
|
||||
// We do not have a universal method to show file in file manager.
|
||||
openFolder(file.getParent().toFile());
|
||||
}
|
||||
}
|
||||
|
||||
@ -426,34 +450,38 @@ public final class FXUtils {
|
||||
if (link == null)
|
||||
return;
|
||||
|
||||
if (java.awt.Desktop.isDesktopSupported()) {
|
||||
thread(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
||||
if (is.read() != -1) {
|
||||
Runtime.getRuntime().exec(new String[]{browser, link});
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "No known browser found");
|
||||
}
|
||||
}
|
||||
thread(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
|
||||
LOG.log(Level.WARNING, "An exception occurred while calling rundll32", e);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
} if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
||||
if (is.read() != -1) {
|
||||
Runtime.getRuntime().exec(new String[]{browser, link});
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "No known browser found");
|
||||
}
|
||||
}
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void showWebDialog(String title, String content) {
|
||||
@ -469,12 +497,12 @@ public final class FXUtils {
|
||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
||||
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
|
||||
|
||||
SwingUtils.initLookAndFeel();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
final JFrame frame = new JFrame(title);
|
||||
frame.setSize(width, height);
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
frame.setLocationByPlatform(true);
|
||||
//noinspection ConstantConditions
|
||||
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
|
||||
frame.setLayout(new BorderLayout());
|
||||
|
||||
@ -632,10 +660,17 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
return view -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
|
||||
if (!empty) {
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(graphicBuilder.apply(item));
|
||||
@ -688,11 +723,12 @@ public final class FXUtils {
|
||||
// Based on https://stackoverflow.com/a/57552025
|
||||
// Fix #874: Use it instead of SwingFXUtils.toFXImage
|
||||
public static WritableImage toFXImage(BufferedImage image) {
|
||||
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
|
||||
PixelWriter pw = wr.getPixelWriter();
|
||||
|
||||
final int iw = image.getWidth();
|
||||
final int ih = image.getHeight();
|
||||
|
||||
WritableImage wr = new WritableImage(iw, ih);
|
||||
PixelWriter pw = wr.getPixelWriter();
|
||||
|
||||
for (int x = 0; x < iw; x++) {
|
||||
for (int y = 0; y < ih; y++) {
|
||||
pw.setArgb(x, y, image.getRGB(x, y));
|
||||
@ -701,6 +737,21 @@ public final class FXUtils {
|
||||
return wr;
|
||||
}
|
||||
|
||||
public static BufferedImage fromFXImage(Image image) {
|
||||
final int iw = (int) image.getWidth();
|
||||
final int ih = (int) image.getHeight();
|
||||
|
||||
PixelReader pr = image.getPixelReader();
|
||||
BufferedImage bufferedImage = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int x = 0; x < iw; x++) {
|
||||
for (int y = 0; y < ih; y++) {
|
||||
bufferedImage.setRGB(x, y, pr.getArgb(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
public static void copyText(String text) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(text);
|
||||
|
@ -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);
|
||||
|
@ -34,23 +34,19 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
@ -155,12 +151,12 @@ public final class LogWindow extends Stage {
|
||||
|
||||
public class LogWindowImpl extends Control {
|
||||
|
||||
private ListView<Log> listView = new JFXListView<>();
|
||||
private BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
||||
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||
private JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||
private BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||
private final ListView<Log> listView = new JFXListView<>();
|
||||
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
||||
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
@ -207,13 +203,13 @@ public final class LogWindow extends Stage {
|
||||
return;
|
||||
}
|
||||
|
||||
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
});
|
||||
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
});
|
||||
}
|
||||
|
||||
@ -229,13 +225,13 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
||||
private static PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||
private static PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||
private static PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
||||
private static PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||
private static final PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
||||
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
|
||||
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
|
||||
ToggleButton button = new ToggleButton();
|
||||
@ -292,6 +288,7 @@ public final class LogWindow extends Stage {
|
||||
listView.scrollTo(listView.getItems().size() - 1);
|
||||
});
|
||||
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||
{
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
@ -308,6 +305,12 @@ public final class LogWindow extends Stage {
|
||||
@Override
|
||||
protected void updateItem(Log item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
|
||||
pseudoClassStateChanged(EMPTY, empty);
|
||||
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
|
||||
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
|
||||
|
33
HMCL/src/main/java/org/jackhuang/hmcl/ui/SwingUtils.java
Normal file
33
HMCL/src/main/java/org/jackhuang/hmcl/ui/SwingUtils.java
Normal file
@ -0,0 +1,33 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public final class SwingUtils {
|
||||
static {
|
||||
if (System.getProperty("swing.defaultlaf") == null) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SwingUtils() {
|
||||
}
|
||||
|
||||
public static void initLookAndFeel() {
|
||||
// Make sure the static constructor is called
|
||||
}
|
||||
|
||||
public static void showInfoDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Info", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showWarningDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
@ -22,9 +22,8 @@ import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
@ -34,20 +33,18 @@ import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createStringBinding;
|
||||
import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory;
|
||||
import static org.jackhuang.hmcl.setting.Accounts.getLocalizedLoginTypeName;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.toFXImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
private final Tooltip tooltip;
|
||||
private final ImageView imageView;
|
||||
private final Canvas canvas;
|
||||
|
||||
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
|
||||
private final ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
@ -55,17 +52,19 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
if (account == null) {
|
||||
titleProperty().unbind();
|
||||
subtitleProperty().unbind();
|
||||
imageView.imageProperty().unbind();
|
||||
tooltip.textProperty().unbind();
|
||||
setTitle(i18n("account.missing"));
|
||||
setSubtitle(i18n("account.missing.add"));
|
||||
imageView.setImage(toFXImage(TexturesLoader.toAvatar(TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage(), 32)));
|
||||
tooltip.setText(i18n("account.create"));
|
||||
|
||||
TexturesLoader.unbindAvatar(canvas);
|
||||
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage());
|
||||
|
||||
} else {
|
||||
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
|
||||
subtitleProperty().bind(accountSubtitle(account));
|
||||
imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
tooltip.textProperty().bind(accountTooltip(account));
|
||||
TexturesLoader.bindAvatar(canvas, account);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -74,9 +73,8 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
tooltip = new Tooltip();
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
|
||||
Pair<Node, ImageView> view = createImageView(null);
|
||||
setLeftGraphic(view.getKey());
|
||||
imageView = view.getValue();
|
||||
canvas = new Canvas(32, 32);
|
||||
setLeftGraphic(canvas);
|
||||
|
||||
setActionButtonVisible(false);
|
||||
|
||||
|
@ -20,14 +20,11 @@ package org.jackhuang.hmcl.ui.account;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
@ -39,7 +36,6 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@ -70,7 +66,6 @@ public class AccountListItem extends RadioButton {
|
||||
private final Account account;
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final StringProperty subtitle = new SimpleStringProperty();
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
|
||||
|
||||
public AccountListItem(Account account) {
|
||||
this.account = account;
|
||||
@ -95,8 +90,6 @@ public class AccountListItem extends RadioButton {
|
||||
account.getUsername().isEmpty() ? characterName :
|
||||
Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
}
|
||||
|
||||
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -226,16 +219,4 @@ public class AccountListItem extends RadioButton {
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
return image.get();
|
||||
}
|
||||
|
||||
public void setImage(Image image) {
|
||||
this.image.set(image);
|
||||
}
|
||||
|
||||
public ObjectProperty<Image> imageProperty() {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
@ -21,15 +21,16 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@ -64,9 +65,8 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
imageView.imageProperty().bind(skinnable.imageProperty());
|
||||
Canvas canvas = new Canvas(32, 32);
|
||||
TexturesLoader.bindAvatar(canvas, skinnable.getAccount());
|
||||
|
||||
Label title = new Label();
|
||||
title.getStyleClass().add("title");
|
||||
@ -84,7 +84,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
item.getStyleClass().add("two-line-list-item");
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
|
||||
center.getChildren().setAll(imageView, item);
|
||||
center.getChildren().setAll(canvas, item);
|
||||
root.setCenter(center);
|
||||
|
||||
HBox right = new HBox();
|
||||
|
@ -30,10 +30,10 @@ import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
@ -652,12 +652,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
|
||||
Platform.runLater(() -> {
|
||||
for (GameProfile profile : profiles) {
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
Canvas portraitCanvas = new Canvas(32, 32);
|
||||
TexturesLoader.bindAvatar(portraitCanvas, service, profile.getId());
|
||||
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
IconedItem accountItem = new IconedItem(portraitCanvas, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
|
@ -22,6 +22,7 @@ import javafx.css.PseudoClass;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public abstract class MDListCell<T> extends ListCell<T> {
|
||||
@ -29,8 +30,11 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
private final MutableObject<Object> lastCell;
|
||||
|
||||
public MDListCell(JFXListView<T> listView, MutableObject<Object> lastCell) {
|
||||
this.lastCell = lastCell;
|
||||
|
||||
public MDListCell(JFXListView<T> listView) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
|
||||
@ -50,6 +54,14 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (lastCell != null) {
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
}
|
||||
|
||||
updateControl(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
|
@ -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
|
||||
|
@ -242,7 +242,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
tab.select(worldTab);
|
||||
}
|
||||
|
||||
private class DownloadNavigator implements Navigation {
|
||||
private static final class DownloadNavigator implements Navigation {
|
||||
private final Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
@Override
|
||||
|
@ -31,6 +31,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
@ -40,7 +41,10 @@ import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
@ -52,9 +56,9 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.HMCLService;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@ -122,86 +126,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
|
||||
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
|
||||
|
||||
list.setCellFactory(listView -> new ListCell<RemoteVersion>() {
|
||||
IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
StackPane pane = new StackPane();
|
||||
|
||||
{
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
super.updateItem(remoteVersion, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
return;
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
content.setTitle(remoteVersion.getSelfVersion());
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
|
||||
} else {
|
||||
content.setSubtitle("");
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
|
||||
break;
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
|
||||
break;
|
||||
}
|
||||
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof ForgeRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof FabricRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof FabricAPIRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
MutableObject<RemoteVersionListCell> lastCell = new MutableObject<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell));
|
||||
|
||||
list.setOnMouseClicked(e -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
@ -290,4 +216,89 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
private void onSponsor() {
|
||||
HMCLService.openRedirectLink("bmclapi_sponsor");
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
private static final EnumMap<VersionIconType, Image> icon = new EnumMap<>(VersionIconType.class);
|
||||
|
||||
private static Image getIcon(VersionIconType type) {
|
||||
assert Platform.isFxApplicationThread();
|
||||
return icon.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl(), 32, 32, false, true));
|
||||
}
|
||||
|
||||
final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
final StackPane pane = new StackPane();
|
||||
|
||||
private final MutableObject<RemoteVersionListCell> lastCell;
|
||||
|
||||
RemoteVersionListCell(MutableObject<RemoteVersionListCell> lastCell) {
|
||||
this.lastCell = lastCell;
|
||||
}
|
||||
|
||||
{
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
super.updateItem(remoteVersion, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
return;
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
content.setTitle(remoteVersion.getSelfVersion());
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
|
||||
} else {
|
||||
content.setSubtitle(null);
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(getIcon(VersionIconType.GRASS));
|
||||
break;
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(getIcon(VersionIconType.COMMAND));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(getIcon(VersionIconType.CRAFT_TABLE));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
VersionIconType iconType;
|
||||
if (remoteVersion instanceof LiteLoaderRemoteVersion)
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
else if (remoteVersion instanceof OptiFineRemoteVersion)
|
||||
iconType = VersionIconType.COMMAND;
|
||||
else if (remoteVersion instanceof ForgeRemoteVersion)
|
||||
iconType = VersionIconType.FORGE;
|
||||
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.FABRIC;
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
iconType = VersionIconType.QUILT;
|
||||
else
|
||||
iconType = null;
|
||||
|
||||
content.setImage(iconType != null ? getIcon(iconType) : null);
|
||||
if (content.getSubtitle() == null)
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
else
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
@ -117,7 +118,8 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
JFXListView<FeedbackResponse> listView = new JFXListView<>();
|
||||
spinnerPane.setContent(listView);
|
||||
Bindings.bindContent(listView.getItems(), feedbacks);
|
||||
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView) {
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView, lastCell) {
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final JFXButton likeButton = new JFXButton();
|
||||
private final JFXButton unlikeButton = new JFXButton();
|
||||
@ -230,7 +232,7 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
Controllers.dialog(new AddFeedbackDialog());
|
||||
}
|
||||
|
||||
private class LoginDialog extends JFXDialogLayout {
|
||||
private static final class LoginDialog extends JFXDialogLayout {
|
||||
private final SpinnerPane spinnerPane = new SpinnerPane();
|
||||
private final Label errorLabel = new Label();
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
@ -241,10 +243,7 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
VBox vbox = new VBox(8);
|
||||
setBody(vbox);
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
||||
logging
|
||||
? i18n("account.hmcl.hint")
|
||||
: i18n("account.hmcl.hint")));
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> i18n("account.hmcl.hint")));
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@ -84,10 +85,17 @@ public class SponsorPage extends StackPane {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("card");
|
||||
listView = new JFXListView<>();
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
listView.setCellFactory((listView) -> new JFXListCell<Sponsor>() {
|
||||
@Override
|
||||
public void updateItem(Sponsor item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
|
||||
if (!empty) {
|
||||
setText(item.getName());
|
||||
setGraphic(null);
|
||||
|
@ -47,6 +47,8 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
|
||||
private final Path worldDir;
|
||||
private final Datapack datapack;
|
||||
|
||||
// Strongly referencing items, preventing GC collection
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
|
||||
|
||||
public DatapackListPage(String worldName, Path worldDir) {
|
||||
|
@ -31,6 +31,7 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@ -84,12 +85,9 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
toolbarNormal.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
|
||||
createToolbarButton2(i18n("mods.add"), SVG::plus, skinnable::add),
|
||||
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, () ->
|
||||
skinnable.openModFolder()),
|
||||
createToolbarButton2(i18n("mods.check_updates"), SVG::update, () ->
|
||||
skinnable.checkUpdates()),
|
||||
createToolbarButton2(i18n("download"), SVG::downloadOutline, () ->
|
||||
skinnable.download()));
|
||||
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, skinnable::openModFolder),
|
||||
createToolbarButton2(i18n("mods.check_updates"), SVG::update, skinnable::checkUpdates),
|
||||
createToolbarButton2(i18n("download"), SVG::downloadOutline, skinnable::download));
|
||||
HBox toolbarSelecting = new HBox();
|
||||
toolbarSelecting.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG::delete, () -> {
|
||||
@ -121,7 +119,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView));
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView, lastCell));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
|
||||
@ -276,10 +275,10 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
private static Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
|
||||
private static Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
||||
private static final Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
|
||||
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
||||
|
||||
class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
final class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
JFXButton restoreButton = new JFXButton();
|
||||
@ -287,8 +286,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
JFXButton revealButton = new JFXButton();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView) {
|
||||
super(listView);
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView, MutableObject<Object> lastCell) {
|
||||
super(listView, lastCell);
|
||||
|
||||
HBox container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
|
@ -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) {
|
||||
|
@ -27,6 +27,7 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
@ -158,8 +159,8 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
public static class ModUpdateCell extends MDListCell<LocalModFile.ModUpdate> {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
|
||||
public ModUpdateCell(JFXListView<LocalModFile.ModUpdate> listView) {
|
||||
super(listView);
|
||||
public ModUpdateCell(JFXListView<LocalModFile.ModUpdate> listView, MutableObject<Object> lastCell) {
|
||||
super(listView, lastCell);
|
||||
|
||||
getContainer().getChildren().setAll(content);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -43,6 +43,7 @@ package org.jackhuang.hmcl.util;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
@ -55,6 +56,7 @@ import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@ -72,6 +74,8 @@ public final class SelfDependencyPatcher {
|
||||
private final List<DependencyDescriptor> dependencies = DependencyDescriptor.readDependencies();
|
||||
private final List<Repository> repositories;
|
||||
private final Repository defaultRepository;
|
||||
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
private final MessageDigest digest = DigestUtils.getDigest("SHA-1");
|
||||
|
||||
private SelfDependencyPatcher() throws IncompatibleVersionException {
|
||||
// We can only self-patch JavaFX on specific platform.
|
||||
@ -261,9 +265,10 @@ public final class SelfDependencyPatcher {
|
||||
* @throws IOException When the files cannot be fetched or saved.
|
||||
*/
|
||||
private void fetchDependencies(List<DependencyDescriptor> dependencies) throws IOException {
|
||||
SwingUtils.initLookAndFeel();
|
||||
|
||||
boolean isFirstTime = true;
|
||||
|
||||
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
Repository repository = defaultRepository;
|
||||
|
||||
int count = 0;
|
||||
@ -360,8 +365,18 @@ public final class SelfDependencyPatcher {
|
||||
return missing;
|
||||
}
|
||||
|
||||
private static void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
|
||||
ChecksumMismatchException.verifyChecksum(dependency.localPath(), "SHA-1", dependency.sha1());
|
||||
private void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
|
||||
digest.reset();
|
||||
try (InputStream is = Files.newInputStream(dependency.localPath())) {
|
||||
int read;
|
||||
while ((read = is.read(buffer, 0, IOUtils.DEFAULT_BUFFER_SIZE)) > -1) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
String sha1 = Hex.encodeHex(digest.digest());
|
||||
if (!dependency.sha1().equalsIgnoreCase(sha1))
|
||||
throw new ChecksumMismatchException("SHA-1", dependency.sha1(), sha1);
|
||||
}
|
||||
|
||||
public static class PatchException extends Exception {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -25,7 +25,6 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.io.HttpServer;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.*;
|
||||
@ -80,8 +79,7 @@ public class YggdrasilServer extends HttpServer {
|
||||
}
|
||||
|
||||
private Response profiles(Request request) throws IOException {
|
||||
String body = IOUtils.readFullyAsString(request.getSession().getInputStream(), UTF_8);
|
||||
List<String> names = JsonUtils.fromNonNullJson(body, new TypeToken<List<String>>() {
|
||||
List<String> names = JsonUtils.fromNonNullJsonFully(request.getSession().getInputStream(), new TypeToken<List<String>>() {
|
||||
}.getType());
|
||||
return ok(names.stream().distinct()
|
||||
.map(this::findCharacterByName)
|
||||
|
@ -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;
|
||||
|
@ -68,8 +68,7 @@ public class ForgeOldInstallTask extends Task<Version> {
|
||||
InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json"));
|
||||
if (stream == null)
|
||||
throw new ArtifactMalformedException("Malformed forge installer file, install_profile.json does not exist.");
|
||||
String json = IOUtils.readFullyAsString(stream);
|
||||
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class);
|
||||
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJsonFully(stream, ForgeInstallProfile.class);
|
||||
|
||||
// unpack the universal jar in the installer file.
|
||||
Library forgeLibrary = new Library(installProfile.getInstall().getPath());
|
||||
|
@ -67,7 +67,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
|
||||
private File baseDirectory;
|
||||
protected Map<String, Version> versions;
|
||||
private ConcurrentHashMap<File, Optional<String>> gameVersions = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<File, Optional<String>> gameVersions = new ConcurrentHashMap<>();
|
||||
|
||||
public DefaultGameRepository(File baseDirectory) {
|
||||
this.baseDirectory = baseDirectory;
|
||||
@ -145,19 +145,13 @@ public class DefaultGameRepository implements GameRepository {
|
||||
// This implementation may cause multiple flows against the same version entering
|
||||
// this function, which is accepted because GameVersion::minecraftVersion should
|
||||
// be consistent.
|
||||
File versionJar = getVersionJar(version);
|
||||
if (gameVersions.containsKey(versionJar)) {
|
||||
return gameVersions.get(versionJar);
|
||||
} else {
|
||||
return gameVersions.computeIfAbsent(getVersionJar(version), versionJar -> {
|
||||
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
||||
|
||||
if (!gameVersion.isPresent()) {
|
||||
LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists());
|
||||
}
|
||||
|
||||
gameVersions.put(versionJar, gameVersion);
|
||||
return gameVersion;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -19,8 +19,6 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPool;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantType;
|
||||
@ -28,15 +26,15 @@ import org.jenkinsci.constant_pool_scanner.StringConstant;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@ -48,9 +46,9 @@ public final class GameVersion {
|
||||
private GameVersion() {
|
||||
}
|
||||
|
||||
private static Optional<String> getVersionFromJson(Path versionJson) {
|
||||
private static Optional<String> getVersionFromJson(InputStream versionJson) {
|
||||
try {
|
||||
Map<?, ?> version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), Map.class);
|
||||
Map<?, ?> version = JsonUtils.fromNonNullJsonFully(versionJson, Map.class);
|
||||
return tryCast(version.get("name"), String.class);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse version.json", e);
|
||||
@ -58,7 +56,7 @@ public final class GameVersion {
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException {
|
||||
private static Optional<String> getVersionOfClassMinecraft(InputStream bytecode) throws IOException {
|
||||
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||
|
||||
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
||||
@ -68,7 +66,7 @@ public final class GameVersion {
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private static Optional<String> getVersionFromClassMinecraftServer(byte[] bytecode) throws IOException {
|
||||
private static Optional<String> getVersionFromClassMinecraftServer(InputStream bytecode) throws IOException {
|
||||
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||
|
||||
List<String> list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
||||
@ -94,23 +92,29 @@ public final class GameVersion {
|
||||
if (file == null || !file.exists() || !file.isFile() || !file.canRead())
|
||||
return Optional.empty();
|
||||
|
||||
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
|
||||
Path versionJson = gameJar.getPath("version.json");
|
||||
if (Files.exists(versionJson)) {
|
||||
Optional<String> result = getVersionFromJson(versionJson);
|
||||
try (ZipFile gameJar = new ZipFile(file)) {
|
||||
ZipEntry versionJson = gameJar.getEntry("version.json");
|
||||
if (versionJson != null) {
|
||||
Optional<String> result = getVersionFromJson(gameJar.getInputStream(versionJson));
|
||||
if (result.isPresent())
|
||||
return result;
|
||||
}
|
||||
|
||||
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
||||
if (Files.exists(minecraft)) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
||||
if (result.isPresent())
|
||||
return result;
|
||||
ZipEntry minecraft = gameJar.getEntry("net/minecraft/client/Minecraft.class");
|
||||
if (minecraft != null) {
|
||||
try (InputStream is = gameJar.getInputStream(minecraft)) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(is);
|
||||
if (result.isPresent())
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
ZipEntry minecraftServer = gameJar.getEntry("net/minecraft/server/MinecraftServer.class");
|
||||
if (minecraftServer != null) {
|
||||
try (InputStream is = gameJar.getInputStream(minecraftServer)) {
|
||||
return getVersionFromClassMinecraftServer(is);
|
||||
}
|
||||
}
|
||||
Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class");
|
||||
if (Files.exists(minecraftServer))
|
||||
return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer));
|
||||
return Optional.empty();
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
|
@ -67,7 +67,7 @@ public final class StringArgument implements Argument {
|
||||
return argument;
|
||||
}
|
||||
|
||||
public class Serializer implements JsonSerializer<StringArgument> {
|
||||
public static final class Serializer implements JsonSerializer<StringArgument> {
|
||||
@Override
|
||||
public JsonElement serialize(StringArgument src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.getArgument());
|
||||
|
@ -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(),
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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.");
|
||||
|
||||
|
@ -239,7 +239,7 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
CACHED
|
||||
}
|
||||
|
||||
protected class DownloadState {
|
||||
protected static final class DownloadState {
|
||||
private final int startPosition;
|
||||
private final int endPosition;
|
||||
private final int currentPosition;
|
||||
@ -272,9 +272,7 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
}
|
||||
}
|
||||
|
||||
protected class DownloadMission {
|
||||
|
||||
|
||||
protected static final class DownloadMission {
|
||||
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -27,9 +27,9 @@ import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
@ -54,7 +54,7 @@ public class CacheRepository {
|
||||
private Path cacheDirectory;
|
||||
private Path indexFile;
|
||||
private Map<String, ETagItem> index;
|
||||
private Map<String, Storage> storages = new HashMap<>();
|
||||
private final Map<String, Storage> storages = new HashMap<>();
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
public void changeDirectory(Path commonDir) {
|
||||
@ -293,9 +293,8 @@ public class CacheRepository {
|
||||
ETagIndex indexOnDisk = JsonUtils.fromMaybeMalformedJson(new String(IOUtils.readFullyWithoutClosing(Channels.newInputStream(channel)), UTF_8), ETagIndex.class);
|
||||
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
|
||||
channel.truncate(0);
|
||||
OutputStream os = Channels.newOutputStream(channel);
|
||||
ETagIndex writeTo = new ETagIndex(newIndex.values());
|
||||
IOUtils.write(JsonUtils.GSON.toJson(writeTo).getBytes(UTF_8), os);
|
||||
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(writeTo).getBytes(UTF_8)));
|
||||
this.index = newIndex;
|
||||
} finally {
|
||||
lock.release();
|
||||
@ -303,7 +302,7 @@ public class CacheRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private class ETagIndex {
|
||||
private static final class ETagIndex {
|
||||
private final Collection<ETagItem> eTag;
|
||||
|
||||
public ETagIndex() {
|
||||
@ -315,7 +314,7 @@ public class CacheRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private class ETagItem {
|
||||
private static final class ETagItem {
|
||||
private final String url;
|
||||
private final String eTag;
|
||||
private final String hash;
|
||||
@ -429,8 +428,7 @@ public class CacheRepository {
|
||||
if (indexOnDisk == null) indexOnDisk = new HashMap<>();
|
||||
indexOnDisk.putAll(storage);
|
||||
channel.truncate(0);
|
||||
OutputStream os = Channels.newOutputStream(channel);
|
||||
IOUtils.write(JsonUtils.GSON.toJson(storage).getBytes(UTF_8), os);
|
||||
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(storage).getBytes(UTF_8)));
|
||||
this.storage = indexOnDisk;
|
||||
} finally {
|
||||
lock.release();
|
||||
|
@ -23,7 +23,11 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
@ -44,6 +48,18 @@ public final class JsonUtils {
|
||||
private JsonUtils() {
|
||||
}
|
||||
|
||||
public static <T> T fromJsonFully(InputStream json, Class<T> classOfT) throws IOException, JsonParseException {
|
||||
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
|
||||
return GSON.fromJson(reader, classOfT);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T fromJsonFully(InputStream json, Type type) throws IOException, JsonParseException {
|
||||
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
|
||||
return GSON.fromJson(reader, type);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T fromNonNullJson(String json, Class<T> classOfT) throws JsonParseException {
|
||||
T parsed = GSON.fromJson(json, classOfT);
|
||||
if (parsed == null)
|
||||
@ -58,6 +74,24 @@ public final class JsonUtils {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public static <T> T fromNonNullJsonFully(InputStream json, Class<T> classOfT) throws IOException, JsonParseException {
|
||||
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
|
||||
T parsed = GSON.fromJson(reader, classOfT);
|
||||
if (parsed == null)
|
||||
throw new JsonParseException("Json object cannot be null.");
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T fromNonNullJsonFully(InputStream json, Type type) throws IOException, JsonParseException {
|
||||
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
|
||||
T parsed = GSON.fromJson(reader, type);
|
||||
if (parsed == null)
|
||||
throw new JsonParseException("Json object cannot be null.");
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T fromMaybeMalformedJson(String json, Class<T> classOfT) throws JsonParseException {
|
||||
try {
|
||||
return GSON.fromJson(json, classOfT);
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
@ -176,7 +175,7 @@ public final class NetworkUtils {
|
||||
public static String doGet(URL url) throws IOException {
|
||||
HttpURLConnection con = createHttpConnection(url);
|
||||
con = resolveConnection(con);
|
||||
return IOUtils.readFullyAsString(con.getInputStream(), StandardCharsets.UTF_8);
|
||||
return IOUtils.readFullyAsString(con.getInputStream());
|
||||
}
|
||||
|
||||
public static String doPost(URL u, Map<String, String> params) throws IOException {
|
||||
@ -210,13 +209,13 @@ public final class NetworkUtils {
|
||||
public static String readData(HttpURLConnection con) throws IOException {
|
||||
try {
|
||||
try (InputStream stdout = con.getInputStream()) {
|
||||
return IOUtils.readFullyAsString(stdout, UTF_8);
|
||||
return IOUtils.readFullyAsString(stdout);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
try (InputStream stderr = con.getErrorStream()) {
|
||||
if (stderr == null)
|
||||
throw e;
|
||||
return IOUtils.readFullyAsString(stderr, UTF_8);
|
||||
return IOUtils.readFullyAsString(stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class CrashReportAnalyzerTest {
|
||||
@ -35,7 +34,7 @@ public class CrashReportAnalyzerTest {
|
||||
if (is == null) {
|
||||
throw new IllegalStateException("Resource not found: " + path);
|
||||
}
|
||||
return IOUtils.readFullyAsString(is, StandardCharsets.UTF_8);
|
||||
return IOUtils.readFullyAsString(is);
|
||||
}
|
||||
|
||||
private CrashReportAnalyzer.Result findResultByRule(List<CrashReportAnalyzer.Result> results, CrashReportAnalyzer.Rule rule) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user