Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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();
|
||||
|
||||
@@ -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 <huanghongxun2008@126.com> 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 <huanghongxun2008@126.com> 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 <huanghongxun2008@126.com> 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 <huanghongxun2008@126.com> 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);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
|
||||
+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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user