Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d1bd96d56 | ||
|
|
d48a3c2109 | ||
|
|
a0f9ba1976 | ||
|
|
b8df46ef87 | ||
|
|
0c04a013c8 | ||
|
|
3cf0acc505 | ||
|
|
3be5ea409b | ||
|
|
42f03411de | ||
|
|
941e7cd858 | ||
|
|
61c1b041cd | ||
|
|
7a48be5364 | ||
|
|
7d76fea48d | ||
|
|
600bd6e9ee | ||
|
|
ccc64db805 | ||
|
|
ea407be5f0 | ||
|
|
9b9471da49 | ||
|
|
b1402d3821 | ||
|
|
62f0d0a1c0 | ||
|
|
2865f2ffca |
No files matched your search
@@ -78,13 +78,13 @@ public class JFXRippler extends StackPane {
|
||||
protected Node control;
|
||||
|
||||
protected static final double RIPPLE_MAX_RADIUS = 300;
|
||||
|
||||
private boolean forceOverlay = false;
|
||||
private final Interpolator rippleInterpolator = Interpolator.SPLINE(0.0825,
|
||||
private static final Interpolator RIPPLE_INTERPOLATOR = Interpolator.SPLINE(0.0825,
|
||||
0.3025,
|
||||
0.0875,
|
||||
0.9975); //0.1, 0.54, 0.28, 0.95);
|
||||
|
||||
private boolean forceOverlay = false;
|
||||
|
||||
/// creates empty rippler node
|
||||
public JFXRippler() {
|
||||
this(null, RipplerMask.RECT, RipplerPos.FRONT);
|
||||
@@ -414,15 +414,15 @@ public class JFXRippler extends StackPane {
|
||||
overlayRect.setClip(getMask());
|
||||
getChildren().add(0, overlayRect);
|
||||
overlayRect.fillProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (ripplerFill.get() instanceof Color) {
|
||||
return new Color(((Color) ripplerFill.get()).getRed(),
|
||||
((Color) ripplerFill.get()).getGreen(),
|
||||
((Color) ripplerFill.get()).getBlue(),
|
||||
if (getRipplerFill() instanceof Color fill) {
|
||||
return new Color(fill.getRed(),
|
||||
fill.getGreen(),
|
||||
fill.getBlue(),
|
||||
0.2);
|
||||
} else {
|
||||
return Color.TRANSPARENT;
|
||||
}
|
||||
}, ripplerFill));
|
||||
}, ripplerFillProperty()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,8 +472,8 @@ public class JFXRippler extends StackPane {
|
||||
private Ripple(double centerX, double centerY) {
|
||||
super(centerX,
|
||||
centerY,
|
||||
ripplerRadius.get().doubleValue() == Region.USE_COMPUTED_SIZE ?
|
||||
computeRippleRadius() : ripplerRadius.get().doubleValue(), null);
|
||||
getRipplerRadius() == Region.USE_COMPUTED_SIZE ?
|
||||
computeRippleRadius() : getRipplerRadius(), null);
|
||||
setCache(true);
|
||||
setCacheHint(CacheHint.SPEED);
|
||||
setCacheShape(true);
|
||||
@@ -483,12 +483,12 @@ public class JFXRippler extends StackPane {
|
||||
KeyValue[] inKeyValues = new KeyValue[isRipplerRecenter() ? 4 : 2];
|
||||
outKeyValues = new KeyValue[isRipplerRecenter() ? 5 : 3];
|
||||
|
||||
inKeyValues[0] = new KeyValue(scaleXProperty(), 0.9, rippleInterpolator);
|
||||
inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, rippleInterpolator);
|
||||
inKeyValues[0] = new KeyValue(scaleXProperty(), 0.9, RIPPLE_INTERPOLATOR);
|
||||
inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, RIPPLE_INTERPOLATOR);
|
||||
|
||||
outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, rippleInterpolator);
|
||||
outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, rippleInterpolator);
|
||||
outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, rippleInterpolator);
|
||||
outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, RIPPLE_INTERPOLATOR);
|
||||
outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, RIPPLE_INTERPOLATOR);
|
||||
outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, RIPPLE_INTERPOLATOR);
|
||||
|
||||
if (isRipplerRecenter()) {
|
||||
double dx = (control.getLayoutBounds().getWidth() / 2 - centerX) / 1.55;
|
||||
@@ -496,42 +496,42 @@ public class JFXRippler extends StackPane {
|
||||
inKeyValues[2] = outKeyValues[3] = new KeyValue(translateXProperty(),
|
||||
Math.signum(dx) * Math.min(Math.abs(dx),
|
||||
this.getRadius() / 2),
|
||||
rippleInterpolator);
|
||||
RIPPLE_INTERPOLATOR);
|
||||
inKeyValues[3] = outKeyValues[4] = new KeyValue(translateYProperty(),
|
||||
Math.signum(dy) * Math.min(Math.abs(dy),
|
||||
this.getRadius() / 2),
|
||||
rippleInterpolator);
|
||||
RIPPLE_INTERPOLATOR);
|
||||
}
|
||||
inAnimation = new Timeline(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(scaleXProperty(),
|
||||
0,
|
||||
rippleInterpolator),
|
||||
RIPPLE_INTERPOLATOR),
|
||||
new KeyValue(scaleYProperty(),
|
||||
0,
|
||||
rippleInterpolator),
|
||||
RIPPLE_INTERPOLATOR),
|
||||
new KeyValue(translateXProperty(),
|
||||
0,
|
||||
rippleInterpolator),
|
||||
RIPPLE_INTERPOLATOR),
|
||||
new KeyValue(translateYProperty(),
|
||||
0,
|
||||
rippleInterpolator),
|
||||
RIPPLE_INTERPOLATOR),
|
||||
new KeyValue(opacityProperty(),
|
||||
1,
|
||||
rippleInterpolator)
|
||||
RIPPLE_INTERPOLATOR)
|
||||
), new KeyFrame(Duration.millis(900), inKeyValues));
|
||||
|
||||
setScaleX(0);
|
||||
setScaleY(0);
|
||||
if (ripplerFill.get() instanceof Color) {
|
||||
Color circleColor = new Color(((Color) ripplerFill.get()).getRed(),
|
||||
((Color) ripplerFill.get()).getGreen(),
|
||||
((Color) ripplerFill.get()).getBlue(),
|
||||
if (getRipplerFill() instanceof Color fill) {
|
||||
Color circleColor = new Color(fill.getRed(),
|
||||
fill.getGreen(),
|
||||
fill.getBlue(),
|
||||
0.3);
|
||||
setStroke(circleColor);
|
||||
setFill(circleColor);
|
||||
} else {
|
||||
setStroke(ripplerFill.get());
|
||||
setFill(ripplerFill.get());
|
||||
setStroke(getRipplerFill());
|
||||
setFill(getRipplerFill());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -584,110 +584,125 @@ public class JFXRippler extends StackPane {
|
||||
* the ripple recenter property, by default it's false.
|
||||
* if true the ripple effect will show gravitational pull to the center of its control
|
||||
*/
|
||||
private final StyleableObjectProperty<Boolean> ripplerRecenter = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.RIPPLER_RECENTER,
|
||||
JFXRippler.this,
|
||||
"ripplerRecenter",
|
||||
false);
|
||||
private StyleableBooleanProperty ripplerRecenter;
|
||||
|
||||
public Boolean isRipplerRecenter() {
|
||||
public boolean isRipplerRecenter() {
|
||||
return ripplerRecenter != null && ripplerRecenter.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Boolean> ripplerRecenterProperty() {
|
||||
public StyleableBooleanProperty ripplerRecenterProperty() {
|
||||
if (this.ripplerRecenter == null) {
|
||||
this.ripplerRecenter = new SimpleStyleableBooleanProperty(
|
||||
StyleableProperties.RIPPLER_RECENTER,
|
||||
JFXRippler.this,
|
||||
"ripplerRecenter",
|
||||
false);
|
||||
}
|
||||
return this.ripplerRecenter;
|
||||
}
|
||||
|
||||
public void setRipplerRecenter(Boolean radius) {
|
||||
this.ripplerRecenter.set(radius);
|
||||
public void setRipplerRecenter(boolean recenter) {
|
||||
ripplerRecenterProperty().set(recenter);
|
||||
}
|
||||
|
||||
/**
|
||||
* the ripple radius size, by default it will be automatically computed.
|
||||
*/
|
||||
private final StyleableObjectProperty<Number> ripplerRadius = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.RIPPLER_RADIUS,
|
||||
JFXRippler.this,
|
||||
"ripplerRadius",
|
||||
Region.USE_COMPUTED_SIZE);
|
||||
private StyleableDoubleProperty ripplerRadius;
|
||||
|
||||
public Number getRipplerRadius() {
|
||||
public double getRipplerRadius() {
|
||||
return ripplerRadius == null ? Region.USE_COMPUTED_SIZE : ripplerRadius.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Number> ripplerRadiusProperty() {
|
||||
public StyleableDoubleProperty ripplerRadiusProperty() {
|
||||
if (this.ripplerRadius == null) {
|
||||
this.ripplerRadius = new SimpleStyleableDoubleProperty(
|
||||
StyleableProperties.RIPPLER_RADIUS,
|
||||
JFXRippler.this,
|
||||
"ripplerRadius",
|
||||
Region.USE_COMPUTED_SIZE);
|
||||
}
|
||||
return this.ripplerRadius;
|
||||
}
|
||||
|
||||
public void setRipplerRadius(Number radius) {
|
||||
this.ripplerRadius.set(radius);
|
||||
public void setRipplerRadius(double radius) {
|
||||
ripplerRadiusProperty().set(radius);
|
||||
}
|
||||
|
||||
private static final Color DEFAULT_RIPPLER_FILL = Color.rgb(0, 200, 255);
|
||||
|
||||
/**
|
||||
* the default color of the ripple effect
|
||||
*/
|
||||
private final StyleableObjectProperty<Paint> ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
|
||||
JFXRippler.this,
|
||||
"ripplerFill",
|
||||
Color.rgb(0,
|
||||
200,
|
||||
255));
|
||||
private StyleableObjectProperty<Paint> ripplerFill;
|
||||
|
||||
public Paint getRipplerFill() {
|
||||
return ripplerFill == null ? Color.rgb(0, 200, 255) : ripplerFill.get();
|
||||
return ripplerFill == null ? DEFAULT_RIPPLER_FILL : ripplerFill.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> ripplerFillProperty() {
|
||||
if (this.ripplerFill == null) {
|
||||
this.ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
|
||||
JFXRippler.this,
|
||||
"ripplerFill",
|
||||
DEFAULT_RIPPLER_FILL);
|
||||
}
|
||||
return this.ripplerFill;
|
||||
}
|
||||
|
||||
public void setRipplerFill(Paint color) {
|
||||
this.ripplerFill.set(color);
|
||||
ripplerFillProperty().set(color);
|
||||
}
|
||||
|
||||
/// mask property used for clipping the rippler.
|
||||
/// can be either CIRCLE/RECT
|
||||
private final StyleableObjectProperty<RipplerMask> maskType = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.MASK_TYPE,
|
||||
JFXRippler.this,
|
||||
"maskType",
|
||||
RipplerMask.RECT);
|
||||
private StyleableObjectProperty<RipplerMask> maskType;
|
||||
|
||||
public RipplerMask getMaskType() {
|
||||
return maskType == null ? RipplerMask.RECT : maskType.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<RipplerMask> maskTypeProperty() {
|
||||
if (this.maskType == null) {
|
||||
this.maskType = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.MASK_TYPE,
|
||||
JFXRippler.this,
|
||||
"maskType",
|
||||
RipplerMask.RECT);
|
||||
}
|
||||
return this.maskType;
|
||||
}
|
||||
|
||||
public void setMaskType(RipplerMask type) {
|
||||
this.maskType.set(type);
|
||||
if (this.maskType != null || type != RipplerMask.RECT)
|
||||
maskTypeProperty().set(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* the ripple disable, by default it's false.
|
||||
* if true the ripple effect will be hidden
|
||||
*/
|
||||
private final StyleableBooleanProperty ripplerDisabled = new SimpleStyleableBooleanProperty(
|
||||
StyleableProperties.RIPPLER_DISABLED,
|
||||
JFXRippler.this,
|
||||
"ripplerDisabled",
|
||||
false);
|
||||
private StyleableBooleanProperty ripplerDisabled;
|
||||
|
||||
public Boolean isRipplerDisabled() {
|
||||
public boolean isRipplerDisabled() {
|
||||
return ripplerDisabled != null && ripplerDisabled.get();
|
||||
}
|
||||
|
||||
public StyleableBooleanProperty ripplerDisabledProperty() {
|
||||
if (this.ripplerDisabled == null) {
|
||||
this.ripplerDisabled = new SimpleStyleableBooleanProperty(
|
||||
StyleableProperties.RIPPLER_DISABLED,
|
||||
JFXRippler.this,
|
||||
"ripplerDisabled",
|
||||
false);
|
||||
}
|
||||
return this.ripplerDisabled;
|
||||
}
|
||||
|
||||
public void setRipplerDisabled(Boolean disabled) {
|
||||
this.ripplerDisabled.set(disabled);
|
||||
public void setRipplerDisabled(boolean disabled) {
|
||||
ripplerDisabledProperty().set(disabled);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* indicates whether the ripple effect is infront of or behind the node
|
||||
*/
|
||||
@@ -734,7 +749,7 @@ public class JFXRippler extends StackPane {
|
||||
};
|
||||
private static final CssMetaData<JFXRippler, Paint> RIPPLER_FILL =
|
||||
new CssMetaData<>("-jfx-rippler-fill",
|
||||
PaintConverter.getInstance(), Color.rgb(0, 200, 255)) {
|
||||
PaintConverter.getInstance(), DEFAULT_RIPPLER_FILL) {
|
||||
@Override
|
||||
public boolean isSettable(JFXRippler control) {
|
||||
return control.ripplerFill == null || !control.ripplerFill.isBound();
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
@@ -44,13 +45,15 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -310,11 +313,18 @@ public final class TerracottaManager {
|
||||
public static TerracottaState.HostScanning setScanning() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/scanning", portSpecific.port), Map.of(
|
||||
"player", getPlayerName()
|
||||
));
|
||||
Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
List<Pair<String, String>> query = new ArrayList<>(nodes.size() + 1);
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery(
|
||||
"http://127.0.0.1:%d/state/scanning".formatted(portSpecific.port), query
|
||||
)).setSignificance(Task.TaskSignificance.MINOR);
|
||||
}).start();
|
||||
|
||||
new GetTask(uri).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||
return new TerracottaState.HostScanning(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
@@ -323,15 +333,19 @@ public final class TerracottaManager {
|
||||
public static Task<TerracottaState.GuestConnecting> setGuesting(String room) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
String uri = NetworkUtils.withQuery(String.format("http://127.0.0.1:%d/state/guesting", portSpecific.port), Map.of(
|
||||
"room", room,
|
||||
"player", getPlayerName()
|
||||
));
|
||||
|
||||
return new GetTask(uri)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
return Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
ArrayList<Pair<String, String>> query = new ArrayList<>(nodes.size() + 2);
|
||||
query.add(pair("room", room));
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (URI node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery("http://127.0.0.1:%d/state/guesting".formatted(portSpecific.port), query))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <[email protected]> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class TerracottaNodeList {
|
||||
private static final String NODE_LIST_URL = "https://terracotta.glavo.site/nodes";
|
||||
|
||||
@JsonSerializable
|
||||
private record TerracottaNode(String url, @Nullable String region) implements Validation {
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
Validation.requireNonNull(url, "TerracottaNode.url cannot be null");
|
||||
try {
|
||||
new URI(url);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new JsonParseException("Invalid URL: " + url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile List<URI> list;
|
||||
|
||||
public static List<URI> fetch() {
|
||||
List<URI> list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
synchronized (TerracottaNodeList.class) {
|
||||
list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
try {
|
||||
List<TerracottaNode> nodes = HttpRequest.GET(NODE_LIST_URL)
|
||||
.getJson(JsonUtils.listTypeOf(TerracottaNode.class));
|
||||
|
||||
if (nodes == null) {
|
||||
list = List.of();
|
||||
LOG.info("No available Terracotta nodes found");
|
||||
} else {
|
||||
list = nodes.stream()
|
||||
.filter(node -> {
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
node.validate();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Invalid terracotta node: " + node, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region);
|
||||
})
|
||||
.map(it -> URI.create(it.url()))
|
||||
.toList();
|
||||
LOG.info("Terracotta node list: " + list);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to fetch terracotta node list", e);
|
||||
list = List.of();
|
||||
}
|
||||
|
||||
TerracottaNodeList.list = list;
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private TerracottaNodeList() {
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class InstallerItem extends Control {
|
||||
private final ObjectProperty<Runnable> onInstall = new SimpleObjectProperty<>(this, "onInstall");
|
||||
private final ObjectProperty<Runnable> onRemove = new SimpleObjectProperty<>(this, "onRemove");
|
||||
|
||||
public interface State {
|
||||
public sealed interface State {
|
||||
}
|
||||
|
||||
public static final class InstallableState implements State {
|
||||
@@ -73,46 +73,10 @@ public class InstallerItem extends Control {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class IncompatibleState implements State {
|
||||
private final String incompatibleItemName;
|
||||
private final String incompatibleItemVersion;
|
||||
|
||||
public IncompatibleState(String incompatibleItemName, String incompatibleItemVersion) {
|
||||
this.incompatibleItemName = incompatibleItemName;
|
||||
this.incompatibleItemVersion = incompatibleItemVersion;
|
||||
}
|
||||
|
||||
public String getIncompatibleItemName() {
|
||||
return incompatibleItemName;
|
||||
}
|
||||
|
||||
public String getIncompatibleItemVersion() {
|
||||
return incompatibleItemVersion;
|
||||
}
|
||||
public record IncompatibleState(String incompatibleItemName, String incompatibleItemVersion) implements State {
|
||||
}
|
||||
|
||||
public static final class InstalledState implements State {
|
||||
private final String version;
|
||||
private final boolean external;
|
||||
private final boolean incompatibleWithGame;
|
||||
|
||||
public InstalledState(String version, boolean external, boolean incompatibleWithGame) {
|
||||
this.version = version;
|
||||
this.external = external;
|
||||
this.incompatibleWithGame = incompatibleWithGame;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public boolean isExternal() {
|
||||
return external;
|
||||
}
|
||||
|
||||
public boolean isIncompatibleWithGame() {
|
||||
return incompatibleWithGame;
|
||||
}
|
||||
public record InstalledState(String version, boolean external, boolean incompatibleWithGame) implements State {
|
||||
}
|
||||
|
||||
public enum Style {
|
||||
@@ -336,11 +300,15 @@ public class InstallerItem extends Control {
|
||||
}
|
||||
pane.getStyleClass().add("installer-item");
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
container.setPosition(JFXRippler.RipplerPos.FRONT);
|
||||
getChildren().setAll(container);
|
||||
container.setPosition(JFXRippler.RipplerPos.BACK);
|
||||
StackPane paneWrapper = new StackPane();
|
||||
paneWrapper.getStyleClass().add("installer-item-wrapper");
|
||||
paneWrapper.getChildren().setAll(container);
|
||||
getChildren().setAll(paneWrapper);
|
||||
|
||||
pane.pseudoClassStateChanged(LIST_ITEM, control.style == Style.LIST_ITEM);
|
||||
pane.pseudoClassStateChanged(CARD, control.style == Style.CARD);
|
||||
paneWrapper.pseudoClassStateChanged(CARD, control.style == Style.CARD);
|
||||
|
||||
if (control.iconType != null) {
|
||||
ImageView view = new ImageView(control.iconType.getIcon());
|
||||
@@ -369,21 +337,20 @@ public class InstallerItem extends Control {
|
||||
statusLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
State state = control.resolvedStateProperty.get();
|
||||
|
||||
if (state instanceof InstalledState) {
|
||||
InstalledState s = (InstalledState) state;
|
||||
if (s.incompatibleWithGame) {
|
||||
return i18n("install.installer.change_version", s.version);
|
||||
if (state instanceof InstalledState installedState) {
|
||||
if (installedState.incompatibleWithGame) {
|
||||
return i18n("install.installer.change_version", installedState.version);
|
||||
}
|
||||
if (s.external) {
|
||||
return i18n("install.installer.external_version", s.version);
|
||||
if (installedState.external) {
|
||||
return i18n("install.installer.external_version", installedState.version);
|
||||
}
|
||||
return i18n("install.installer.version", s.version);
|
||||
return i18n("install.installer.version", installedState.version);
|
||||
} else if (state instanceof InstallableState) {
|
||||
return control.style == Style.CARD
|
||||
? i18n("install.installer.do_not_install")
|
||||
: i18n("install.installer.not_installed");
|
||||
} else if (state instanceof IncompatibleState) {
|
||||
return i18n("install.installer.incompatible", i18n("install.installer." + ((IncompatibleState) state).incompatibleItemName));
|
||||
} else if (state instanceof IncompatibleState incompatibleState) {
|
||||
return i18n("install.installer.incompatible", i18n("install.installer." + incompatibleState.incompatibleItemName));
|
||||
} else {
|
||||
throw new AssertionError("Unknown state type: " + state.getClass());
|
||||
}
|
||||
@@ -404,7 +371,7 @@ public class InstallerItem extends Control {
|
||||
} else {
|
||||
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
State state = control.resolvedStateProperty.get();
|
||||
return state instanceof InstalledState && !((InstalledState) state).external;
|
||||
return state instanceof InstalledState installedState && !installedState.external;
|
||||
}, control.resolvedStateProperty));
|
||||
}
|
||||
removeButton.managedProperty().bind(removeButton.visibleProperty());
|
||||
|
||||
@@ -18,24 +18,26 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>> extends SkinBase<T> {
|
||||
public abstract class ToolbarListPageSkin<E, P extends ListPageBase<E>> extends SkinBase<P> {
|
||||
|
||||
public ToolbarListPageSkin(T skinnable) {
|
||||
protected final JFXListView<E> listView;
|
||||
|
||||
public ToolbarListPageSkin(P skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
@@ -58,18 +60,12 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
}
|
||||
|
||||
{
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
ComponentList.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
VBox content = new VBox();
|
||||
|
||||
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(content);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
|
||||
root.getContent().add(scrollPane);
|
||||
this.listView = new JFXListView<>();
|
||||
this.listView.setPadding(Insets.EMPTY);
|
||||
this.listView.setCellFactory(listView -> createListCell((JFXListView<E>) listView));
|
||||
ComponentList.setVgrow(listView, Priority.ALWAYS);
|
||||
Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty());
|
||||
root.getContent().add(listView);
|
||||
}
|
||||
|
||||
spinnerPane.setContent(root);
|
||||
@@ -102,5 +98,21 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected abstract List<Node> initializeToolbar(T skinnable);
|
||||
protected abstract List<Node> initializeToolbar(P skinnable);
|
||||
|
||||
protected ListCell<E> createListCell(JFXListView<E> listView) {
|
||||
return new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(E item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (!empty && item instanceof Node node) {
|
||||
setGraphic(node);
|
||||
setText(null);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setText(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// TODO: Replace ToolbarListPageSkin with this class gradually
|
||||
public abstract class ToolbarListPageSkin2<E, P extends ListPageBase<E>> extends SkinBase<P> {
|
||||
|
||||
protected final JFXListView<E> listView;
|
||||
|
||||
public ToolbarListPageSkin2(P skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
|
||||
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
|
||||
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
StackPane.setMargin(root, new Insets(10));
|
||||
|
||||
List<Node> toolbarButtons = initializeToolbar(skinnable);
|
||||
if (!toolbarButtons.isEmpty()) {
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
toolbar.setPickOnBounds(false);
|
||||
toolbar.getChildren().setAll(toolbarButtons);
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
{
|
||||
this.listView = new JFXListView<>();
|
||||
this.listView.setPadding(Insets.EMPTY);
|
||||
ComponentList.setVgrow(listView, Priority.ALWAYS);
|
||||
Bindings.bindContent(this.listView.getItems(), skinnable.itemsProperty());
|
||||
root.getContent().add(listView);
|
||||
}
|
||||
|
||||
spinnerPane.setContent(root);
|
||||
|
||||
getChildren().setAll(spinnerPane);
|
||||
}
|
||||
|
||||
protected abstract List<Node> initializeToolbar(P skinnable);
|
||||
}
|
||||
@@ -82,7 +82,9 @@ public class TransitionPane extends StackPane {
|
||||
duration, interpolator);
|
||||
newAnimation.setOnFinished(e -> {
|
||||
setMouseTransparent(false);
|
||||
getChildren().remove(previousNode);
|
||||
if (previousNode != currentNode) {
|
||||
getChildren().remove(previousNode);
|
||||
}
|
||||
|
||||
if (cacheHint != null) {
|
||||
newView.setCache(false);
|
||||
|
||||
@@ -264,7 +264,7 @@ public class DecoratorController {
|
||||
private Background createBackgroundWithOpacity(Image image, int opacity) {
|
||||
if (opacity <= 0) {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
} else if (opacity >= 100) {
|
||||
} else if (opacity >= 100 || image.getPixelReader() == null) {
|
||||
return new Background(new BackgroundImage(
|
||||
image,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
|
||||
@@ -50,7 +50,7 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return group.getGame().versionProperty().get().getVersion();
|
||||
return group.getGame().versionProperty().get().version();
|
||||
}
|
||||
|
||||
private String getVersion(String id) {
|
||||
@@ -111,7 +111,7 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
}
|
||||
|
||||
private void setTxtNameWithLoaders() {
|
||||
StringBuilder nameBuilder = new StringBuilder(group.getGame().versionProperty().get().getVersion());
|
||||
StringBuilder nameBuilder = new StringBuilder(group.getGame().versionProperty().get().version());
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId().replace(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), "");
|
||||
|
||||
@@ -18,17 +18,19 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.java.JavaInfo;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
@@ -42,6 +44,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -62,7 +65,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class JavaManagementPage extends ListPageBase<JavaManagementPage.JavaItem> {
|
||||
public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final ChangeListener<Collection<JavaRuntime>> listener;
|
||||
@@ -172,130 +175,17 @@ public final class JavaManagementPage extends ListPageBase<JavaManagementPage.Ja
|
||||
}).start();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
@FXThread
|
||||
private void loadJava(Collection<JavaRuntime> javaRuntimes) {
|
||||
if (javaRuntimes != null) {
|
||||
List<JavaItem> items = new ArrayList<>();
|
||||
for (JavaRuntime java : javaRuntimes) {
|
||||
items.add(new JavaItem(java));
|
||||
}
|
||||
this.setItems(FXCollections.observableList(items));
|
||||
this.setItems(FXCollections.observableArrayList(javaRuntimes));
|
||||
this.setLoading(false);
|
||||
} else
|
||||
} else {
|
||||
this.setLoading(true);
|
||||
}
|
||||
|
||||
static final class JavaItem extends Control {
|
||||
private final JavaRuntime java;
|
||||
|
||||
public JavaItem(JavaRuntime java) {
|
||||
this.java = java;
|
||||
}
|
||||
|
||||
public JavaRuntime getJava() {
|
||||
return java;
|
||||
}
|
||||
|
||||
public void onReveal() {
|
||||
Path target;
|
||||
Path parent = java.getBinary().getParent();
|
||||
if (parent != null
|
||||
&& parent.getParent() != null
|
||||
&& parent.getFileName() != null
|
||||
&& parent.getFileName().toString().equals("bin")
|
||||
&& Files.exists(parent.getParent().resolve("release"))) {
|
||||
target = parent.getParent();
|
||||
} else {
|
||||
target = java.getBinary();
|
||||
}
|
||||
|
||||
FXUtils.showFileInExplorer(target);
|
||||
}
|
||||
|
||||
public void onRemove() {
|
||||
if (java.isManaged()) {
|
||||
Controllers.taskDialog(JavaManager.getUninstallJavaTask(java), i18n("java.uninstall"), TaskCancellationAction.NORMAL);
|
||||
} else {
|
||||
String path = java.getBinary().toString();
|
||||
ConfigHolder.globalConfig().getUserJava().remove(path);
|
||||
ConfigHolder.globalConfig().getDisabledJava().add(path);
|
||||
try {
|
||||
JavaManager.removeJava(java);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JavaRuntimeItemSkin(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class JavaRuntimeItemSkin extends SkinBase<JavaItem> {
|
||||
|
||||
JavaRuntimeItemSkin(JavaItem control) {
|
||||
super(control);
|
||||
JavaRuntime java = control.getJava();
|
||||
String vendor = JavaInfo.normalizeVendor(java.getVendor());
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
HBox center = new HBox();
|
||||
center.setMouseTransparent(true);
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.setTitle((java.isJDK() ? "JDK" : "JRE") + " " + java.getVersion());
|
||||
item.setSubtitle(java.getBinary().toString());
|
||||
item.addTag(i18n("java.info.architecture") + ": " + java.getArchitecture().getDisplayName());
|
||||
if (vendor != null)
|
||||
item.addTag(i18n("java.info.vendor") + ": " + vendor);
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
center.getChildren().setAll(item);
|
||||
root.setCenter(center);
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
{
|
||||
JFXButton revealButton = new JFXButton();
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OPEN.createIcon(24), 24, 24));
|
||||
revealButton.setOnAction(e -> control.onReveal());
|
||||
FXUtils.installFastTooltip(revealButton, i18n("reveal.in_file_manager"));
|
||||
|
||||
JFXButton removeButton = new JFXButton();
|
||||
removeButton.getStyleClass().add("toggle-icon4");
|
||||
removeButton.setOnAction(e -> Controllers.confirm(
|
||||
java.isManaged() ? i18n("java.uninstall.confirm") : i18n("java.disable.confirm"),
|
||||
i18n("message.warning"),
|
||||
control::onRemove,
|
||||
null
|
||||
));
|
||||
if (java.isManaged()) {
|
||||
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE_FOREVER.createIcon(24), 24, 24));
|
||||
FXUtils.installFastTooltip(removeButton, i18n("java.uninstall"));
|
||||
if (JavaRuntime.CURRENT_JAVA != null && java.getBinary().equals(JavaRuntime.CURRENT_JAVA.getBinary()))
|
||||
removeButton.setDisable(true);
|
||||
} else {
|
||||
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE.createIcon(24), 24, 24));
|
||||
FXUtils.installFastTooltip(removeButton, i18n("java.disable"));
|
||||
}
|
||||
|
||||
right.getChildren().setAll(revealButton, removeButton);
|
||||
}
|
||||
root.setRight(right);
|
||||
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
root.setPadding(new Insets(8));
|
||||
|
||||
getChildren().setAll(new RipplerContainer(root));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class JavaPageSkin extends ToolbarListPageSkin<JavaManagementPage> {
|
||||
private static final class JavaPageSkin extends ToolbarListPageSkin<JavaRuntime, JavaManagementPage> {
|
||||
|
||||
JavaPageSkin(JavaManagementPage skinnable) {
|
||||
super(skinnable);
|
||||
@@ -317,5 +207,139 @@ public final class JavaManagementPage extends ListPageBase<JavaManagementPage.Ja
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListCell<JavaRuntime> createListCell(JFXListView<JavaRuntime> listView) {
|
||||
return new JavaItemCell();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class JavaItemCell extends ListCell<JavaRuntime> {
|
||||
private final Node graphic;
|
||||
private final TwoLineListItem content;
|
||||
|
||||
private SVG removeIcon;
|
||||
private final StackPane removeIconPane;
|
||||
private final Tooltip removeTooltip = new Tooltip();
|
||||
|
||||
JavaItemCell() {
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
HBox center = new HBox();
|
||||
center.setMouseTransparent(true);
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
this.content = new TwoLineListItem();
|
||||
|
||||
BorderPane.setAlignment(content, Pos.CENTER);
|
||||
center.getChildren().setAll(content);
|
||||
root.setCenter(center);
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
{
|
||||
JFXButton revealButton = new JFXButton();
|
||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OPEN.createIcon(24), 24, 24));
|
||||
revealButton.getStyleClass().add("toggle-icon4");
|
||||
revealButton.setOnAction(e -> {
|
||||
JavaRuntime java = getItem();
|
||||
if (java != null)
|
||||
onReveal(java);
|
||||
});
|
||||
FXUtils.installFastTooltip(revealButton, i18n("reveal.in_file_manager"));
|
||||
|
||||
JFXButton removeButton = new JFXButton();
|
||||
removeButton.getStyleClass().add("toggle-icon4");
|
||||
removeButton.setOnAction(e -> {
|
||||
JavaRuntime java = getItem();
|
||||
if (java != null)
|
||||
onRemove(java);
|
||||
});
|
||||
FXUtils.installFastTooltip(removeButton, removeTooltip);
|
||||
|
||||
this.removeIconPane = new StackPane();
|
||||
removeIconPane.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(removeIconPane, 24);
|
||||
FXUtils.setLimitHeight(removeIconPane, 24);
|
||||
removeButton.setGraphic(removeIconPane);
|
||||
|
||||
right.getChildren().setAll(revealButton, removeButton);
|
||||
}
|
||||
root.setRight(right);
|
||||
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
root.setPadding(new Insets(8));
|
||||
|
||||
this.graphic = new RipplerContainer(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(JavaRuntime item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
content.setTitle((item.isJDK() ? "JDK" : "JRE") + " " + item.getVersion());
|
||||
content.setSubtitle(item.getBinary().toString());
|
||||
|
||||
content.getTags().clear();
|
||||
content.addTag(i18n("java.info.architecture") + ": " + item.getArchitecture().getDisplayName());
|
||||
String vendor = JavaInfo.normalizeVendor(item.getVendor());
|
||||
if (vendor != null)
|
||||
content.addTag(i18n("java.info.vendor") + ": " + vendor);
|
||||
|
||||
SVG newRemoveIcon = item.isManaged() ? SVG.DELETE_FOREVER : SVG.DELETE;
|
||||
if (removeIcon != newRemoveIcon) {
|
||||
removeIcon = newRemoveIcon;
|
||||
removeIconPane.getChildren().setAll(removeIcon.createIcon(24));
|
||||
removeTooltip.setText(item.isManaged() ? i18n("java.uninstall") : i18n("java.disable"));
|
||||
}
|
||||
|
||||
setGraphic(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
private void onReveal(JavaRuntime java) {
|
||||
Path target;
|
||||
Path parent = java.getBinary().getParent();
|
||||
if (parent != null
|
||||
&& parent.getParent() != null
|
||||
&& parent.getFileName() != null
|
||||
&& parent.getFileName().toString().equals("bin")
|
||||
&& Files.exists(parent.getParent().resolve("release"))) {
|
||||
target = parent.getParent();
|
||||
} else {
|
||||
target = java.getBinary();
|
||||
}
|
||||
|
||||
FXUtils.showFileInExplorer(target);
|
||||
}
|
||||
|
||||
private void onRemove(JavaRuntime java) {
|
||||
if (java.isManaged()) {
|
||||
Controllers.confirm(
|
||||
i18n("java.uninstall.confirm"),
|
||||
i18n("message.warning"),
|
||||
() -> Controllers.taskDialog(JavaManager.getUninstallJavaTask(java), i18n("java.uninstall"), TaskCancellationAction.NORMAL),
|
||||
null
|
||||
);
|
||||
} else {
|
||||
Controllers.confirm(
|
||||
i18n("java.disable.confirm"),
|
||||
i18n("message.warning"),
|
||||
() -> {
|
||||
String path = java.getBinary().toString();
|
||||
ConfigHolder.globalConfig().getUserJava().remove(path);
|
||||
ConfigHolder.globalConfig().getDisabledJava().add(path);
|
||||
try {
|
||||
JavaManager.removeJava(java);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
},
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,7 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
|
||||
}
|
||||
}
|
||||
|
||||
private static final class JavaRestorePageSkin extends ToolbarListPageSkin<JavaRestorePage> {
|
||||
private static final class JavaRestorePageSkin extends ToolbarListPageSkin<DisabledJavaItem, JavaRestorePage> {
|
||||
JavaRestorePageSkin(JavaRestorePage skinnable) {
|
||||
super(skinnable);
|
||||
}
|
||||
|
||||
@@ -56,29 +56,20 @@ import org.jackhuang.hmcl.ui.construct.FloatListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.AggregatedObservableList;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
|
||||
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
@@ -247,6 +238,12 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
||||
private final JFXListView<RemoteMod> listView = new JFXListView<>();
|
||||
private final RemoteImageLoader iconLoader = new RemoteImageLoader() {
|
||||
@Override
|
||||
protected @NotNull Task<Image> createLoadTask(@NotNull URI uri) {
|
||||
return FXUtils.getRemoteImageTask(uri, 80, 80, true, true);
|
||||
}
|
||||
};
|
||||
|
||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||
super(control);
|
||||
@@ -377,6 +374,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
IntegerProperty filterID = new SimpleIntegerProperty(this, "Filter ID", 0);
|
||||
IntegerProperty currentFilterID = new SimpleIntegerProperty(this, "Current Filter ID", -1);
|
||||
EventHandler<ActionEvent> searchAction = e -> {
|
||||
iconLoader.clearInvalidCache();
|
||||
if (currentFilterID.get() != -1 && currentFilterID.get() != filterID.get()) {
|
||||
control.pageOffset.set(0);
|
||||
}
|
||||
@@ -536,7 +534,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
var iconCache = new WeakHashMap<String, WeakReference<CompletableFuture<Image>>>();
|
||||
|
||||
listView.setCellFactory(x -> new FloatListCell<>(listView) {
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final ImageView imageView = new ImageView();
|
||||
@@ -564,66 +562,9 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
if (getSkinnable().shouldDisplayCategory(category))
|
||||
content.addTag(getSkinnable().getLocalizedCategory(category));
|
||||
}
|
||||
loadIcon(dataItem);
|
||||
iconLoader.load(imageView.imageProperty(), dataItem.getIconUrl());
|
||||
}
|
||||
|
||||
private void loadIcon(RemoteMod mod) {
|
||||
if (StringUtils.isBlank(mod.getIconUrl())) {
|
||||
imageView.setImage(null);
|
||||
return;
|
||||
}
|
||||
|
||||
WeakReference<CompletableFuture<Image>> cacheRef = iconCache.get(mod.getIconUrl());
|
||||
CompletableFuture<Image> cache;
|
||||
if (cacheRef != null && (cache = cacheRef.get()) != null) {
|
||||
loadIcon(cache, mod.getIconUrl());
|
||||
return;
|
||||
}
|
||||
|
||||
URI iconUrl = NetworkUtils.toURIOrNull(mod.getIconUrl());
|
||||
if (iconUrl == null) {
|
||||
imageView.setImage(null);
|
||||
return;
|
||||
}
|
||||
|
||||
CompletableFuture<Image> future = new CompletableFuture<>();
|
||||
WeakReference<CompletableFuture<Image>> futureRef = new WeakReference<>(future);
|
||||
iconCache.put(mod.getIconUrl(), futureRef);
|
||||
|
||||
FXUtils.getRemoteImageTask(iconUrl, 80, 80, true, true)
|
||||
.whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
future.complete(result);
|
||||
} else {
|
||||
LOG.warning("Failed to load image from " + iconUrl, exception);
|
||||
future.completeExceptionally(exception);
|
||||
}
|
||||
}).start();
|
||||
loadIcon(future, mod.getIconUrl());
|
||||
}
|
||||
|
||||
private void loadIcon(@NotNull CompletableFuture<Image> future,
|
||||
@NotNull String iconUrl) {
|
||||
Image image;
|
||||
try {
|
||||
image = future.getNow(null);
|
||||
} catch (CancellationException | CompletionException ignored) {
|
||||
imageView.setImage(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (image != null) {
|
||||
imageView.setImage(image);
|
||||
} else {
|
||||
imageView.setImage(null);
|
||||
future.thenAcceptAsync(result -> {
|
||||
RemoteMod item = getItem();
|
||||
if (item != null && iconUrl.equals(item.getIconUrl())) {
|
||||
this.imageView.setImage(result);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -89,9 +89,6 @@ public class GameItem {
|
||||
return new Result(gameVersion.orElse(null), modPackVersion);
|
||||
}, POOL_VERSION_RESOLVE).whenCompleteAsync((result, exception) -> {
|
||||
if (exception == null) {
|
||||
if (result.gameVersion != null) {
|
||||
title.set(result.gameVersion);
|
||||
}
|
||||
if (result.tag != null) {
|
||||
tag.set(result.tag);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,9 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
|
||||
{
|
||||
HBox center = new HBox();
|
||||
center.setMouseTransparent(true);
|
||||
root.setCenter(center);
|
||||
center.setPrefWidth(Region.USE_PREF_SIZE);
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -154,17 +156,21 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
return new GameListSkin();
|
||||
}
|
||||
|
||||
private class GameListSkin extends ToolbarListPageSkin2<GameListItem, GameList> {
|
||||
private class GameListSkin extends ToolbarListPageSkin<GameListItem, GameList> {
|
||||
|
||||
public GameListSkin() {
|
||||
super(GameList.this);
|
||||
this.listView.setCellFactory(listView -> new GameListCell());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Node> initializeToolbar(GameList skinnable) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListCell<GameListItem> createListCell(JFXListView<GameListItem> listView) {
|
||||
return new GameListCell();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
executor.start();
|
||||
}
|
||||
|
||||
private class InstallerListPageSkin extends ToolbarListPageSkin<InstallerListPage> {
|
||||
private class InstallerListPageSkin extends ToolbarListPageSkin<InstallerItem, InstallerListPage> {
|
||||
|
||||
InstallerListPageSkin() {
|
||||
super(InstallerListPage.this);
|
||||
|
||||
@@ -599,7 +599,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
}
|
||||
}
|
||||
|
||||
private final class SchematicsPageSkin extends ToolbarListPageSkin<SchematicsPage> {
|
||||
private final class SchematicsPageSkin extends ToolbarListPageSkin<Item, SchematicsPage> {
|
||||
SchematicsPageSkin() {
|
||||
super(SchematicsPage.this);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
||||
}), i18n("world.backup"), null);
|
||||
}
|
||||
|
||||
private final class WorldBackupsPageSkin extends ToolbarListPageSkin<WorldBackupsPage> {
|
||||
private final class WorldBackupsPageSkin extends ToolbarListPageSkin<BackupInfo, WorldBackupsPage> {
|
||||
|
||||
WorldBackupsPageSkin() {
|
||||
super(WorldBackupsPage.this);
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class WorldListItem extends Control {
|
||||
private final World world;
|
||||
private final Path backupsDir;
|
||||
private final WorldListPage parent;
|
||||
private final Profile profile;
|
||||
private final String id;
|
||||
|
||||
public WorldListItem(WorldListPage parent, World world, Path backupsDir, Profile profile, String id) {
|
||||
this.world = world;
|
||||
this.backupsDir = backupsDir;
|
||||
this.parent = parent;
|
||||
this.profile = profile;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new WorldListItemSkin(this);
|
||||
}
|
||||
|
||||
public World getWorld() {
|
||||
return world;
|
||||
}
|
||||
|
||||
public void export() {
|
||||
WorldManageUIUtils.export(world);
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
WorldManageUIUtils.delete(world, () -> parent.remove(this));
|
||||
}
|
||||
|
||||
public void copy() {
|
||||
WorldManageUIUtils.copyWorld(world, parent::refresh);
|
||||
}
|
||||
|
||||
public void reveal() {
|
||||
FXUtils.openFolder(world.getFile());
|
||||
}
|
||||
|
||||
public void showManagePage() {
|
||||
Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class WorldListItemSkin extends SkinBase<WorldListItem> {
|
||||
|
||||
private final BorderPane root;
|
||||
|
||||
public WorldListItemSkin(WorldListItem skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
World world = skinnable.getWorld();
|
||||
|
||||
root = new BorderPane();
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
root.setPadding(new Insets(8));
|
||||
|
||||
{
|
||||
StackPane left = new StackPane();
|
||||
FXUtils.installSlowTooltip(left, world.getFile().toString());
|
||||
root.setLeft(left);
|
||||
left.setPadding(new Insets(0, 8, 0, 0));
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
left.getChildren().add(imageView);
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
imageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
|
||||
}
|
||||
|
||||
{
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
root.setCenter(item);
|
||||
item.setMouseTransparent(true);
|
||||
if (world.getWorldName() != null)
|
||||
item.setTitle(parseColorEscapes(world.getWorldName()));
|
||||
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
|
||||
|
||||
if (world.getGameVersion() != null)
|
||||
item.addTag(I18n.getDisplayVersion(world.getGameVersion()));
|
||||
if (world.isLocked())
|
||||
item.addTag(i18n("world.locked"));
|
||||
}
|
||||
|
||||
{
|
||||
HBox right = new HBox(8);
|
||||
root.setRight(right);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton btnMore = new JFXButton();
|
||||
right.getChildren().add(btnMore);
|
||||
btnMore.getStyleClass().add("toggle-icon4");
|
||||
btnMore.setGraphic(SVG.MORE_VERT.createIcon());
|
||||
btnMore.setOnAction(event -> showPopupMenu(JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()));
|
||||
}
|
||||
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
container.setOnMouseClicked(event -> {
|
||||
if (event.getClickCount() != 1)
|
||||
return;
|
||||
|
||||
if (event.getButton() == MouseButton.PRIMARY)
|
||||
skinnable.showManagePage();
|
||||
else if (event.getButton() == MouseButton.SECONDARY)
|
||||
showPopupMenu(JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
|
||||
});
|
||||
|
||||
getChildren().setAll(container);
|
||||
}
|
||||
|
||||
// Popup Menu
|
||||
|
||||
public void showPopupMenu(JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(popupMenu);
|
||||
|
||||
WorldListItem item = getSkinnable();
|
||||
World world = item.getWorld();
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
|
||||
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), item::launch, popup);
|
||||
launchItem.setDisable(world.isLocked());
|
||||
popupMenu.getContent().add(launchItem);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), item::generateLaunchScript, popup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
popupMenu.getContent().add(new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), item::showManagePage, popup));
|
||||
|
||||
if (ChunkBaseApp.isSupported(world)) {
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup),
|
||||
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup),
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
|
||||
);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
||||
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
|
||||
() -> ChunkBaseApp.openEndCityFinder(world), popup));
|
||||
}
|
||||
}
|
||||
|
||||
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup);
|
||||
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup);
|
||||
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), item::copy, popup);
|
||||
boolean worldLocked = world.isLocked();
|
||||
Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem)
|
||||
.forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked));
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
exportMenuItem,
|
||||
deleteMenuItem,
|
||||
duplicateMenuItem
|
||||
);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup)
|
||||
);
|
||||
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
|
||||
|
||||
popup.show(root, vPosition, hPosition, initOffsetX, vPosition == JFXPopup.PopupVPosition.TOP ? initOffsetY : -initOffsetY);
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,32 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
@@ -35,15 +50,18 @@ import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class WorldListPage extends ListPageBase<WorldListItem> implements VersionPage.VersionLoadable {
|
||||
public final class WorldListPage extends ListPageBase<World> implements VersionPage.VersionLoadable {
|
||||
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
|
||||
|
||||
private Path savesDir;
|
||||
@@ -51,23 +69,19 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
|
||||
private List<World> worlds;
|
||||
private Profile profile;
|
||||
private String id;
|
||||
private GameVersionNumber gameVersion;
|
||||
|
||||
private int refreshCount = 0;
|
||||
|
||||
public WorldListPage() {
|
||||
FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
|
||||
installWorld(modpacks.get(0));
|
||||
});
|
||||
|
||||
showAll.addListener(e -> {
|
||||
if (worlds != null)
|
||||
itemsProperty().setAll(worlds.stream()
|
||||
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
|
||||
.map(world -> new WorldListItem(this, world, backupsDir, profile, id)).toList());
|
||||
});
|
||||
showAll.addListener(e -> updateWorldList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ToolbarListPageSkin<WorldListPage> createDefaultSkin() {
|
||||
protected Skin<WorldListPage> createDefaultSkin() {
|
||||
return new WorldListPageSkin();
|
||||
}
|
||||
|
||||
@@ -80,33 +94,46 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void remove(WorldListItem item) {
|
||||
itemsProperty().remove(item);
|
||||
private void updateWorldList() {
|
||||
if (worlds == null) {
|
||||
getItems().clear();
|
||||
} else if (showAll.get()) {
|
||||
getItems().setAll(worlds);
|
||||
} else {
|
||||
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null);
|
||||
getItems().setAll(worlds.stream()
|
||||
.filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (profile == null || id == null)
|
||||
return;
|
||||
|
||||
int currentRefresh = ++refreshCount;
|
||||
|
||||
setLoading(true);
|
||||
Task.runAsync(() -> gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null))
|
||||
.thenApplyAsync(unused -> {
|
||||
try (Stream<World> stream = World.getWorlds(savesDir)) {
|
||||
return stream.parallel().collect(Collectors.toList());
|
||||
}
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
worlds = result;
|
||||
setLoading(false);
|
||||
if (exception == null) {
|
||||
itemsProperty().setAll(result.stream()
|
||||
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
|
||||
.map(world -> new WorldListItem(this, world, backupsDir, profile, id))
|
||||
.collect(Collectors.toList()));
|
||||
} else {
|
||||
LOG.warning("Failed to load world list page", exception);
|
||||
}
|
||||
}).start();
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
// Ensure the game version number is parsed
|
||||
profile.getRepository().getGameVersion(id);
|
||||
try (Stream<World> stream = World.getWorlds(savesDir)) {
|
||||
return stream.toList();
|
||||
}
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (refreshCount != currentRefresh) {
|
||||
// A newer refresh task is running, discard this result
|
||||
return;
|
||||
}
|
||||
|
||||
worlds = result;
|
||||
updateWorldList();
|
||||
|
||||
if (exception != null)
|
||||
LOG.warning("Failed to load world list page", exception);
|
||||
|
||||
setLoading(false);
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void add() {
|
||||
@@ -132,8 +159,8 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
|
||||
Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> {
|
||||
Task.runAsync(() -> world.install(savesDir, name))
|
||||
.whenComplete(Schedulers.javafx(), () -> {
|
||||
itemsProperty().add(new WorldListItem(this, new World(savesDir.resolve(name)), backupsDir, profile, id));
|
||||
resolve.run();
|
||||
refresh();
|
||||
}, e -> {
|
||||
if (e instanceof FileAlreadyExistsException)
|
||||
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||
@@ -149,19 +176,39 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
|
||||
}).start();
|
||||
}
|
||||
|
||||
public boolean isShowAll() {
|
||||
return showAll.get();
|
||||
private void showManagePage(World world) {
|
||||
Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
|
||||
}
|
||||
|
||||
public void export(World world) {
|
||||
WorldManageUIUtils.export(world);
|
||||
}
|
||||
|
||||
public void delete(World world) {
|
||||
WorldManageUIUtils.delete(world, this::refresh);
|
||||
}
|
||||
|
||||
public void copy(World world) {
|
||||
WorldManageUIUtils.copyWorld(world, this::refresh);
|
||||
}
|
||||
|
||||
public void reveal(World world) {
|
||||
FXUtils.openFolder(world.getFile());
|
||||
}
|
||||
|
||||
public void launch(World world) {
|
||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript(World world) {
|
||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
|
||||
}
|
||||
|
||||
public BooleanProperty showAllProperty() {
|
||||
return showAll;
|
||||
}
|
||||
|
||||
public void setShowAll(boolean showAll) {
|
||||
this.showAll.set(showAll);
|
||||
}
|
||||
|
||||
private final class WorldListPageSkin extends ToolbarListPageSkin<WorldListPage> {
|
||||
private final class WorldListPageSkin extends ToolbarListPageSkin<World, WorldListPage> {
|
||||
|
||||
WorldListPageSkin() {
|
||||
super(WorldListPage.this);
|
||||
@@ -178,5 +225,162 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
|
||||
createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add),
|
||||
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListCell<World> createListCell(JFXListView<World> listView) {
|
||||
return new WorldListCell(getSkinnable());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WorldListCell extends ListCell<World> {
|
||||
|
||||
private final WorldListPage page;
|
||||
|
||||
private final RipplerContainer graphic;
|
||||
private final ImageView imageView;
|
||||
private final Tooltip leftTooltip;
|
||||
private final TwoLineListItem content;
|
||||
|
||||
public WorldListCell(WorldListPage page) {
|
||||
this.page = page;
|
||||
|
||||
var root = new BorderPane();
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
root.setPadding(new Insets(8));
|
||||
|
||||
{
|
||||
StackPane left = new StackPane();
|
||||
this.leftTooltip = new Tooltip();
|
||||
FXUtils.installSlowTooltip(left, leftTooltip);
|
||||
root.setLeft(left);
|
||||
left.setPadding(new Insets(0, 8, 0, 0));
|
||||
|
||||
this.imageView = new ImageView();
|
||||
left.getChildren().add(imageView);
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
}
|
||||
|
||||
{
|
||||
this.content = new TwoLineListItem();
|
||||
root.setCenter(content);
|
||||
content.setMouseTransparent(true);
|
||||
}
|
||||
|
||||
{
|
||||
HBox right = new HBox(8);
|
||||
root.setRight(right);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton btnMore = new JFXButton();
|
||||
right.getChildren().add(btnMore);
|
||||
btnMore.getStyleClass().add("toggle-icon4");
|
||||
btnMore.setGraphic(SVG.MORE_VERT.createIcon());
|
||||
btnMore.setOnAction(event -> {
|
||||
World world = getItem();
|
||||
if (world != null)
|
||||
showPopupMenu(world, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
|
||||
});
|
||||
}
|
||||
|
||||
this.graphic = new RipplerContainer(root);
|
||||
graphic.setOnMouseClicked(event -> {
|
||||
if (event.getClickCount() != 1)
|
||||
return;
|
||||
|
||||
World world = getItem();
|
||||
if (world == null)
|
||||
return;
|
||||
|
||||
if (event.getButton() == MouseButton.PRIMARY)
|
||||
page.showManagePage(world);
|
||||
else if (event.getButton() == MouseButton.SECONDARY)
|
||||
showPopupMenu(world, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(World world, boolean empty) {
|
||||
super.updateItem(world, empty);
|
||||
|
||||
this.content.getTags().clear();
|
||||
|
||||
if (empty || world == null) {
|
||||
setGraphic(null);
|
||||
imageView.setImage(null);
|
||||
leftTooltip.setText("");
|
||||
content.setTitle("");
|
||||
content.setSubtitle("");
|
||||
} else {
|
||||
imageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
|
||||
leftTooltip.setText(world.getFile().toString());
|
||||
content.setTitle(world.getWorldName() != null ? parseColorEscapes(world.getWorldName()) : "");
|
||||
|
||||
if (world.getGameVersion() != null)
|
||||
content.addTag(I18n.getDisplayVersion(world.getGameVersion()));
|
||||
if (world.isLocked())
|
||||
content.addTag(i18n("world.locked"));
|
||||
|
||||
content.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
|
||||
|
||||
setGraphic(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
// Popup Menu
|
||||
|
||||
public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
|
||||
PopupMenu popupMenu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(popupMenu);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
|
||||
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
|
||||
launchItem.setDisable(world.isLocked());
|
||||
popupMenu.getContent().add(launchItem);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> page.generateLaunchScript(world), popup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
popupMenu.getContent().add(new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), () -> page.showManagePage(world), popup));
|
||||
|
||||
if (ChunkBaseApp.isSupported(world)) {
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup),
|
||||
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup),
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
|
||||
);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
||||
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
|
||||
() -> ChunkBaseApp.openEndCityFinder(world), popup));
|
||||
}
|
||||
}
|
||||
|
||||
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup);
|
||||
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> page.delete(world), popup);
|
||||
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup);
|
||||
boolean worldLocked = world.isLocked();
|
||||
Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem)
|
||||
.forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked));
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
exportMenuItem,
|
||||
deleteMenuItem,
|
||||
duplicateMenuItem
|
||||
);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), () -> page.reveal(world), popup)
|
||||
);
|
||||
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup);
|
||||
popup.show(this, vPosition, hPosition, initOffsetX, vPosition == JFXPopup.PopupVPosition.TOP ? initOffsetY : -initOffsetY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,14 +112,12 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, advancedListItem -> advancedListItem.setDisable(isReadOnly()));
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch_script"), SVG.SCRIPT, this::generateLaunchScript, null);
|
||||
}
|
||||
|
||||
if (ChunkBaseApp.isSupported(world)) {
|
||||
PopupMenu chunkBasePopupMenu = new PopupMenu();
|
||||
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
|
||||
|
||||
|
||||
chunkBasePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), chunkBasePopup),
|
||||
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), chunkBasePopup),
|
||||
@@ -144,6 +142,14 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
PopupMenu managePopupMenu = new PopupMenu();
|
||||
JFXPopup managePopup = new JFXPopup(managePopupMenu);
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), this::launch, managePopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), this::generateLaunchScript, managePopup),
|
||||
new MenuSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
managePopupMenu.getContent().addAll(
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup),
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <[email protected]> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class RemoteImageLoader {
|
||||
private final Map<URI, WeakReference<Image>> cache = new HashMap<>();
|
||||
private final Map<URI, List<WeakReference<WritableValue<Image>>>> pendingRequests = new HashMap<>();
|
||||
private final WeakHashMap<WritableValue<Image>, URI> reverseLookup = new WeakHashMap<>();
|
||||
|
||||
public RemoteImageLoader() {
|
||||
}
|
||||
|
||||
protected @Nullable Image getPlaceholder() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract @NotNull Task<Image> createLoadTask(@NotNull URI uri);
|
||||
|
||||
@FXThread
|
||||
public void load(@NotNull WritableValue<Image> writableValue, String url) {
|
||||
URI uri = NetworkUtils.toURIOrNull(url);
|
||||
if (uri == null) {
|
||||
reverseLookup.remove(writableValue);
|
||||
writableValue.setValue(getPlaceholder());
|
||||
return;
|
||||
}
|
||||
|
||||
WeakReference<Image> reference = cache.get(uri);
|
||||
if (reference != null) {
|
||||
Image image = reference.get();
|
||||
if (image != null) {
|
||||
reverseLookup.remove(writableValue);
|
||||
writableValue.setValue(image);
|
||||
return;
|
||||
}
|
||||
cache.remove(uri);
|
||||
}
|
||||
|
||||
writableValue.setValue(getPlaceholder());
|
||||
|
||||
{
|
||||
List<WeakReference<WritableValue<Image>>> list = pendingRequests.get(uri);
|
||||
if (list != null) {
|
||||
list.add(new WeakReference<>(writableValue));
|
||||
reverseLookup.put(writableValue, uri);
|
||||
return;
|
||||
} else {
|
||||
list = new ArrayList<>(1);
|
||||
list.add(new WeakReference<>(writableValue));
|
||||
pendingRequests.put(uri, list);
|
||||
reverseLookup.put(writableValue, uri);
|
||||
}
|
||||
}
|
||||
|
||||
createLoadTask(uri).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
Image image;
|
||||
if (exception == null) {
|
||||
image = result;
|
||||
} else {
|
||||
LOG.warning("Failed to load image from " + uri, exception);
|
||||
image = getPlaceholder();
|
||||
}
|
||||
|
||||
cache.put(uri, new WeakReference<>(image));
|
||||
List<WeakReference<WritableValue<Image>>> list = pendingRequests.remove(uri);
|
||||
if (list != null) {
|
||||
for (WeakReference<WritableValue<Image>> ref : list) {
|
||||
WritableValue<Image> target = ref.get();
|
||||
if (target != null && uri.equals(reverseLookup.get(target))) {
|
||||
reverseLookup.remove(target);
|
||||
target.setValue(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public void unload(@NotNull WritableValue<Image> writableValue) {
|
||||
reverseLookup.remove(writableValue);
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public void clearInvalidCache() {
|
||||
cache.entrySet().removeIf(entry -> entry.getValue().get() == null);
|
||||
}
|
||||
}
|
||||
@@ -388,31 +388,33 @@
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.installer-item {
|
||||
-fx-padding: 8px;
|
||||
.installer-item-wrapper {
|
||||
-fx-background-color: -monet-surface;
|
||||
-fx-background-radius: 4;
|
||||
-fx-pref-width: 180px;
|
||||
}
|
||||
|
||||
.installer-item:list-item {
|
||||
.installer-item-wrapper:card {
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
|
||||
}
|
||||
|
||||
.installer-item-wrapper .installer-item:list-item {
|
||||
-fx-padding: 8px;
|
||||
-fx-border-color: -monet-outline-variant;
|
||||
-fx-border-width: 0 0 1 0;
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.installer-item:list-item > .installer-item-name {
|
||||
.installer-item-wrapper .installer-item:list-item > .installer-item-name {
|
||||
-fx-pref-width: 80px;
|
||||
}
|
||||
|
||||
.installer-item:list-item > .installer-item-status {
|
||||
.installer-item-wrapper .installer-item:list-item > .installer-item-status {
|
||||
-fx-max-width: infinity;
|
||||
}
|
||||
|
||||
.installer-item:card {
|
||||
-fx-background-color: -monet-surface;
|
||||
-fx-background-radius: 4;
|
||||
.installer-item-wrapper .installer-item:card {
|
||||
-fx-alignment: center;
|
||||
-fx-pref-width: 180px;
|
||||
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
{
|
||||
"__comment__": "THIS FILE IS MACHINE GENERATED! DO NOT EDIT!",
|
||||
"version_latest": "0.4.1",
|
||||
"version_latest": "0.4.2",
|
||||
"packages": {
|
||||
"windows-x86_64": {
|
||||
"hash": "4693fec29bb54a0bb1a1a8a263a935f1673b8f76e84d125974cf259f1912a195beab4dfd04c23cae592cf71b116e82ecd48828b1445ab75c980c8cd79c777d21",
|
||||
"hash": "6a98f524d4f00373696517306af8aa50d01d55ce4eadb27e9e4bc2f882707a0b5f20d5d4c33371d1459dcf5bf144ffed9beb414202d9ccf32b11dbbfcf19d650",
|
||||
"files": {
|
||||
"VCRUNTIME140.DLL": "3d4b24061f72c0e957c7b04a0c4098c94c8f1afb4a7e159850b9939c7210d73398be6f27b5ab85073b4e8c999816e7804fef0f6115c39cd061f4aaeb4dcda8cf",
|
||||
"terracotta-0.4.1-windows-x86_64.exe": "3d29f7deb61b8b87e14032baecad71042340f6259d7ff2822336129714470e1a951cd96ee5d6a8c9312e7cf1bb9bb9d1cbf757cfa44d2b2f214362c32ea03b5b"
|
||||
"terracotta-0.4.2-windows-x86_64.exe": "6e98d1f2380ed22fb5a2dd4aafce6c773e9cf69100c8bb8e49e7d6983756bdb9a31f80e06bcfbe5a2742144fe806d3d687dec54d8f09d87c659341f99dd9fd80"
|
||||
}
|
||||
},
|
||||
"windows-arm64": {
|
||||
"hash": "6c68da53142cc92598a9d3b803f110c77927ee7b481e2f623dfab17cd97fee4a58378e796030529713d3e394355135cc01d5f5d86cef7dbd31bbf8e913993d4c",
|
||||
"hash": "fc1077247014ac0c712469498bde2ef7f6d881d5fcb7bdd5e11ebe20218fed365be19afdb8d453a79d77b729f866058522b910741767f4df947faa891434b463",
|
||||
"files": {
|
||||
"VCRUNTIME140.DLL": "5cb5ce114614101d260f4754c09e8a0dd57e4da885ebb96b91e274326f3e1dd95ed0ade9f542f1922fad0ed025e88a1f368e791e1d01fae69718f0ec3c7b98c8",
|
||||
"terracotta-0.4.1-windows-arm64.exe": "d2cf0f29aac752d322e594da6144bbe2874aabde52b5890a6f04a33b77a377464cbf3367e40de6b046c7226517f48e23fd40e611198fcaa1f30503c36d99b20c"
|
||||
"terracotta-0.4.2-windows-arm64.exe": "30a15c5c53e5817c5a3634532172559327474741d3b2c7ef4e8a30acc6f59cdcf3570bf5f583e3cbe9e2abc8253e977c1abda1e9f36c88c4e99240da257347d0"
|
||||
}
|
||||
},
|
||||
"macos-x86_64": {
|
||||
"hash": "c247ab9023cf47231f3a056ddf70fe823e504c19ce241bf264c0a3cf2981c044b236dc239f430afb2541445d1b61d18898f8af5e1c063f31fb952bdfbea4aff5",
|
||||
"hash": "a762e4b2d6f84e899292b9e3856d009411a516d3c47f54575f843ce082f63dff2baa68ba0faa844b8b64fb12e91017386f15f5e7f975f8ee605bf8d4217cb091",
|
||||
"files": {
|
||||
"terracotta-0.4.1-macos-x86_64": "dd2cde70a950849498425b8b00c17fb80edb8dd9bc0312da4885d97a28a4959d87782bd2202ef7f71bdbf2a92609089585c9e3faf24c10df6150e221e111a764",
|
||||
"terracotta-0.4.1-macos-x86_64.pkg": "6057d5b4ea93da45796277a20ddaea7850e8c66326ded769f20fff74e132b453d6d615d937fc8f1d62b0b36b26961df5afb76535aecf85f25d2160889225ac6d"
|
||||
"terracotta-0.4.2-macos-x86_64": "24efb85390eff88a538ed7e503fb1488e5e622730ca30c741a0e8b4c8f4e8d4868a2f9f38da8de540aeb535af2fd1e41c7081dae9c700e8a1a03b6c540218164",
|
||||
"terracotta-0.4.2-macos-x86_64.pkg": "0f80437061231018ec0f860f875aba02cae9e0b36d21f2db8e99d25948806e1119f7844a4d4acca01d6124d7079718762cdebe3756b005a55632de7029fe0d24"
|
||||
}
|
||||
},
|
||||
"macos-arm64": {
|
||||
"hash": "0ddf48a44ea2563c37707caea8381ad3c69da73dd443d07dd98fe630f4e24ccda6f1fcdc9207a66bf61758b887a7b47186883ccd12bcb6b68387d8d756873f44",
|
||||
"hash": "09e444fea2d9fd19f3e5cb62e29055228345be163924cbd408d947646fafed1012cf48508ee6a155ede3d571e2ffaa72d09ceeb1493c8a60feb05e0699f19ba3",
|
||||
"files": {
|
||||
"terracotta-0.4.1-macos-arm64": "ddc335ee082b4c0323314d482933810fc2d377cfc131a9aa978b54f245f1bed8973139edf7cf94f7ae6c04660dfe18d275b1a81638781e482b4ff585f351eed9",
|
||||
"terracotta-0.4.1-macos-arm64.pkg": "5cbb41a450f080234b66fa264351bd594e3f6ef1599c96f277baa308e34dd26caefa3a34b3d65e972bc20868f2d4a661e8b3910d4b0311a374c6ac680bdccf8f"
|
||||
"terracotta-0.4.2-macos-arm64": "8e59a9d78acd57702dc044d6f2799c6af586b075a262f7d4dbbf0876e1af8d8271e04783c24ff820b801e3b14cd0190ab8403d097f3f2d98b6d911f95ed1e972",
|
||||
"terracotta-0.4.2-macos-arm64.pkg": "b0b72f8883767c359f0700e958bd859dd5932c4674fbdf2a32f7de189fb9f54822d4825a754dc856416c8760338eb8ffca80b40c0a57c608fbe6ae9928888993"
|
||||
}
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"hash": "c00b0622203c1610a8e72bde5230fca6fe56cf1f45dc9bc7e857350f3a050d442c814084758090799b9a5b875730fa666539ee75bec706c05d9338ea208301eb",
|
||||
"hash": "d326ad95815d04568d485b5038e40ffc47ca54292fa0925eee6f5cea014024f901d661708aac2a743037b990882ad82b4d0b7bb03dc3b2fe720dbf0f3efe1c98",
|
||||
"files": {
|
||||
"terracotta-0.4.1-linux-x86_64": "e53a9d8ec085ef7a7816b3da374e5c88fced2cf8319d447be3226422d01f9d7ee2019e403eafe21082135050652b2788b7d9520cc432c8d08931930b99595ed7"
|
||||
"terracotta-0.4.2-linux-x86_64": "fac328ba8957a711b03557bb913940f22d61b76608cd203fdf51024b6f94b19f5bc91c9b8a9fa80baf6968e1e6873c1880fd4cf54a2f8e3c6cf1e6ac161f8d0c"
|
||||
}
|
||||
},
|
||||
"linux-arm64": {
|
||||
"hash": "4d661e1123ca050980960fe2d32b8d93a2c3ba807748569a45f16050fb1b52089bfc64c2dd64aba652dfed0e4ac3fba1ff9518cc3f95598c99fc71d93552b589",
|
||||
"hash": "57c08f48d9535e93ad547d2dfc852d267992cc164a7208b42a2da0a6cbc2f21862f610e02a746b4b67150f4dec26b86a4f96eb9bd2f58d124d5b40ba50c6d55e",
|
||||
"files": {
|
||||
"terracotta-0.4.1-linux-arm64": "d4ccf6ff8c2fac060fecaa3c8368677f9342e283f2435493b3e259e922ee0bb0b08f15a292bf91898800a462336c64d0dee7b73f03c1752e5b0988a05adb3b52"
|
||||
"terracotta-0.4.2-linux-arm64": "d807744c2041c98686e4b505324713badea7a0f31e8810be49ae053a63fb6dfc474ac58d678fb93eea0dd5cccff7372d9ec6135a1046f4b306cad35cd90ecacd"
|
||||
}
|
||||
},
|
||||
"linux-loongarch64": {
|
||||
"hash": "8297bb37617e9e7ce282fc553c5b14c84a900bcff4d025be31fd4a4da8b3943d040afc6143aa17de9a88e5fa29af7254d38db8ae6418ee539c2301632448da09",
|
||||
"hash": "a19f4ad4fa1fd2f3c1c867f8743178ab0524d1634d63e122ffc080623e4f14207ecab58830fa0d888696ef28ce4cfa189e6ffbe92c6e1aa1836652617a96a801",
|
||||
"files": {
|
||||
"terracotta-0.4.1-linux-loongarch64": "ffdf7582d095307b91ddfc3e5d0daa78d434e4e34916d0cdf1520ae74b188fe5a48307047bf2da9a526eb725fe80cf230a93001bc8199d236b9cf28a1beaa6e9"
|
||||
"terracotta-0.4.2-linux-loongarch64": "7d4896b275207def0861e544cf2afc9501c1946f1bc8bb2bad9ef4510dd915eabb63eeedff725e69565cdf5c27c146b52dd75d24115d5141909c0865f89cd7b9"
|
||||
}
|
||||
},
|
||||
"linux-riscv64": {
|
||||
"hash": "092f863885205107525e7ccb0e18977f6fd3018910ca5819772ec741dd8cffee52cc352a44b928bd2ba99ab881adaadff9e3bf4bf283f7384a35fea14becb0b4",
|
||||
"hash": "ef3dfcf3c64dfcaf94adc518f0184d017a32a924f67ef33ffb8128f20669cf177cd345df3be8c00225a275bd44f70657b94fb43c60afeb21238000ac7a7f1721",
|
||||
"files": {
|
||||
"terracotta-0.4.1-linux-riscv64": "a6e5d70ddc433bf804764b69e8c204a6a428ece22b9d8ab713ed339fb81bfa1d29daeb6bdfd62c85ff193396315f96172f4a28925e5a4efc45f7d6fa868782a9"
|
||||
"terracotta-0.4.2-linux-riscv64": "afddc55f16f5cf3103548d13f7a3238964c8899dba8eb0e1fdc9e354da65e91fa4486de4b8680a7a3d9326eacf18c1ddb6b5b1f9459908f35f77b36f769172ea"
|
||||
}
|
||||
},
|
||||
"freebsd-x86_64": {
|
||||
"hash": "288bd7a97b2e2c5fb3c7d8107ed1311bc881fb74cf92da40b6c84156239d0832d2306b74b1e93a683188a5db896a6506326c6a7a4ac0ab2e798fa4f1f00787f0",
|
||||
"hash": "a4d02ebbb1419f4e9265f5b24060432799151a27e0d1871690d2f3a51f40bcfd6d92c176001a15a62caa9bf31ea1b67a7b2da10e3351bfcdd36bfac1377d08d9",
|
||||
"files": {
|
||||
"terracotta-0.4.1-freebsd-x86_64": "c90e7db3c5e5cc8d53d8d10a8bf88899e2c418758c14c3d14bc24efc32a711f76882bb5b5a49db5b0256ead4f22b666139bd2a212bf09036d7e7e64ddec4ec3c"
|
||||
"terracotta-0.4.2-freebsd-x86_64": "a50da02752cef73d21804b26eb02261568ed3053c8fef1074014d50226843810f71e6eda39a53af3bad8e173cec40826631a4c8877af460d14a307f1b0a62519"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+4
-1
@@ -92,7 +92,10 @@ public final class CleanroomInstallTask extends Task<Version> {
|
||||
|
||||
@Override
|
||||
public void postExecute() throws Exception {
|
||||
Files.deleteIfExists(installer);
|
||||
if (remote != null) {
|
||||
Files.deleteIfExists(installer);
|
||||
}
|
||||
|
||||
setResult(task.getResult());
|
||||
}
|
||||
|
||||
|
||||
+15
-18
@@ -35,14 +35,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -125,16 +118,20 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
if (category != null && category.getSelf() instanceof CurseAddon.Category) {
|
||||
categoryId = ((CurseAddon.Category) category.getSelf()).getId();
|
||||
}
|
||||
Response<List<CurseAddon>> response = withApiKey(HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v1/mods/search", mapOf(
|
||||
pair("gameId", "432"),
|
||||
pair("classId", Integer.toString(section)),
|
||||
pair("categoryId", Integer.toString(categoryId)),
|
||||
pair("gameVersion", gameVersion),
|
||||
pair("searchFilter", searchFilter),
|
||||
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
|
||||
pair("sortOrder", toSortOrder(sortOrder)),
|
||||
pair("index", Integer.toString(pageOffset * pageSize)),
|
||||
pair("pageSize", Integer.toString(pageSize)))))))
|
||||
|
||||
var query = new LinkedHashMap<String, String>();
|
||||
query.put("gameId", "432");
|
||||
query.put("classId", Integer.toString(section));
|
||||
if (categoryId != 0)
|
||||
query.put("categoryId", Integer.toString(categoryId));
|
||||
query.put("gameVersion", gameVersion);
|
||||
query.put("searchFilter", searchFilter);
|
||||
query.put("sortField", Integer.toString(toModsSearchSortField(sortType)));
|
||||
query.put("sortOrder", toSortOrder(sortOrder));
|
||||
query.put("index", Integer.toString(pageOffset * pageSize));
|
||||
query.put("pageSize", Integer.toString(pageSize));
|
||||
|
||||
Response<List<CurseAddon>> response = withApiKey(HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v1/mods/search", query))))
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.class)));
|
||||
if (searchFilter.isEmpty()) {
|
||||
return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize));
|
||||
|
||||
@@ -95,6 +95,27 @@ public final class NetworkUtils {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String withQuery(String baseUrl, List<Pair<String, String>> params) {
|
||||
StringBuilder sb = new StringBuilder(baseUrl);
|
||||
boolean first = true;
|
||||
for (Pair<String, String> param : params) {
|
||||
if (param.getValue() == null)
|
||||
continue;
|
||||
if (first) {
|
||||
if (!baseUrl.isEmpty()) {
|
||||
sb.append('?');
|
||||
}
|
||||
first = false;
|
||||
} else {
|
||||
sb.append(PARAMETER_SEPARATOR);
|
||||
}
|
||||
sb.append(encodeURL(param.getKey()));
|
||||
sb.append(NAME_VALUE_SEPARATOR);
|
||||
sb.append(encodeURL(param.getValue()));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static List<URI> withQuery(List<URI> list, Map<String, String> params) {
|
||||
return list.stream().map(uri -> URI.create(withQuery(uri.toString(), params))).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ public final class Unzipper {
|
||||
}
|
||||
}
|
||||
|
||||
if (entryCount == 0 && !terminateIfSubDirectoryNotExists) {
|
||||
if (entryCount == 0 && !"/".equals(subDirectory) && !terminateIfSubDirectoryNotExists) {
|
||||
throw new NoSuchFileException("Subdirectory " + subDirectory + " does not exist in the zip file.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -795,7 +795,8 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
|
||||
} else if (version instanceof Release release) {
|
||||
currentRelease = release;
|
||||
|
||||
if (currentRelease.eaType == Release.ReleaseType.GA) {
|
||||
if (currentRelease.eaType == Release.ReleaseType.GA
|
||||
&& currentRelease.additional == Release.Additional.NONE) {
|
||||
defaultGameVersions.addFirst(currentRelease.value);
|
||||
}
|
||||
} else if (version instanceof Special special) {
|
||||
|
||||
@@ -18,7 +18,7 @@ pci-ids = "0.4.0"
|
||||
java-info = "1.0"
|
||||
authlib-injector = "1.2.7"
|
||||
monet-fx = "0.4.0"
|
||||
terracotta = "0.4.1"
|
||||
terracotta = "0.4.2"
|
||||
|
||||
# testing
|
||||
junit = "6.0.1"
|
||||
|
||||
Reference in new issue
Block a user