代码清理与修复 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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ hs_err_pid*
|
|||||||
.mine*
|
.mine*
|
||||||
/externalgames
|
/externalgames
|
||||||
NVIDIA
|
NVIDIA
|
||||||
|
minecraft-exported-crash-info*
|
||||||
|
|
||||||
# gradle build
|
# gradle build
|
||||||
/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.CommandBuilder;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.management.ManagementFactory;
|
import java.lang.management.ManagementFactory;
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
@@ -131,7 +130,8 @@ public final class Launcher extends Application {
|
|||||||
Platform.setImplicitExit(false);
|
Platform.setImplicitExit(false);
|
||||||
Controllers.initialize(primaryStage);
|
Controllers.initialize(primaryStage);
|
||||||
|
|
||||||
initIcon();
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||||
|
initIcon();
|
||||||
|
|
||||||
UpdateChecker.init();
|
UpdateChecker.init();
|
||||||
|
|
||||||
@@ -149,8 +149,7 @@ public final class Launcher extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initIcon() {
|
private void initIcon() {
|
||||||
Toolkit toolkit = Toolkit.getDefaultToolkit();
|
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Launcher.class.getResource("/assets/img/icon.png"));
|
||||||
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
|
|
||||||
AwtUtils.setAppleIcon(image);
|
AwtUtils.setAppleIcon(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ import javafx.application.Platform;
|
|||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
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.Architecture;
|
||||||
|
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
import javax.swing.*;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -58,13 +59,11 @@ public final class Main {
|
|||||||
// Fix title bar not displaying in GTK systems
|
// Fix title bar not displaying in GTK systems
|
||||||
System.setProperty("jdk.gtk.version", "2");
|
System.setProperty("jdk.gtk.version", "2");
|
||||||
|
|
||||||
// Use System look and feel
|
|
||||||
initLookAndFeel();
|
|
||||||
|
|
||||||
checkDirectoryPath();
|
checkDirectoryPath();
|
||||||
|
|
||||||
// This environment check will take ~300ms
|
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9)
|
||||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
// This environment check will take ~300ms
|
||||||
|
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||||
|
|
||||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||||
|
|
||||||
@@ -73,15 +72,6 @@ public final class Main {
|
|||||||
Launcher.main(args);
|
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() {
|
private static void checkDirectoryPath() {
|
||||||
String currentDirectory = new File("").getAbsolutePath();
|
String currentDirectory = new File("").getAbsolutePath();
|
||||||
if (currentDirectory.contains("!")) {
|
if (currentDirectory.contains("!")) {
|
||||||
@@ -126,7 +116,7 @@ public final class Main {
|
|||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
SwingUtils.showErrorDialog(message);
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +134,8 @@ public final class Main {
|
|||||||
}
|
}
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
|
||||||
|
SwingUtils.showWarningDialog(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void fixLetsEncrypt() {
|
static void fixLetsEncrypt() {
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.io.JarUtils;
|
|||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -104,7 +103,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
|
|
||||||
String html;
|
String html;
|
||||||
try {
|
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"));
|
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
||||||
|
|||||||
@@ -17,9 +17,13 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.ObjectBinding;
|
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.Image;
|
||||||
|
import javafx.scene.image.PixelWriter;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.auth.Account;
|
import org.jackhuang.hmcl.auth.Account;
|
||||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
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.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.EnumMap;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -64,15 +65,15 @@ public final class TexturesLoader {
|
|||||||
|
|
||||||
// ==== Texture Loading ====
|
// ==== Texture Loading ====
|
||||||
public static class LoadedTexture {
|
public static class LoadedTexture {
|
||||||
private final BufferedImage image;
|
private final Image image;
|
||||||
private final Map<String, String> metadata;
|
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.image = requireNonNull(image);
|
||||||
this.metadata = requireNonNull(metadata);
|
this.metadata = requireNonNull(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BufferedImage getImage() {
|
public Image getImage() {
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ public final class TexturesLoader {
|
|||||||
return TEXTURES_DIR.resolve(prefix).resolve(hash);
|
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())) {
|
if (StringUtils.isBlank(texture.getUrl())) {
|
||||||
throw new IOException("Texture url is empty");
|
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)) {
|
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();
|
Map<String, String> metadata = texture.getMetadata();
|
||||||
if (metadata == null) {
|
if (metadata == null) {
|
||||||
@@ -141,11 +143,16 @@ public final class TexturesLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void loadDefaultSkin(String path, TextureModel model) {
|
private static void loadDefaultSkin(String path, TextureModel model) {
|
||||||
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
|
Image skin;
|
||||||
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
|
try {
|
||||||
|
skin = new Image(path);
|
||||||
|
if (skin.isError())
|
||||||
|
throw skin.getException();
|
||||||
} catch (Throwable e) {
|
} 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) {
|
public static LoadedTexture getDefaultSkin(TextureModel model) {
|
||||||
@@ -172,7 +179,7 @@ public final class TexturesLoader {
|
|||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try {
|
try {
|
||||||
return loadTexture(texture);
|
return loadTexture(texture);
|
||||||
} catch (IOException e) {
|
} catch (Throwable e) {
|
||||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||||
return uuidFallback;
|
return uuidFallback;
|
||||||
}
|
}
|
||||||
@@ -195,7 +202,7 @@ public final class TexturesLoader {
|
|||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try {
|
try {
|
||||||
return loadTexture(texture);
|
return loadTexture(texture);
|
||||||
} catch (IOException e) {
|
} catch (Throwable e) {
|
||||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||||
return uuidFallback;
|
return uuidFallback;
|
||||||
}
|
}
|
||||||
@@ -209,38 +216,109 @@ public final class TexturesLoader {
|
|||||||
// ====
|
// ====
|
||||||
|
|
||||||
// ==== Avatar ====
|
// ==== Avatar ====
|
||||||
public static BufferedImage toAvatar(BufferedImage skin, int size) {
|
public static void drawAvatar(Canvas canvas, Image skin) {
|
||||||
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
canvas.getGraphicsContext2D().clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||||
Graphics2D g = avatar.createGraphics();
|
|
||||||
|
|
||||||
int scale = skin.getWidth() / 64;
|
int size = (int) canvas.getWidth();
|
||||||
|
int scale = (int) skin.getWidth() / 64;
|
||||||
int faceOffset = (int) Math.round(size / 18.0);
|
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,
|
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,
|
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||||
8 * scale, 8 * scale, 16 * scale, 16 * scale,
|
8 * scale, 8 * scale, 16 * scale, 16 * scale,
|
||||||
null);
|
null);
|
||||||
g.drawImage(skin,
|
g2d.drawImage(bi,
|
||||||
0, 0, size, size,
|
0, 0, size, size,
|
||||||
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
||||||
|
|
||||||
g.dispose();
|
g2d.dispose();
|
||||||
return avatar;
|
|
||||||
|
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) {
|
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
|
||||||
return BindingMapping.of(skinBinding(service, uuid))
|
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
|
||||||
.map(it -> toAvatar(it.image, size))
|
|
||||||
.map(FXUtils::toFXImage);
|
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) {
|
public static void fxAvatarBinding(Canvas canvas, ObjectBinding<LoadedTexture> skinBinding) {
|
||||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
|
synchronized (SkinBindingChangeListener.hole) {
|
||||||
return BindingMapping.of(skinBinding(account))
|
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||||
.map(it -> toAvatar(it.image, size))
|
if (oldListener != null)
|
||||||
.map(FXUtils::toFXImage);
|
oldListener.binding.removeListener(oldListener);
|
||||||
} else {
|
|
||||||
return Bindings.createObjectBinding(
|
SkinBindingChangeListener listener = new SkinBindingChangeListener(canvas, skinBinding);
|
||||||
() -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
|
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"),
|
CRAFT_TABLE("/assets/img/craft_table.png"),
|
||||||
FABRIC("/assets/img/fabric.png"),
|
FABRIC("/assets/img/fabric.png"),
|
||||||
FORGE("/assets/img/forge.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
|
// Please append new items at last
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import javafx.beans.property.*;
|
|||||||
import org.jackhuang.hmcl.game.*;
|
import org.jackhuang.hmcl.game.*;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||||
@@ -41,8 +42,6 @@ import java.util.Optional;
|
|||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.jfoenix.concurrency.JFXUtilities.runInFX;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
@@ -660,9 +659,7 @@ public final class VersionSetting implements Cloneable {
|
|||||||
.filter(java -> java.getVersion().equals(getJava()))
|
.filter(java -> java.getVersion().equals(getJava()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (matchedJava.isEmpty()) {
|
if (matchedJava.isEmpty()) {
|
||||||
runInFX(() -> {
|
FXUtils.runInFX(() -> setJava("Auto"));
|
||||||
setJava("Auto");
|
|
||||||
});
|
|
||||||
return JavaVersion.fromCurrentEnvironment();
|
return JavaVersion.fromCurrentEnvironment();
|
||||||
} else {
|
} else {
|
||||||
return matchedJava.stream()
|
return matchedJava.stream()
|
||||||
|
|||||||
@@ -34,10 +34,8 @@ import javafx.geometry.Pos;
|
|||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.image.*;
|
||||||
import javafx.scene.image.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.input.*;
|
||||||
import javafx.scene.layout.ColumnConstraints;
|
import javafx.scene.layout.ColumnConstraints;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
@@ -49,6 +47,7 @@ import javafx.scene.text.TextFlow;
|
|||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
import org.apache.commons.lang3.mutable.MutableObject;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
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.ExtendedProperties;
|
||||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
import org.w3c.dom.NodeList;
|
import org.w3c.dom.NodeList;
|
||||||
@@ -357,53 +357,77 @@ public final class FXUtils {
|
|||||||
|
|
||||||
public static void openFolder(File file) {
|
public static void openFolder(File file) {
|
||||||
if (!FileUtils.makeDirectory(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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String path = file.getAbsolutePath();
|
String path = file.getAbsolutePath();
|
||||||
|
|
||||||
switch (OperatingSystem.CURRENT_OS) {
|
String openCommand;
|
||||||
case OSX:
|
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 {
|
try {
|
||||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path});
|
int exitCode = SystemUtils.callExternalProcess(openCommand, path);
|
||||||
} catch (IOException e) {
|
|
||||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e);
|
// 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(() -> {
|
// Fallback to java.awt.Desktop::open
|
||||||
if (java.awt.Desktop.isDesktopSupported()) {
|
try {
|
||||||
try {
|
java.awt.Desktop.getDesktop().open(file);
|
||||||
java.awt.Desktop.getDesktop().open(file);
|
} catch (Throwable e) {
|
||||||
} catch (Throwable e) {
|
LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
}
|
||||||
}
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showFileInExplorer(Path file) {
|
public static void showFileInExplorer(Path file) {
|
||||||
switch (OperatingSystem.CURRENT_OS) {
|
String path = file.toAbsolutePath().toString();
|
||||||
case WINDOWS:
|
|
||||||
|
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 {
|
try {
|
||||||
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
|
int exitCode = SystemUtils.callExternalProcess(openCommands);
|
||||||
} catch (IOException e) {
|
|
||||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select," + file, e);
|
// 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:
|
// Fallback to open folder
|
||||||
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.
|
|
||||||
openFolder(file.getParent().toFile());
|
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)
|
if (link == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (java.awt.Desktop.isDesktopSupported()) {
|
thread(() -> {
|
||||||
thread(() -> {
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||||
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 {
|
try {
|
||||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
|
||||||
|
return;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
LOG.log(Level.WARNING, "An exception occurred while calling rundll32", e);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
} 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) {
|
public static void showWebDialog(String title, String content) {
|
||||||
@@ -469,12 +497,12 @@ public final class FXUtils {
|
|||||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
||||||
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
|
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
|
||||||
|
|
||||||
|
SwingUtils.initLookAndFeel();
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
final JFrame frame = new JFrame(title);
|
final JFrame frame = new JFrame(title);
|
||||||
frame.setSize(width, height);
|
frame.setSize(width, height);
|
||||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||||
frame.setLocationByPlatform(true);
|
frame.setLocationByPlatform(true);
|
||||||
//noinspection ConstantConditions
|
|
||||||
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
|
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
|
||||||
frame.setLayout(new BorderLayout());
|
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) {
|
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
|
||||||
|
MutableObject<Object> lastCell = new MutableObject<>();
|
||||||
return view -> new JFXListCell<T>() {
|
return view -> new JFXListCell<T>() {
|
||||||
@Override
|
@Override
|
||||||
public void updateItem(T item, boolean empty) {
|
public void updateItem(T item, boolean empty) {
|
||||||
super.updateItem(item, 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) {
|
if (!empty) {
|
||||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||||
setGraphic(graphicBuilder.apply(item));
|
setGraphic(graphicBuilder.apply(item));
|
||||||
@@ -688,11 +723,12 @@ public final class FXUtils {
|
|||||||
// Based on https://stackoverflow.com/a/57552025
|
// Based on https://stackoverflow.com/a/57552025
|
||||||
// Fix #874: Use it instead of SwingFXUtils.toFXImage
|
// Fix #874: Use it instead of SwingFXUtils.toFXImage
|
||||||
public static WritableImage toFXImage(BufferedImage image) {
|
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 iw = image.getWidth();
|
||||||
final int ih = image.getHeight();
|
final int ih = image.getHeight();
|
||||||
|
|
||||||
|
WritableImage wr = new WritableImage(iw, ih);
|
||||||
|
PixelWriter pw = wr.getPixelWriter();
|
||||||
|
|
||||||
for (int x = 0; x < iw; x++) {
|
for (int x = 0; x < iw; x++) {
|
||||||
for (int y = 0; y < ih; y++) {
|
for (int y = 0; y < ih; y++) {
|
||||||
pw.setArgb(x, y, image.getRGB(x, y));
|
pw.setArgb(x, y, image.getRGB(x, y));
|
||||||
@@ -701,6 +737,21 @@ public final class FXUtils {
|
|||||||
return wr;
|
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) {
|
public static void copyText(String text) {
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
content.putString(text);
|
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.ManagedProcess;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
@@ -239,15 +238,11 @@ public class GameCrashWindow extends Stage {
|
|||||||
.thenComposeAsync(logs ->
|
.thenComposeAsync(logs ->
|
||||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||||
.thenRunAsync(() -> {
|
.thenRunAsync(() -> {
|
||||||
|
FXUtils.showFileInExplorer(logFile);
|
||||||
|
|
||||||
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", 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.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
if (Desktop.isDesktopSupported()) {
|
|
||||||
try {
|
|
||||||
Desktop.getDesktop().open(logFile.toFile());
|
|
||||||
} catch (IOException | IllegalArgumentException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, Schedulers.javafx())
|
}, Schedulers.javafx())
|
||||||
.exceptionally(e -> {
|
.exceptionally(e -> {
|
||||||
LOG.log(Level.WARNING, "Failed to export game crash info", 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.control.*;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.apache.commons.lang3.mutable.MutableObject;
|
||||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayDeque;
|
import java.util.*;
|
||||||
import java.util.EnumMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -155,12 +151,12 @@ public final class LogWindow extends Stage {
|
|||||||
|
|
||||||
public class LogWindowImpl extends Control {
|
public class LogWindowImpl extends Control {
|
||||||
|
|
||||||
private ListView<Log> listView = new JFXListView<>();
|
private final ListView<Log> listView = new JFXListView<>();
|
||||||
private BooleanProperty autoScroll = new SimpleBooleanProperty();
|
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||||
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
private final 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 final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||||
private JFXComboBox<String> cboLines = new JFXComboBox<>();
|
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||||
private BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||||
|
|
||||||
LogWindowImpl() {
|
LogWindowImpl() {
|
||||||
getStyleClass().add("log-window");
|
getStyleClass().add("log-window");
|
||||||
@@ -207,13 +203,13 @@ public final class LogWindow extends Stage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
|
Platform.runLater(() -> {
|
||||||
if (Desktop.isDesktopSupported()) {
|
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||||
try {
|
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||||
Desktop.getDesktop().open(logFile.toFile());
|
alert.showAndWait();
|
||||||
} catch (IOException | IllegalArgumentException ignored) {
|
});
|
||||||
}
|
|
||||||
}
|
FXUtils.showFileInExplorer(logFile);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,13 +225,13 @@ public final class LogWindow extends Stage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
||||||
private static PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||||
private static PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||||
private static PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||||
private static PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
private static final PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
||||||
private static PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||||
private static PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||||
private static PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||||
|
|
||||||
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
|
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
|
||||||
ToggleButton button = new ToggleButton();
|
ToggleButton button = new ToggleButton();
|
||||||
@@ -292,6 +288,7 @@ public final class LogWindow extends Stage {
|
|||||||
listView.scrollTo(listView.getItems().size() - 1);
|
listView.scrollTo(listView.getItems().size() - 1);
|
||||||
});
|
});
|
||||||
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
|
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
|
||||||
|
MutableObject<Object> lastCell = new MutableObject<>();
|
||||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||||
{
|
{
|
||||||
getStyleClass().add("log-window-list-cell");
|
getStyleClass().add("log-window-list-cell");
|
||||||
@@ -308,6 +305,12 @@ public final class LogWindow extends Stage {
|
|||||||
@Override
|
@Override
|
||||||
protected void updateItem(Log item, boolean empty) {
|
protected void updateItem(Log item, boolean empty) {
|
||||||
super.updateItem(item, 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(EMPTY, empty);
|
||||||
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
|
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
|
||||||
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
|
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.property.SimpleObjectProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.canvas.Canvas;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import org.jackhuang.hmcl.auth.Account;
|
import org.jackhuang.hmcl.auth.Account;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
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.setting.Accounts;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||||
import org.jackhuang.hmcl.util.Pair;
|
|
||||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
|
|
||||||
import static javafx.beans.binding.Bindings.createStringBinding;
|
import static javafx.beans.binding.Bindings.createStringBinding;
|
||||||
import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory;
|
import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory;
|
||||||
import static org.jackhuang.hmcl.setting.Accounts.getLocalizedLoginTypeName;
|
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;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public class AccountAdvancedListItem extends AdvancedListItem {
|
public class AccountAdvancedListItem extends AdvancedListItem {
|
||||||
private final Tooltip tooltip;
|
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
|
@Override
|
||||||
protected void invalidated() {
|
protected void invalidated() {
|
||||||
@@ -55,17 +52,19 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
|||||||
if (account == null) {
|
if (account == null) {
|
||||||
titleProperty().unbind();
|
titleProperty().unbind();
|
||||||
subtitleProperty().unbind();
|
subtitleProperty().unbind();
|
||||||
imageView.imageProperty().unbind();
|
|
||||||
tooltip.textProperty().unbind();
|
tooltip.textProperty().unbind();
|
||||||
setTitle(i18n("account.missing"));
|
setTitle(i18n("account.missing"));
|
||||||
setSubtitle(i18n("account.missing.add"));
|
setSubtitle(i18n("account.missing.add"));
|
||||||
imageView.setImage(toFXImage(TexturesLoader.toAvatar(TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage(), 32)));
|
|
||||||
tooltip.setText(i18n("account.create"));
|
tooltip.setText(i18n("account.create"));
|
||||||
|
|
||||||
|
TexturesLoader.unbindAvatar(canvas);
|
||||||
|
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
|
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
|
||||||
subtitleProperty().bind(accountSubtitle(account));
|
subtitleProperty().bind(accountSubtitle(account));
|
||||||
imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
|
|
||||||
tooltip.textProperty().bind(accountTooltip(account));
|
tooltip.textProperty().bind(accountTooltip(account));
|
||||||
|
TexturesLoader.bindAvatar(canvas, account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -74,9 +73,8 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
|||||||
tooltip = new Tooltip();
|
tooltip = new Tooltip();
|
||||||
FXUtils.installFastTooltip(this, tooltip);
|
FXUtils.installFastTooltip(this, tooltip);
|
||||||
|
|
||||||
Pair<Node, ImageView> view = createImageView(null);
|
canvas = new Canvas(32, 32);
|
||||||
setLeftGraphic(view.getKey());
|
setLeftGraphic(canvas);
|
||||||
imageView = view.getValue();
|
|
||||||
|
|
||||||
setActionButtonVisible(false);
|
setActionButtonVisible(false);
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,11 @@ package org.jackhuang.hmcl.ui.account;
|
|||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.ObjectBinding;
|
import javafx.beans.binding.ObjectBinding;
|
||||||
import javafx.beans.binding.StringBinding;
|
import javafx.beans.binding.StringBinding;
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.beans.value.ObservableBooleanValue;
|
import javafx.beans.value.ObservableBooleanValue;
|
||||||
import javafx.scene.control.RadioButton;
|
import javafx.scene.control.RadioButton;
|
||||||
import javafx.scene.control.Skin;
|
import javafx.scene.control.Skin;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import org.jackhuang.hmcl.auth.Account;
|
import org.jackhuang.hmcl.auth.Account;
|
||||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
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.CompleteGameProfile;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
|
||||||
import org.jackhuang.hmcl.setting.Accounts;
|
import org.jackhuang.hmcl.setting.Accounts;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
@@ -70,7 +66,6 @@ public class AccountListItem extends RadioButton {
|
|||||||
private final Account account;
|
private final Account account;
|
||||||
private final StringProperty title = new SimpleStringProperty();
|
private final StringProperty title = new SimpleStringProperty();
|
||||||
private final StringProperty subtitle = new SimpleStringProperty();
|
private final StringProperty subtitle = new SimpleStringProperty();
|
||||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
|
|
||||||
|
|
||||||
public AccountListItem(Account account) {
|
public AccountListItem(Account account) {
|
||||||
this.account = account;
|
this.account = account;
|
||||||
@@ -95,8 +90,6 @@ public class AccountListItem extends RadioButton {
|
|||||||
account.getUsername().isEmpty() ? characterName :
|
account.getUsername().isEmpty() ? characterName :
|
||||||
Bindings.concat(account.getUsername(), " - ", characterName));
|
Bindings.concat(account.getUsername(), " - ", characterName));
|
||||||
}
|
}
|
||||||
|
|
||||||
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -226,16 +219,4 @@ public class AccountListItem extends RadioButton {
|
|||||||
public StringProperty subtitleProperty() {
|
public StringProperty subtitleProperty() {
|
||||||
return subtitle;
|
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.controls.JFXRadioButton;
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
import com.jfoenix.effects.JFXDepthManager;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.canvas.Canvas;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.SkinBase;
|
import javafx.scene.control.SkinBase;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||||
|
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||||
import org.jackhuang.hmcl.setting.Accounts;
|
import org.jackhuang.hmcl.setting.Accounts;
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
@@ -64,9 +65,8 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
|||||||
center.setSpacing(8);
|
center.setSpacing(8);
|
||||||
center.setAlignment(Pos.CENTER_LEFT);
|
center.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
ImageView imageView = new ImageView();
|
Canvas canvas = new Canvas(32, 32);
|
||||||
FXUtils.limitSize(imageView, 32, 32);
|
TexturesLoader.bindAvatar(canvas, skinnable.getAccount());
|
||||||
imageView.imageProperty().bind(skinnable.imageProperty());
|
|
||||||
|
|
||||||
Label title = new Label();
|
Label title = new Label();
|
||||||
title.getStyleClass().add("title");
|
title.getStyleClass().add("title");
|
||||||
@@ -84,7 +84,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
|||||||
item.getStyleClass().add("two-line-list-item");
|
item.getStyleClass().add("two-line-list-item");
|
||||||
BorderPane.setAlignment(item, Pos.CENTER);
|
BorderPane.setAlignment(item, Pos.CENTER);
|
||||||
|
|
||||||
center.getChildren().setAll(imageView, item);
|
center.getChildren().setAll(canvas, item);
|
||||||
root.setCenter(center);
|
root.setCenter(center);
|
||||||
|
|
||||||
HBox right = new HBox();
|
HBox right = new HBox();
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ import javafx.geometry.HPos;
|
|||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.canvas.Canvas;
|
||||||
import javafx.scene.control.Hyperlink;
|
import javafx.scene.control.Hyperlink;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.TextInputControl;
|
import javafx.scene.control.TextInputControl;
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
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 {
|
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
for (GameProfile profile : profiles) {
|
for (GameProfile profile : profiles) {
|
||||||
ImageView portraitView = new ImageView();
|
Canvas portraitCanvas = new Canvas(32, 32);
|
||||||
portraitView.setSmooth(false);
|
TexturesLoader.bindAvatar(portraitCanvas, service, profile.getId());
|
||||||
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
|
|
||||||
FXUtils.limitSize(portraitView, 32, 32);
|
|
||||||
|
|
||||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
IconedItem accountItem = new IconedItem(portraitCanvas, profile.getName());
|
||||||
accountItem.setOnMouseClicked(e -> {
|
accountItem.setOnMouseClicked(e -> {
|
||||||
selectedProfile = profile;
|
selectedProfile = profile;
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import javafx.css.PseudoClass;
|
|||||||
import javafx.scene.control.ListCell;
|
import javafx.scene.control.ListCell;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
import org.apache.commons.lang3.mutable.MutableObject;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
|
|
||||||
public abstract class MDListCell<T> extends ListCell<T> {
|
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 container = new StackPane();
|
||||||
private final StackPane root = 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);
|
setText(null);
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
|
|
||||||
@@ -50,6 +54,14 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
|||||||
@Override
|
@Override
|
||||||
protected void updateItem(T item, boolean empty) {
|
protected void updateItem(T item, boolean empty) {
|
||||||
super.updateItem(item, 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);
|
updateControl(item, empty);
|
||||||
if (empty) {
|
if (empty) {
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ public class DecoratorController {
|
|||||||
config().backgroundImageUrlProperty()));
|
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
|
* Load background image from bg/, background.png, background.jpg
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
|||||||
tab.select(worldTab);
|
tab.select(worldTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DownloadNavigator implements Navigation {
|
private static final class DownloadNavigator implements Navigation {
|
||||||
private final Map<String, Object> settings = new HashMap<>();
|
private final Map<String, Object> settings = new HashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import javafx.scene.layout.BorderPane;
|
|||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
import org.apache.commons.lang3.mutable.MutableObject;
|
||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||||
import org.jackhuang.hmcl.download.VersionList;
|
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.game.GameRemoteVersion;
|
||||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
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.Theme;
|
||||||
|
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
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.Refreshable;
|
||||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||||
import org.jackhuang.hmcl.util.HMCLService;
|
import org.jackhuang.hmcl.util.HMCLService;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
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)));
|
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
|
||||||
|
|
||||||
list.setCellFactory(listView -> new ListCell<RemoteVersion>() {
|
MutableObject<RemoteVersionListCell> lastCell = new MutableObject<>();
|
||||||
IconedTwoLineListItem content = new IconedTwoLineListItem();
|
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell));
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
list.setOnMouseClicked(e -> {
|
list.setOnMouseClicked(e -> {
|
||||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||||
@@ -290,4 +216,89 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
|||||||
private void onSponsor() {
|
private void onSponsor() {
|
||||||
HMCLService.openRedirectLink("bmclapi_sponsor");
|
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.geometry.Pos;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
|
import org.apache.commons.lang3.mutable.MutableObject;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.game.OAuthServer;
|
import org.jackhuang.hmcl.game.OAuthServer;
|
||||||
import org.jackhuang.hmcl.setting.Accounts;
|
import org.jackhuang.hmcl.setting.Accounts;
|
||||||
@@ -117,7 +118,8 @@ public class FeedbackPage extends VBox implements PageAware {
|
|||||||
JFXListView<FeedbackResponse> listView = new JFXListView<>();
|
JFXListView<FeedbackResponse> listView = new JFXListView<>();
|
||||||
spinnerPane.setContent(listView);
|
spinnerPane.setContent(listView);
|
||||||
Bindings.bindContent(listView.getItems(), feedbacks);
|
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 TwoLineListItem content = new TwoLineListItem();
|
||||||
private final JFXButton likeButton = new JFXButton();
|
private final JFXButton likeButton = new JFXButton();
|
||||||
private final JFXButton unlikeButton = new JFXButton();
|
private final JFXButton unlikeButton = new JFXButton();
|
||||||
@@ -230,7 +232,7 @@ public class FeedbackPage extends VBox implements PageAware {
|
|||||||
Controllers.dialog(new AddFeedbackDialog());
|
Controllers.dialog(new AddFeedbackDialog());
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LoginDialog extends JFXDialogLayout {
|
private static final class LoginDialog extends JFXDialogLayout {
|
||||||
private final SpinnerPane spinnerPane = new SpinnerPane();
|
private final SpinnerPane spinnerPane = new SpinnerPane();
|
||||||
private final Label errorLabel = new Label();
|
private final Label errorLabel = new Label();
|
||||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||||
@@ -241,10 +243,7 @@ public class FeedbackPage extends VBox implements PageAware {
|
|||||||
VBox vbox = new VBox(8);
|
VBox vbox = new VBox(8);
|
||||||
setBody(vbox);
|
setBody(vbox);
|
||||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> i18n("account.hmcl.hint")));
|
||||||
logging
|
|
||||||
? i18n("account.hmcl.hint")
|
|
||||||
: i18n("account.hmcl.hint")));
|
|
||||||
hintPane.setOnMouseClicked(e -> {
|
hintPane.setOnMouseClicked(e -> {
|
||||||
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
|
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
|
||||||
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
|
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.i18n.Locales;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
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)));
|
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
|
||||||
if (Desktop.isDesktopSupported()) {
|
FXUtils.showFileInExplorer(logFile);
|
||||||
try {
|
|
||||||
Desktop.getDesktop().open(logFile.toFile());
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import javafx.scene.Cursor;
|
|||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.text.TextAlignment;
|
import javafx.scene.text.TextAlignment;
|
||||||
|
import org.apache.commons.lang3.mutable.MutableObject;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
@@ -84,10 +85,17 @@ public class SponsorPage extends StackPane {
|
|||||||
StackPane pane = new StackPane();
|
StackPane pane = new StackPane();
|
||||||
pane.getStyleClass().add("card");
|
pane.getStyleClass().add("card");
|
||||||
listView = new JFXListView<>();
|
listView = new JFXListView<>();
|
||||||
|
MutableObject<Object> lastCell = new MutableObject<>();
|
||||||
listView.setCellFactory((listView) -> new JFXListCell<Sponsor>() {
|
listView.setCellFactory((listView) -> new JFXListCell<Sponsor>() {
|
||||||
@Override
|
@Override
|
||||||
public void updateItem(Sponsor item, boolean empty) {
|
public void updateItem(Sponsor item, boolean empty) {
|
||||||
super.updateItem(item, 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) {
|
if (!empty) {
|
||||||
setText(item.getName());
|
setText(item.getName());
|
||||||
setGraphic(null);
|
setGraphic(null);
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
|
|||||||
private final Path worldDir;
|
private final Path worldDir;
|
||||||
private final Datapack datapack;
|
private final Datapack datapack;
|
||||||
|
|
||||||
|
// Strongly referencing items, preventing GC collection
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
|
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
|
||||||
|
|
||||||
public DatapackListPage(String worldName, Path worldDir) {
|
public DatapackListPage(String worldName, Path worldDir) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import javafx.scene.image.ImageView;
|
|||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
import org.apache.commons.lang3.mutable.MutableObject;
|
||||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||||
import org.jackhuang.hmcl.mod.ModManager;
|
import org.jackhuang.hmcl.mod.ModManager;
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
@@ -84,12 +85,9 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
toolbarNormal.getChildren().setAll(
|
toolbarNormal.getChildren().setAll(
|
||||||
createToolbarButton2(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
|
createToolbarButton2(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
|
||||||
createToolbarButton2(i18n("mods.add"), SVG::plus, skinnable::add),
|
createToolbarButton2(i18n("mods.add"), SVG::plus, skinnable::add),
|
||||||
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, () ->
|
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, skinnable::openModFolder),
|
||||||
skinnable.openModFolder()),
|
createToolbarButton2(i18n("mods.check_updates"), SVG::update, skinnable::checkUpdates),
|
||||||
createToolbarButton2(i18n("mods.check_updates"), SVG::update, () ->
|
createToolbarButton2(i18n("download"), SVG::downloadOutline, skinnable::download));
|
||||||
skinnable.checkUpdates()),
|
|
||||||
createToolbarButton2(i18n("download"), SVG::downloadOutline, () ->
|
|
||||||
skinnable.download()));
|
|
||||||
HBox toolbarSelecting = new HBox();
|
HBox toolbarSelecting = new HBox();
|
||||||
toolbarSelecting.getChildren().setAll(
|
toolbarSelecting.getChildren().setAll(
|
||||||
createToolbarButton2(i18n("button.remove"), SVG::delete, () -> {
|
createToolbarButton2(i18n("button.remove"), SVG::delete, () -> {
|
||||||
@@ -121,7 +119,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
center.getStyleClass().add("large-spinner-pane");
|
center.getStyleClass().add("large-spinner-pane");
|
||||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
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);
|
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
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 final Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
|
||||||
private static Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
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();
|
JFXCheckBox checkBox = new JFXCheckBox();
|
||||||
TwoLineListItem content = new TwoLineListItem();
|
TwoLineListItem content = new TwoLineListItem();
|
||||||
JFXButton restoreButton = new JFXButton();
|
JFXButton restoreButton = new JFXButton();
|
||||||
@@ -287,8 +286,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
JFXButton revealButton = new JFXButton();
|
JFXButton revealButton = new JFXButton();
|
||||||
BooleanProperty booleanProperty;
|
BooleanProperty booleanProperty;
|
||||||
|
|
||||||
ModInfoListCell(JFXListView<ModInfoObject> listView) {
|
ModInfoListCell(JFXListView<ModInfoObject> listView, MutableObject<Object> lastCell) {
|
||||||
super(listView);
|
super(listView, lastCell);
|
||||||
|
|
||||||
HBox container = new HBox(8);
|
HBox container = new HBox(8);
|
||||||
container.setPickOnBounds(false);
|
container.setPickOnBounds(false);
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.util.StringUtils;
|
|||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -123,7 +122,7 @@ public enum ModTranslations {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
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());
|
mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import javafx.geometry.Insets;
|
|||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
import org.apache.commons.lang3.mutable.MutableObject;
|
||||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||||
import org.jackhuang.hmcl.mod.ModManager;
|
import org.jackhuang.hmcl.mod.ModManager;
|
||||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
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> {
|
public static class ModUpdateCell extends MDListCell<LocalModFile.ModUpdate> {
|
||||||
TwoLineListItem content = new TwoLineListItem();
|
TwoLineListItem content = new TwoLineListItem();
|
||||||
|
|
||||||
public ModUpdateCell(JFXListView<LocalModFile.ModUpdate> listView) {
|
public ModUpdateCell(JFXListView<LocalModFile.ModUpdate> listView, MutableObject<Object> lastCell) {
|
||||||
super(listView);
|
super(listView, lastCell);
|
||||||
|
|
||||||
getContainer().getChildren().setAll(content);
|
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.UpgradeDialog;
|
||||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -65,7 +65,7 @@ public final class UpdateHandler {
|
|||||||
performMigration();
|
performMigration();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.log(Level.WARNING, "Failed to perform migration", 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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -75,13 +75,13 @@ public final class UpdateHandler {
|
|||||||
applyUpdate(Paths.get(args[1]));
|
applyUpdate(Paths.get(args[1]));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.log(Level.WARNING, "Failed to apply update", 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFirstLaunchAfterUpgrade()) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ package org.jackhuang.hmcl.util;
|
|||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.reflect.TypeToken;
|
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.ChecksumMismatchException;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.Platform;
|
import org.jackhuang.hmcl.util.platform.Platform;
|
||||||
@@ -55,6 +56,7 @@ import java.io.*;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
@@ -72,6 +74,8 @@ public final class SelfDependencyPatcher {
|
|||||||
private final List<DependencyDescriptor> dependencies = DependencyDescriptor.readDependencies();
|
private final List<DependencyDescriptor> dependencies = DependencyDescriptor.readDependencies();
|
||||||
private final List<Repository> repositories;
|
private final List<Repository> repositories;
|
||||||
private final Repository defaultRepository;
|
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 {
|
private SelfDependencyPatcher() throws IncompatibleVersionException {
|
||||||
// We can only self-patch JavaFX on specific platform.
|
// 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.
|
* @throws IOException When the files cannot be fetched or saved.
|
||||||
*/
|
*/
|
||||||
private void fetchDependencies(List<DependencyDescriptor> dependencies) throws IOException {
|
private void fetchDependencies(List<DependencyDescriptor> dependencies) throws IOException {
|
||||||
|
SwingUtils.initLookAndFeel();
|
||||||
|
|
||||||
boolean isFirstTime = true;
|
boolean isFirstTime = true;
|
||||||
|
|
||||||
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
|
||||||
Repository repository = defaultRepository;
|
Repository repository = defaultRepository;
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@@ -360,8 +365,18 @@ public final class SelfDependencyPatcher {
|
|||||||
return missing;
|
return missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
|
private void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
|
||||||
ChecksumMismatchException.verifyChecksum(dependency.localPath(), "SHA-1", dependency.sha1());
|
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 {
|
public static class PatchException extends Exception {
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ public class OAuth {
|
|||||||
DEVICE,
|
DEVICE,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Result {
|
public static final class Result {
|
||||||
private final String accessToken;
|
private final String accessToken;
|
||||||
private final String refreshToken;
|
private final String refreshToken;
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ public class OAuth {
|
|||||||
@SerializedName("verification_uri")
|
@SerializedName("verification_uri")
|
||||||
public String verificationURI;
|
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")
|
@SerializedName("expires_in")
|
||||||
public int expiresIn;
|
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.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||||
import org.jackhuang.hmcl.util.io.HttpServer;
|
import org.jackhuang.hmcl.util.io.HttpServer;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
@@ -80,8 +79,7 @@ public class YggdrasilServer extends HttpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Response profiles(Request request) throws IOException {
|
private Response profiles(Request request) throws IOException {
|
||||||
String body = IOUtils.readFullyAsString(request.getSession().getInputStream(), UTF_8);
|
List<String> names = JsonUtils.fromNonNullJsonFully(request.getSession().getInputStream(), new TypeToken<List<String>>() {
|
||||||
List<String> names = JsonUtils.fromNonNullJson(body, new TypeToken<List<String>>() {
|
|
||||||
}.getType());
|
}.getType());
|
||||||
return ok(names.stream().distinct()
|
return ok(names.stream().distinct()
|
||||||
.map(this::findCharacterByName)
|
.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 name;
|
||||||
private final String hash;
|
private final String hash;
|
||||||
private final String type;
|
private final String type;
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ public class ForgeOldInstallTask extends Task<Version> {
|
|||||||
InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json"));
|
InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json"));
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
throw new ArtifactMalformedException("Malformed forge installer file, install_profile.json does not exist.");
|
throw new ArtifactMalformedException("Malformed forge installer file, install_profile.json does not exist.");
|
||||||
String json = IOUtils.readFullyAsString(stream);
|
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJsonFully(stream, ForgeInstallProfile.class);
|
||||||
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class);
|
|
||||||
|
|
||||||
// unpack the universal jar in the installer file.
|
// unpack the universal jar in the installer file.
|
||||||
Library forgeLibrary = new Library(installProfile.getInstall().getPath());
|
Library forgeLibrary = new Library(installProfile.getInstall().getPath());
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
|
|
||||||
private File baseDirectory;
|
private File baseDirectory;
|
||||||
protected Map<String, Version> versions;
|
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) {
|
public DefaultGameRepository(File baseDirectory) {
|
||||||
this.baseDirectory = 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 implementation may cause multiple flows against the same version entering
|
||||||
// this function, which is accepted because GameVersion::minecraftVersion should
|
// this function, which is accepted because GameVersion::minecraftVersion should
|
||||||
// be consistent.
|
// be consistent.
|
||||||
File versionJar = getVersionJar(version);
|
return gameVersions.computeIfAbsent(getVersionJar(version), versionJar -> {
|
||||||
if (gameVersions.containsKey(versionJar)) {
|
|
||||||
return gameVersions.get(versionJar);
|
|
||||||
} else {
|
|
||||||
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
||||||
|
|
||||||
if (!gameVersion.isPresent()) {
|
if (!gameVersion.isPresent()) {
|
||||||
LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists());
|
LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
gameVersions.put(versionJar, gameVersion);
|
|
||||||
return gameVersion;
|
return gameVersion;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ package org.jackhuang.hmcl.game;
|
|||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
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.ConstantPool;
|
||||||
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
||||||
import org.jenkinsci.constant_pool_scanner.ConstantType;
|
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.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.FileSystem;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.StreamSupport;
|
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.Lang.tryCast;
|
||||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
@@ -48,9 +46,9 @@ public final class GameVersion {
|
|||||||
private GameVersion() {
|
private GameVersion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<String> getVersionFromJson(Path versionJson) {
|
private static Optional<String> getVersionFromJson(InputStream versionJson) {
|
||||||
try {
|
try {
|
||||||
Map<?, ?> version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), Map.class);
|
Map<?, ?> version = JsonUtils.fromNonNullJsonFully(versionJson, Map.class);
|
||||||
return tryCast(version.get("name"), String.class);
|
return tryCast(version.get("name"), String.class);
|
||||||
} catch (IOException | JsonParseException e) {
|
} catch (IOException | JsonParseException e) {
|
||||||
LOG.log(Level.WARNING, "Failed to parse version.json", 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);
|
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||||
|
|
||||||
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
||||||
@@ -68,7 +66,7 @@ public final class GameVersion {
|
|||||||
.findFirst();
|
.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);
|
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||||
|
|
||||||
List<String> list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
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())
|
if (file == null || !file.exists() || !file.isFile() || !file.canRead())
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|
||||||
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
|
try (ZipFile gameJar = new ZipFile(file)) {
|
||||||
Path versionJson = gameJar.getPath("version.json");
|
ZipEntry versionJson = gameJar.getEntry("version.json");
|
||||||
if (Files.exists(versionJson)) {
|
if (versionJson != null) {
|
||||||
Optional<String> result = getVersionFromJson(versionJson);
|
Optional<String> result = getVersionFromJson(gameJar.getInputStream(versionJson));
|
||||||
if (result.isPresent())
|
if (result.isPresent())
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
ZipEntry minecraft = gameJar.getEntry("net/minecraft/client/Minecraft.class");
|
||||||
if (Files.exists(minecraft)) {
|
if (minecraft != null) {
|
||||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
try (InputStream is = gameJar.getInputStream(minecraft)) {
|
||||||
if (result.isPresent())
|
Optional<String> result = getVersionOfClassMinecraft(is);
|
||||||
return result;
|
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();
|
return Optional.empty();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public final class StringArgument implements Argument {
|
|||||||
return argument;
|
return argument;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Serializer implements JsonSerializer<StringArgument> {
|
public static final class Serializer implements JsonSerializer<StringArgument> {
|
||||||
@Override
|
@Override
|
||||||
public JsonElement serialize(StringArgument src, Type typeOfSrc, JsonSerializationContext context) {
|
public JsonElement serialize(StringArgument src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
return new JsonPrimitive(src.getArgument());
|
return new JsonPrimitive(src.getArgument());
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.mod;
|
|||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import org.jackhuang.hmcl.util.Immutable;
|
import org.jackhuang.hmcl.util.Immutable;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -112,7 +111,7 @@ public final class LiteModMetadata {
|
|||||||
ZipEntry entry = zipFile.getEntry("litemod.json");
|
ZipEntry entry = zipFile.getEntry("litemod.json");
|
||||||
if (entry == null)
|
if (entry == null)
|
||||||
throw new IOException("File " + modFile + "is not a LiteLoader mod.");
|
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)
|
if (metadata == null)
|
||||||
throw new IOException("Mod " + modFile + " `litemod.json` is malformed.");
|
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(),
|
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.mod.*;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ public final class McbbsModpackProvider implements ModpackProvider {
|
|||||||
config.getManifest().injectLaunchOptions(builder);
|
config.getManifest().injectLaunchOptions(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Modpack fromManifestFile(String json, Charset encoding) throws IOException, JsonParseException {
|
private static Modpack fromManifestFile(InputStream json, Charset encoding) throws IOException, JsonParseException {
|
||||||
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
|
McbbsModpackManifest manifest = JsonUtils.fromNonNullJsonFully(json, McbbsModpackManifest.class);
|
||||||
return manifest.toModpack(encoding);
|
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 {
|
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
|
||||||
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
|
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
|
||||||
if (mcbbsPackMeta != null) {
|
if (mcbbsPackMeta != null) {
|
||||||
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(mcbbsPackMeta)), encoding);
|
return fromManifestFile(zip.getInputStream(mcbbsPackMeta), encoding);
|
||||||
}
|
}
|
||||||
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
|
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
|
||||||
if (manifestJson != null) {
|
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");
|
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.apache.commons.compress.archivers.zip.ZipFile;
|
||||||
import org.jackhuang.hmcl.util.Immutable;
|
import org.jackhuang.hmcl.util.Immutable;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -60,8 +59,7 @@ public final class MultiMCManifest {
|
|||||||
ZipArchiveEntry mmcPack = zipFile.getEntry(rootEntryName + "mmc-pack.json");
|
ZipArchiveEntry mmcPack = zipFile.getEntry(rootEntryName + "mmc-pack.json");
|
||||||
if (mmcPack == null)
|
if (mmcPack == null)
|
||||||
return null;
|
return null;
|
||||||
String json = IOUtils.readFullyAsString(zipFile.getInputStream(mmcPack));
|
MultiMCManifest manifest = JsonUtils.fromNonNullJsonFully(zipFile.getInputStream(mmcPack), MultiMCManifest.class);
|
||||||
MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class);
|
|
||||||
if (manifest.getComponents() == null)
|
if (manifest.getComponents() == null)
|
||||||
throw new IOException("mmc-pack.json malformed.");
|
throw new IOException("mmc-pack.json malformed.");
|
||||||
|
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ public abstract class FetchTask<T> extends Task<T> {
|
|||||||
CACHED
|
CACHED
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class DownloadState {
|
protected static final class DownloadState {
|
||||||
private final int startPosition;
|
private final int startPosition;
|
||||||
private final int endPosition;
|
private final int endPosition;
|
||||||
private final int currentPosition;
|
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 javafx.application.Platform;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||||
@@ -61,10 +60,6 @@ public final class Schedulers {
|
|||||||
return Platform::runLater;
|
return Platform::runLater;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Executor swing() {
|
|
||||||
return SwingUtilities::invokeLater;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Executor defaultScheduler() {
|
public static Executor defaultScheduler() {
|
||||||
return ForkJoinPool.commonPool();
|
return ForkJoinPool.commonPool();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ import org.jackhuang.hmcl.util.io.IOUtils;
|
|||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.Channels;
|
import java.nio.channels.Channels;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.FileLock;
|
import java.nio.channels.FileLock;
|
||||||
@@ -54,7 +54,7 @@ public class CacheRepository {
|
|||||||
private Path cacheDirectory;
|
private Path cacheDirectory;
|
||||||
private Path indexFile;
|
private Path indexFile;
|
||||||
private Map<String, ETagItem> index;
|
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();
|
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
public void changeDirectory(Path commonDir) {
|
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);
|
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());
|
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
|
||||||
channel.truncate(0);
|
channel.truncate(0);
|
||||||
OutputStream os = Channels.newOutputStream(channel);
|
|
||||||
ETagIndex writeTo = new ETagIndex(newIndex.values());
|
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;
|
this.index = newIndex;
|
||||||
} finally {
|
} finally {
|
||||||
lock.release();
|
lock.release();
|
||||||
@@ -303,7 +302,7 @@ public class CacheRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ETagIndex {
|
private static final class ETagIndex {
|
||||||
private final Collection<ETagItem> eTag;
|
private final Collection<ETagItem> eTag;
|
||||||
|
|
||||||
public ETagIndex() {
|
public ETagIndex() {
|
||||||
@@ -315,7 +314,7 @@ public class CacheRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ETagItem {
|
private static final class ETagItem {
|
||||||
private final String url;
|
private final String url;
|
||||||
private final String eTag;
|
private final String eTag;
|
||||||
private final String hash;
|
private final String hash;
|
||||||
@@ -429,8 +428,7 @@ public class CacheRepository {
|
|||||||
if (indexOnDisk == null) indexOnDisk = new HashMap<>();
|
if (indexOnDisk == null) indexOnDisk = new HashMap<>();
|
||||||
indexOnDisk.putAll(storage);
|
indexOnDisk.putAll(storage);
|
||||||
channel.truncate(0);
|
channel.truncate(0);
|
||||||
OutputStream os = Channels.newOutputStream(channel);
|
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(storage).getBytes(UTF_8)));
|
||||||
IOUtils.write(JsonUtils.GSON.toJson(storage).getBytes(UTF_8), os);
|
|
||||||
this.storage = indexOnDisk;
|
this.storage = indexOnDisk;
|
||||||
} finally {
|
} finally {
|
||||||
lock.release();
|
lock.release();
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ import com.google.gson.JsonParseException;
|
|||||||
import com.google.gson.JsonSyntaxException;
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -44,6 +48,18 @@ public final class JsonUtils {
|
|||||||
private 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 {
|
public static <T> T fromNonNullJson(String json, Class<T> classOfT) throws JsonParseException {
|
||||||
T parsed = GSON.fromJson(json, classOfT);
|
T parsed = GSON.fromJson(json, classOfT);
|
||||||
if (parsed == null)
|
if (parsed == null)
|
||||||
@@ -58,6 +74,24 @@ public final class JsonUtils {
|
|||||||
return parsed;
|
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 {
|
public static <T> T fromMaybeMalformedJson(String json, Class<T> classOfT) throws JsonParseException {
|
||||||
try {
|
try {
|
||||||
return GSON.fromJson(json, classOfT);
|
return GSON.fromJson(json, classOfT);
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ public final class CompressingUtils {
|
|||||||
* @return the plain text content of given file.
|
* @return the plain text content of given file.
|
||||||
*/
|
*/
|
||||||
public static String readTextZipEntry(ZipFile zipFile, String name) throws IOException {
|
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 {
|
public static String readTextZipEntry(Path zipFile, String name, Charset encoding) throws IOException {
|
||||||
try (ZipFile s = openZipFile(zipFile, encoding)) {
|
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;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
public class HttpMultipartRequest implements Closeable {
|
public class HttpMultipartRequest implements Closeable {
|
||||||
|
private static final String endl = "\r\n";
|
||||||
|
|
||||||
private final String boundary = "*****" + System.currentTimeMillis() + "*****";
|
private final String boundary = "*****" + System.currentTimeMillis() + "*****";
|
||||||
private final HttpURLConnection urlConnection;
|
private final HttpURLConnection urlConnection;
|
||||||
private final ByteArrayOutputStream stream;
|
private final ByteArrayOutputStream stream;
|
||||||
private final String endl = "\r\n";
|
|
||||||
|
|
||||||
public HttpMultipartRequest(HttpURLConnection urlConnection) throws IOException {
|
public HttpMultipartRequest(HttpURLConnection urlConnection) throws IOException {
|
||||||
this.urlConnection = urlConnection;
|
this.urlConnection = urlConnection;
|
||||||
@@ -69,7 +70,7 @@ public class HttpMultipartRequest implements Closeable {
|
|||||||
addLine("--" + boundary + "--");
|
addLine("--" + boundary + "--");
|
||||||
urlConnection.setRequestProperty("Content-Length", "" + stream.size());
|
urlConnection.setRequestProperty("Content-Length", "" + stream.size());
|
||||||
try (OutputStream os = urlConnection.getOutputStream()) {
|
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.MalformedURLException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -143,7 +142,7 @@ public abstract class HttpRequest {
|
|||||||
return getStringWithRetry(() -> {
|
return getStringWithRetry(() -> {
|
||||||
HttpURLConnection con = createConnection();
|
HttpURLConnection con = createConnection();
|
||||||
con = resolveConnection(con);
|
con = resolveConnection(con);
|
||||||
return IOUtils.readFullyAsString(con.getInputStream(), StandardCharsets.UTF_8);
|
return IOUtils.readFullyAsString(con.getInputStream());
|
||||||
}, retryTimes);
|
}, retryTimes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
package org.jackhuang.hmcl.util.io;
|
package org.jackhuang.hmcl.util.io;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This utility class consists of some util methods operating on InputStream/OutputStream.
|
* 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.
|
* @throws IOException if an I/O error occurs.
|
||||||
*/
|
*/
|
||||||
public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException {
|
public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException {
|
||||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(stream.available(), 32));
|
||||||
copyTo(stream, result);
|
copyTo(stream, result);
|
||||||
return result.toByteArray();
|
return result.toByteArray();
|
||||||
}
|
}
|
||||||
@@ -54,7 +53,7 @@ public final class IOUtils {
|
|||||||
*/
|
*/
|
||||||
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
|
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
|
||||||
try (InputStream is = stream) {
|
try (InputStream is = stream) {
|
||||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(is.available(), 32));
|
||||||
copyTo(is, result);
|
copyTo(is, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -68,18 +67,6 @@ public final class IOUtils {
|
|||||||
return readFully(stream).toString("UTF-8");
|
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 {
|
public static void copyTo(InputStream src, OutputStream dest) throws IOException {
|
||||||
copyTo(src, dest, new byte[DEFAULT_BUFFER_SIZE]);
|
copyTo(src, dest, new byte[DEFAULT_BUFFER_SIZE]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import org.jackhuang.hmcl.util.Pair;
|
|||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ public final class NetworkUtils {
|
|||||||
public static String doGet(URL url) throws IOException {
|
public static String doGet(URL url) throws IOException {
|
||||||
HttpURLConnection con = createHttpConnection(url);
|
HttpURLConnection con = createHttpConnection(url);
|
||||||
con = resolveConnection(con);
|
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 {
|
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 {
|
public static String readData(HttpURLConnection con) throws IOException {
|
||||||
try {
|
try {
|
||||||
try (InputStream stdout = con.getInputStream()) {
|
try (InputStream stdout = con.getInputStream()) {
|
||||||
return IOUtils.readFullyAsString(stdout, UTF_8);
|
return IOUtils.readFullyAsString(stdout);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
try (InputStream stderr = con.getErrorStream()) {
|
try (InputStream stderr = con.getErrorStream()) {
|
||||||
if (stderr == null)
|
if (stderr == null)
|
||||||
throw e;
|
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.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class CrashReportAnalyzerTest {
|
public class CrashReportAnalyzerTest {
|
||||||
@@ -35,7 +34,7 @@ public class CrashReportAnalyzerTest {
|
|||||||
if (is == null) {
|
if (is == null) {
|
||||||
throw new IllegalStateException("Resource not found: " + path);
|
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) {
|
private CrashReportAnalyzer.Result findResultByRule(List<CrashReportAnalyzer.Result> results, CrashReportAnalyzer.Rule rule) {
|
||||||
|
|||||||
Reference in New Issue
Block a user