Compare commits
112
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f921370c21 | ||
|
|
61774c2efd | ||
|
|
4c209d1eb6 | ||
|
|
a1c9f1be7d | ||
|
|
3d1bd96d56 | ||
|
|
d48a3c2109 | ||
|
|
a0f9ba1976 | ||
|
|
b8df46ef87 | ||
|
|
0c04a013c8 | ||
|
|
3cf0acc505 | ||
|
|
3be5ea409b | ||
|
|
42f03411de | ||
|
|
941e7cd858 | ||
|
|
61c1b041cd | ||
|
|
7a48be5364 | ||
|
|
7d76fea48d | ||
|
|
600bd6e9ee | ||
|
|
ccc64db805 | ||
|
|
ea407be5f0 | ||
|
|
9b9471da49 | ||
|
|
b1402d3821 | ||
|
|
62f0d0a1c0 | ||
|
|
2865f2ffca | ||
|
|
3860e68caa | ||
|
|
74d0a89820 | ||
|
|
91e4c85a28 | ||
|
|
a05b24cfd7 | ||
|
|
72e3925696 | ||
|
|
9ea419c05b | ||
|
|
3dc1631207 | ||
|
|
ab5ef6e0ff | ||
|
|
61113d5c08 | ||
|
|
d73d7330eb | ||
|
|
45a85e006b | ||
|
|
06cce5259b | ||
|
|
3e393982e0 | ||
|
|
db2e2d89ee | ||
|
|
a3d9e29350 | ||
|
|
682d88df02 | ||
|
|
b5defd31ed | ||
|
|
553e67bda7 | ||
|
|
95bb669ffc | ||
|
|
505752476c | ||
|
|
c723acfe30 | ||
|
|
2fd7076ccf | ||
|
|
0705465ba4 | ||
|
|
7a4649f0cc | ||
|
|
8b8a62ccf1 | ||
|
|
243f3e7371 | ||
|
|
4ad368560c | ||
|
|
c6c302e3d6 | ||
|
|
686f59e21b | ||
|
|
f1bd4bffb7 | ||
|
|
18a8556997 | ||
|
|
f10c842aa4 | ||
|
|
74d379a09c | ||
|
|
3dd1d5bd62 | ||
|
|
3247b77410 | ||
|
|
ce9194057a | ||
|
|
04cf5eca59 | ||
|
|
86d65e96ec | ||
|
|
1262a760b8 | ||
|
|
acd41e5885 | ||
|
|
f22568d3e8 | ||
|
|
11d182f907 | ||
|
|
e25e58cfad | ||
|
|
2e4cc76d6a | ||
|
|
02e9d807cd | ||
|
|
8923be50ec | ||
|
|
a07c0d9746 | ||
|
|
e56c4643de | ||
|
|
fb942d1099 | ||
|
|
3842b6df32 | ||
|
|
fadd9580a2 | ||
|
|
110a58b3cd | ||
|
|
5a25ba6663 | ||
|
|
0013a52a5c | ||
|
|
05d37e3911 | ||
|
|
bc55da0720 | ||
|
|
57a582e2cb | ||
|
|
7c0faf2143 | ||
|
|
fe0515ed28 | ||
|
|
0c657ef382 | ||
|
|
4ce6588f0c | ||
|
|
d083ff9048 | ||
|
|
54f9902849 | ||
|
|
2e572d2b02 | ||
|
|
ac9f337d4e | ||
|
|
078c2ad98b | ||
|
|
830d9a05e7 | ||
|
|
7dfeebff39 | ||
|
|
05a414c93f | ||
|
|
16a432125b | ||
|
|
573e25ecdf | ||
|
|
821cc6d768 | ||
|
|
c13b5e15d4 | ||
|
|
3eea76784d | ||
|
|
22c66b2790 | ||
|
|
fcabe9ca36 | ||
|
|
2af3c8f714 | ||
|
|
253440844e | ||
|
|
cc87f6cdd1 | ||
|
|
a77ef95ce4 | ||
|
|
7fb7b00836 | ||
|
|
3252de2f3f | ||
|
|
9b0b975490 | ||
|
|
449acbc7dc | ||
|
|
1c82e4e1e1 | ||
|
|
77cdc2108f | ||
|
|
cc092f535b | ||
|
|
13930f8dcc | ||
|
|
ebad1f67ce |
No files matched your search
@@ -11,8 +11,9 @@ concurrency:
|
||||
jobs:
|
||||
mirror:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
include:
|
||||
- name: Gitee
|
||||
repo: gitee.com/huanghongxun/HMCL
|
||||
user: 'hmcl-sync'
|
||||
@@ -21,12 +22,12 @@ jobs:
|
||||
repo: cnb.cool/HMCL-dev/HMCL
|
||||
user: 'cnb'
|
||||
token: 'CNB_SYNC_TOKEN'
|
||||
name: Mirror to ${{ matrix.target.name }}
|
||||
name: Mirror to ${{ matrix.name }}
|
||||
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror GitHub to ${{ matrix.target.name }}
|
||||
- name: Mirror GitHub to ${{ matrix.name }}
|
||||
run: |
|
||||
git clone --mirror "https://github.com/$GITHUB_REPOSITORY.git" -- repo
|
||||
git clone --mirror "https://github.com/${{ github.repository }}.git" -- repo
|
||||
cd repo
|
||||
git push -f --prune "https://${{ matrix.target.user }}:${{ secrets[matrix.target.token] }}@${{ matrix.target.repo }}.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
|
||||
git push -f --prune "https://${{ matrix.user }}:${{ secrets[matrix.token] }}@${{ matrix.repo }}.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
|
||||
@@ -1,3 +1,4 @@
|
||||
import org.jackhuang.hmcl.gradle.TerracottaConfigUpgradeTask
|
||||
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
||||
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||
@@ -240,6 +241,11 @@ tasks.processResources {
|
||||
from(upsideDownTranslate.map { it.outputFile })
|
||||
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
||||
}
|
||||
|
||||
inputs.property("terracotta_version", libs.versions.terracotta)
|
||||
doLast {
|
||||
upgradeTerracottaConfig.get().checkValid()
|
||||
}
|
||||
}
|
||||
|
||||
val makeExecutables by tasks.registering {
|
||||
@@ -362,6 +368,26 @@ tasks.register<JavaExec>("run") {
|
||||
}
|
||||
}
|
||||
|
||||
// terracotta
|
||||
|
||||
val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgradeTerracottaConfig") {
|
||||
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
|
||||
val source = layout.projectDirectory.file("terracotta-template.json");
|
||||
|
||||
classifiers.set(listOf(
|
||||
"windows-x86_64", "windows-arm64",
|
||||
"macos-x86_64", "macos-arm64",
|
||||
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
|
||||
"freebsd-x86_64"
|
||||
))
|
||||
|
||||
version.set(libs.versions.terracotta)
|
||||
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")
|
||||
|
||||
templateFile.set(source)
|
||||
outputFile.set(destination)
|
||||
}
|
||||
|
||||
// Check Translations
|
||||
|
||||
tasks.register<CheckTranslations>("checkTranslations") {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.converters.ButtonTypeConverter;
|
||||
import com.jfoenix.skins.JFXButtonSkin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleableProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Labeled;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
public class JFXButton extends Button {
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-button";
|
||||
|
||||
private List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
public JFXButton() {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public JFXButton(String text) {
|
||||
super(text);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public JFXButton(String text, Node graphic) {
|
||||
super(text, graphic);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXButtonSkin(this);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Paint> ripplerFill = new SimpleObjectProperty<>(this, "ripplerFill", null);
|
||||
|
||||
public final ObjectProperty<Paint> ripplerFillProperty() {
|
||||
return this.ripplerFill;
|
||||
}
|
||||
|
||||
public final Paint getRipplerFill() {
|
||||
return this.ripplerFillProperty().get();
|
||||
}
|
||||
|
||||
public final void setRipplerFill(Paint ripplerFill) {
|
||||
this.ripplerFillProperty().set(ripplerFill);
|
||||
}
|
||||
|
||||
private final StyleableObjectProperty<ButtonType> buttonType = new SimpleStyleableObjectProperty<>(
|
||||
JFXButton.StyleableProperties.BUTTON_TYPE, this, "buttonType", JFXButton.ButtonType.FLAT);
|
||||
|
||||
public ButtonType getButtonType() {
|
||||
return this.buttonType == null ? JFXButton.ButtonType.FLAT : this.buttonType.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<ButtonType> buttonTypeProperty() {
|
||||
return this.buttonType;
|
||||
}
|
||||
|
||||
public void setButtonType(ButtonType type) {
|
||||
this.buttonType.set(type);
|
||||
}
|
||||
|
||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||
if (this.STYLEABLES == null) {
|
||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
styleables.addAll(getClassCssMetaData());
|
||||
styleables.addAll(Labeled.getClassCssMetaData());
|
||||
this.STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
|
||||
return this.STYLEABLES;
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return JFXButton.StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
this.setNeedsLayout(false);
|
||||
}
|
||||
|
||||
public enum ButtonType {
|
||||
FLAT,
|
||||
RAISED;
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXButton, ButtonType> BUTTON_TYPE;
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
BUTTON_TYPE = new CssMetaData<>("-jfx-button-type", ButtonTypeConverter.getInstance(), JFXButton.ButtonType.FLAT) {
|
||||
public boolean isSettable(JFXButton control) {
|
||||
return control.buttonType == null || !control.buttonType.isBound();
|
||||
}
|
||||
|
||||
public StyleableProperty<ButtonType> getStyleableProperty(JFXButton control) {
|
||||
return control.buttonTypeProperty();
|
||||
}
|
||||
};
|
||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
|
||||
Collections.addAll(styleables, BUTTON_TYPE);
|
||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXPopupSkin;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.PopupControl;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/// JFXPopup is the material design implementation of a popup.
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 2.0
|
||||
/// @since 2017-03-01
|
||||
@DefaultProperty(value = "popupContent")
|
||||
public class JFXPopup extends PopupControl {
|
||||
|
||||
public enum PopupHPosition {
|
||||
RIGHT, LEFT
|
||||
}
|
||||
|
||||
public enum PopupVPosition {
|
||||
TOP, BOTTOM
|
||||
}
|
||||
|
||||
/// Creates empty popup.
|
||||
public JFXPopup() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/// creates popup with a specified container and content
|
||||
///
|
||||
/// @param content the node that will be shown in the popup
|
||||
public JFXPopup(Region content) {
|
||||
setPopupContent(content);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.setAutoFix(false);
|
||||
this.setAutoHide(true);
|
||||
this.setHideOnEscape(true);
|
||||
this.setConsumeAutoHidingEvents(false);
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new JFXPopupSkin(this);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Setters / Getters *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private final ObjectProperty<Region> popupContent = new SimpleObjectProperty<>(new Pane());
|
||||
|
||||
public final ObjectProperty<Region> popupContentProperty() {
|
||||
return this.popupContent;
|
||||
}
|
||||
|
||||
public final Region getPopupContent() {
|
||||
return this.popupContentProperty().get();
|
||||
}
|
||||
|
||||
public final void setPopupContent(final Region popupContent) {
|
||||
this.popupContentProperty().set(popupContent);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Public API *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// show the popup using the default position
|
||||
public void show(Node node) {
|
||||
this.show(node, PopupVPosition.TOP, PopupHPosition.LEFT, 0, 0);
|
||||
}
|
||||
|
||||
/// show the popup according to the specified position
|
||||
///
|
||||
/// @param vAlign can be TOP/BOTTOM
|
||||
/// @param hAlign can be LEFT/RIGHT
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign) {
|
||||
this.show(node, vAlign, hAlign, 0, 0);
|
||||
}
|
||||
|
||||
/// show the popup according to the specified position with a certain offset
|
||||
///
|
||||
/// @param vAlign can be TOP/BOTTOM
|
||||
/// @param hAlign can be LEFT/RIGHT
|
||||
/// @param initOffsetX on the x-axis
|
||||
/// @param initOffsetY on the y-axis
|
||||
public void show(Node node, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
|
||||
if (!isShowing()) {
|
||||
if (node.getScene() == null || node.getScene().getWindow() == null) {
|
||||
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
|
||||
}
|
||||
Window parent = node.getScene().getWindow();
|
||||
final Point2D origin = node.localToScene(0, 0);
|
||||
final double anchorX = parent.getX() + origin.getX()
|
||||
+ node.getScene().getX() + (hAlign == PopupHPosition.RIGHT ? ((Region) node).getWidth() : 0);
|
||||
final double anchorY = parent.getY() + origin.getY()
|
||||
+ node.getScene()
|
||||
.getY() + (vAlign == PopupVPosition.BOTTOM ? ((Region) node).getHeight() : 0);
|
||||
this.show(parent, anchorX, anchorY);
|
||||
((JFXPopupSkin) getSkin()).reset(vAlign, hAlign, initOffsetX, initOffsetY);
|
||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
||||
}
|
||||
}
|
||||
|
||||
public void show(Window window, double x, double y, PopupVPosition vAlign, PopupHPosition hAlign, double initOffsetX, double initOffsetY) {
|
||||
if (!isShowing()) {
|
||||
if (window == null) {
|
||||
throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window.");
|
||||
}
|
||||
Window parent = window;
|
||||
final double anchorX = parent.getX() + x + initOffsetX;
|
||||
final double anchorY = parent.getY() + y + initOffsetY;
|
||||
this.show(parent, anchorX, anchorY);
|
||||
((JFXPopupSkin) getSkin()).reset(vAlign, hAlign, initOffsetX, initOffsetY);
|
||||
Platform.runLater(() -> ((JFXPopupSkin) getSkin()).animate());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
super.hide();
|
||||
((JFXPopupSkin) getSkin()).init();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// Initialize the style class to 'jfx-popup'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-popup";
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import javafx.geometry.Bounds;
|
||||
import javafx.scene.CacheHint;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Region;
|
||||
@@ -77,14 +78,13 @@ public class JFXRippler extends StackPane {
|
||||
protected Node control;
|
||||
|
||||
protected static final double RIPPLE_MAX_RADIUS = 300;
|
||||
|
||||
private boolean enabled = true;
|
||||
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);
|
||||
@@ -176,10 +176,6 @@ public class JFXRippler extends StackPane {
|
||||
return control;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enable) {
|
||||
this.enabled = enable;
|
||||
}
|
||||
|
||||
// methods that can be changed by extending the rippler class
|
||||
|
||||
/// generate the clipping mask
|
||||
@@ -255,10 +251,15 @@ public class JFXRippler extends StackPane {
|
||||
if (getChildren().contains(control)) {
|
||||
control.boundsInParentProperty().addListener(observable -> resetRippler());
|
||||
}
|
||||
control.addEventHandler(MouseEvent.MOUSE_PRESSED,
|
||||
(event) -> createRipple(event.getX(), event.getY()));
|
||||
control.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY)
|
||||
createRipple(event.getX(), event.getY());
|
||||
});
|
||||
// create fade out transition for the ripple
|
||||
control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> releaseRipple());
|
||||
control.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY)
|
||||
releaseRipple();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,25 +364,23 @@ public class JFXRippler extends StackPane {
|
||||
}
|
||||
|
||||
void createRipple() {
|
||||
if (enabled) {
|
||||
if (!generating.getAndSet(true)) {
|
||||
// create overlay once then change its color later
|
||||
createOverlay();
|
||||
if (this.getClip() == null || (getChildren().size() == 1 && !cacheRipplerClip) || resetClip) {
|
||||
this.setClip(getMask());
|
||||
}
|
||||
this.resetClip = false;
|
||||
|
||||
// create the ripple effect
|
||||
final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
|
||||
getChildren().add(ripple);
|
||||
ripplesQueue.add(ripple);
|
||||
|
||||
// animate the ripple
|
||||
overlayRect.outAnimation.stop();
|
||||
overlayRect.inAnimation.play();
|
||||
ripple.inAnimation.play();
|
||||
if (!generating.getAndSet(true)) {
|
||||
// create overlay once then change its color later
|
||||
createOverlay();
|
||||
if (this.getClip() == null || (getChildren().size() == 1 && !cacheRipplerClip) || resetClip) {
|
||||
this.setClip(getMask());
|
||||
}
|
||||
this.resetClip = false;
|
||||
|
||||
// create the ripple effect
|
||||
final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
|
||||
getChildren().add(ripple);
|
||||
ripplesQueue.add(ripple);
|
||||
|
||||
// animate the ripple
|
||||
overlayRect.outAnimation.stop();
|
||||
overlayRect.inAnimation.play();
|
||||
ripple.inAnimation.play();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,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);
|
||||
@@ -484,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;
|
||||
@@ -497,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -585,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
|
||||
*/
|
||||
@@ -735,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();
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2016 JFoenix
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.jfoenix.effects;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* it will create a shadow effect for a given node and a specified depth level.
|
||||
* depth levels are {0,1,2,3,4,5}
|
||||
*
|
||||
* @author Shadi Shaheen
|
||||
* @version 1.0
|
||||
* @since 2016-03-09
|
||||
*/
|
||||
public final class JFXDepthManager {
|
||||
private JFXDepthManager() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
private static final DropShadow[] depth = new DropShadow[] {
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.TRANSPARENT, 0, 0, 0, 0),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.12), 4, 0, 0, 1),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.16), 6, 0, 0, 2),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.20), 10, 0, 0, 3),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.24), 14, 0, 0, 4),
|
||||
new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.28), 20, 0, 0, 6)
|
||||
};
|
||||
|
||||
/**
|
||||
* this method is used to add shadow effect to the node,
|
||||
* however the shadow is not real ( gets affected with node transformations)
|
||||
* <p>
|
||||
* use {@link #createMaterialNode(Node, int)} instead to generate a real shadow
|
||||
*/
|
||||
public static void setDepth(Node control, int level) {
|
||||
level = level < 0 ? 0 : level;
|
||||
level = level > 5 ? 5 : level;
|
||||
control.setEffect(new DropShadow(BlurType.GAUSSIAN,
|
||||
depth[level].getColor(),
|
||||
depth[level].getRadius(),
|
||||
depth[level].getSpread(),
|
||||
depth[level].getOffsetX(),
|
||||
depth[level].getOffsetY()));
|
||||
}
|
||||
|
||||
public static int getLevels() {
|
||||
return depth.length;
|
||||
}
|
||||
|
||||
public static DropShadow getShadowAt(int level) {
|
||||
return depth[level];
|
||||
}
|
||||
|
||||
/**
|
||||
* this method will generate a new container node that prevent
|
||||
* control transformation to be applied to the shadow effect
|
||||
* (which makes it looks as a real shadow)
|
||||
*/
|
||||
public static Node createMaterialNode(Node control, int level) {
|
||||
Node container = new Pane(control) {
|
||||
@Override
|
||||
protected double computeMaxWidth(double height) {
|
||||
return computePrefWidth(height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxHeight(double width) {
|
||||
return computePrefHeight(width);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height) {
|
||||
return control.prefWidth(height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width) {
|
||||
return control.prefHeight(width);
|
||||
}
|
||||
};
|
||||
container.getStyleClass().add("depth-container");
|
||||
container.setPickOnBounds(false);
|
||||
level = level < 0 ? 0 : level;
|
||||
level = level > 5 ? 5 : level;
|
||||
container.setEffect(new DropShadow(BlurType.GAUSSIAN,
|
||||
depth[level].getColor(),
|
||||
depth[level].getRadius(),
|
||||
depth[level].getSpread(),
|
||||
depth[level].getOffsetX(),
|
||||
depth[level].getOffsetY()));
|
||||
return container;
|
||||
}
|
||||
|
||||
public static void pop(Node control) {
|
||||
control.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5, 0.05, 0, 1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.adapters.skins.ButtonSkin;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Shape;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class JFXButtonSkin extends ButtonSkin {
|
||||
private final StackPane buttonContainer = new StackPane();
|
||||
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
|
||||
protected Node getMask() {
|
||||
StackPane mask = new StackPane();
|
||||
mask.shapeProperty().bind(JFXButtonSkin.this.buttonContainer.shapeProperty());
|
||||
mask.backgroundProperty().bind(Bindings.createObjectBinding(() -> new Background(
|
||||
new BackgroundFill(Color.WHITE,
|
||||
JFXButtonSkin.this.buttonContainer.backgroundProperty().get() != null && !JFXButtonSkin.this.buttonContainer.getBackground().getFills().isEmpty()
|
||||
? JFXButtonSkin.this.buttonContainer.getBackground().getFills().get(0).getRadii()
|
||||
: JFXButtonSkin.this.defaultRadii, JFXButtonSkin.this.buttonContainer.backgroundProperty().get() != null && !JFXButtonSkin.this.buttonContainer.getBackground().getFills().isEmpty() ? JFXButtonSkin.this.buttonContainer.getBackground().getFills().get(0).getInsets() : Insets.EMPTY)),
|
||||
JFXButtonSkin.this.buttonContainer.backgroundProperty()));
|
||||
mask.resize(JFXButtonSkin.this.buttonContainer.getWidth() - JFXButtonSkin.this.buttonContainer.snappedRightInset() - JFXButtonSkin.this.buttonContainer.snappedLeftInset(), JFXButtonSkin.this.buttonContainer.getHeight() - JFXButtonSkin.this.buttonContainer.snappedBottomInset() - JFXButtonSkin.this.buttonContainer.snappedTopInset());
|
||||
return mask;
|
||||
}
|
||||
|
||||
private void initListeners() {
|
||||
this.ripplerPane.setOnMousePressed((event) -> {
|
||||
if (JFXButtonSkin.this.releaseManualRippler != null) {
|
||||
JFXButtonSkin.this.releaseManualRippler.run();
|
||||
}
|
||||
|
||||
JFXButtonSkin.this.releaseManualRippler = null;
|
||||
this.createRipple(event.getX(), event.getY());
|
||||
});
|
||||
}
|
||||
};
|
||||
private Transition clickedAnimation;
|
||||
private final CornerRadii defaultRadii = new CornerRadii(3.0);
|
||||
private boolean invalid = true;
|
||||
private Runnable releaseManualRippler = null;
|
||||
|
||||
public JFXButtonSkin(JFXButton button) {
|
||||
super(button);
|
||||
this.getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
this.releaseManualRippler = this.buttonRippler.createManualRipple();
|
||||
if (this.clickedAnimation != null) {
|
||||
this.clickedAnimation.setRate(1.0);
|
||||
this.clickedAnimation.play();
|
||||
}
|
||||
} else {
|
||||
if (this.releaseManualRippler != null) {
|
||||
this.releaseManualRippler.run();
|
||||
}
|
||||
|
||||
if (this.clickedAnimation != null) {
|
||||
this.clickedAnimation.setRate(-1.0);
|
||||
this.clickedAnimation.play();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
this.buttonContainer.getChildren().add(this.buttonRippler);
|
||||
button.buttonTypeProperty().addListener((o, oldVal, newVal) -> this.updateButtonType(newVal));
|
||||
button.setOnMousePressed((e) -> {
|
||||
if (this.clickedAnimation != null) {
|
||||
this.clickedAnimation.setRate(1.0F);
|
||||
this.clickedAnimation.play();
|
||||
}
|
||||
});
|
||||
button.setOnMouseReleased((e) -> {
|
||||
if (this.clickedAnimation != null) {
|
||||
this.clickedAnimation.setRate(-1.0F);
|
||||
this.clickedAnimation.play();
|
||||
}
|
||||
});
|
||||
|
||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(button);
|
||||
if (focusVisibleProperty == null) {
|
||||
focusVisibleProperty = button.focusedProperty();
|
||||
}
|
||||
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
if (!this.getSkinnable().isPressed()) {
|
||||
this.buttonRippler.showOverlay();
|
||||
}
|
||||
} else {
|
||||
this.buttonRippler.hideOverlay();
|
||||
}
|
||||
});
|
||||
button.pressedProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.hideOverlay());
|
||||
button.setPickOnBounds(false);
|
||||
this.buttonContainer.setPickOnBounds(false);
|
||||
this.buttonContainer.shapeProperty().bind(this.getSkinnable().shapeProperty());
|
||||
this.buttonContainer.borderProperty().bind(this.getSkinnable().borderProperty());
|
||||
this.buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (button.getBackground() == null || this.isJavaDefaultBackground(button.getBackground()) || this.isJavaDefaultClickedBackground(button.getBackground())) {
|
||||
button.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null)));
|
||||
}
|
||||
|
||||
try {
|
||||
return new Background(new BackgroundFill(this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getFill() : Color.TRANSPARENT, this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getRadii() : this.defaultRadii, Insets.EMPTY));
|
||||
} catch (Exception var3) {
|
||||
return this.getSkinnable().getBackground();
|
||||
}
|
||||
}, this.getSkinnable().backgroundProperty()));
|
||||
button.ripplerFillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
|
||||
if (button.getBackground() == null || this.isJavaDefaultBackground(button.getBackground())) {
|
||||
button.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null)));
|
||||
}
|
||||
|
||||
this.updateButtonType(button.getButtonType());
|
||||
this.updateChildren();
|
||||
}
|
||||
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if (this.buttonContainer != null) {
|
||||
this.getChildren().add(0, this.buttonContainer);
|
||||
}
|
||||
|
||||
for (int i = 1; i < this.getChildren().size(); ++i) {
|
||||
this.getChildren().get(i).setMouseTransparent(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
if (this.invalid) {
|
||||
if (((JFXButton) this.getSkinnable()).getRipplerFill() == null) {
|
||||
for (int i = this.getChildren().size() - 1; i >= 1; --i) {
|
||||
if (this.getChildren().get(i) instanceof Shape shape) {
|
||||
this.buttonRippler.setRipplerFill(shape.getFill());
|
||||
shape.fillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.getChildren().get(i) instanceof Label label) {
|
||||
this.buttonRippler.setRipplerFill(label.getTextFill());
|
||||
label.textFillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.buttonRippler.setRipplerFill(((JFXButton) this.getSkinnable()).getRipplerFill());
|
||||
}
|
||||
|
||||
this.invalid = false;
|
||||
}
|
||||
|
||||
double shift = 1.0F;
|
||||
this.buttonContainer.resizeRelocate(this.getSkinnable().getLayoutBounds().getMinX() - shift, this.getSkinnable().getLayoutBounds().getMinY() - shift, this.getSkinnable().getWidth() + (double) 2.0F * shift, this.getSkinnable().getHeight() + (double) 2.0F * shift);
|
||||
this.layoutLabelInArea(x, y, w, h);
|
||||
}
|
||||
|
||||
private boolean isJavaDefaultBackground(Background background) {
|
||||
try {
|
||||
String firstFill = background.getFills().get(0).getFill().toString();
|
||||
return "0xffffffba".equals(firstFill) || "0xffffffbf".equals(firstFill) || "0xffffff12".equals(firstFill) || "0xffffffbd".equals(firstFill);
|
||||
} catch (Exception var3) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isJavaDefaultClickedBackground(Background background) {
|
||||
try {
|
||||
return "0x039ed3ff".equals(background.getFills().get(0).getFill().toString());
|
||||
} catch (Exception var3) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateButtonType(JFXButton.ButtonType type) {
|
||||
switch (type) {
|
||||
case RAISED -> {
|
||||
JFXDepthManager.setDepth(this.buttonContainer, 2);
|
||||
this.clickedAnimation = new ButtonClickTransition();
|
||||
}
|
||||
case FLAT -> this.buttonContainer.setEffect(null);
|
||||
}
|
||||
}
|
||||
|
||||
private class ButtonClickTransition extends CachedTransition {
|
||||
public ButtonClickTransition() {
|
||||
super(JFXButtonSkin.this.buttonContainer, new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).radiusProperty(), JFXDepthManager.getShadowAt(2).radiusProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).spreadProperty(), JFXDepthManager.getShadowAt(2).spreadProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetXProperty(), JFXDepthManager.getShadowAt(2).offsetXProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetYProperty(), JFXDepthManager.getShadowAt(2).offsetYProperty().get(), Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(Duration.millis(1000.0F),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).radiusProperty(), JFXDepthManager.getShadowAt(5).radiusProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).spreadProperty(), JFXDepthManager.getShadowAt(5).spreadProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetXProperty(), JFXDepthManager.getShadowAt(5).offsetXProperty().get(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(((DropShadow) JFXButtonSkin.this.buttonContainer.getEffect()).offsetYProperty(), JFXDepthManager.getShadowAt(5).offsetYProperty().get(), Interpolator.EASE_BOTH))));
|
||||
this.setCycleDuration(Duration.seconds(0.2));
|
||||
this.setDelay(Duration.seconds(0.0F));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.VPos;
|
||||
@@ -35,6 +36,7 @@ import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
private final StackPane box = new StackPane();
|
||||
@@ -75,7 +77,11 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
this.updateRippleColor();
|
||||
this.playSelectAnimation(newVal);
|
||||
});
|
||||
control.focusedProperty().addListener((o, oldVal, newVal) -> {
|
||||
|
||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
|
||||
if (focusVisibleProperty == null)
|
||||
focusVisibleProperty = control.focusedProperty();
|
||||
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
if (!this.getSkinnable().isPressed()) {
|
||||
this.rippler.showOverlay();
|
||||
|
||||
@@ -20,26 +20,24 @@
|
||||
*/
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.adapters.skins.ListViewSkin;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.skin.ListViewSkin;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4720
|
||||
public class JFXListViewSkin<T> extends ListViewSkin<T> {
|
||||
|
||||
private final VirtualFlow<ListCell<T>> flow;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public JFXListViewSkin(final JFXListView<T> listView) {
|
||||
super(listView);
|
||||
flow = (VirtualFlow<ListCell<T>>) getChildren().get(0);
|
||||
VirtualFlow<ListCell<T>> flow = getVirtualFlow();
|
||||
JFXDepthManager.setDepth(flow, listView.depthProperty().get());
|
||||
listView.depthProperty().addListener((o, oldVal, newVal) -> JFXDepthManager.setDepth(flow, newVal));
|
||||
FXUtils.smoothScrolling(flow);
|
||||
|
||||
if (!Boolean.TRUE.equals(listView.getProperties().get("no-smooth-scrolling"))) {
|
||||
FXUtils.smoothScrolling(flow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,47 +45,4 @@ public class JFXListViewSkin<T> extends ListViewSkin<T> {
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
final int itemsCount = getSkinnable().getItems().size();
|
||||
if (getSkinnable().maxHeightProperty().isBound() || itemsCount <= 0) {
|
||||
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||
}
|
||||
|
||||
final double fixedCellSize = getSkinnable().getFixedCellSize();
|
||||
double computedHeight = fixedCellSize != Region.USE_COMPUTED_SIZE ?
|
||||
fixedCellSize * itemsCount + snapVerticalInsets() : estimateHeight();
|
||||
double height = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||
if (height > computedHeight) {
|
||||
height = computedHeight;
|
||||
}
|
||||
|
||||
if (getSkinnable().getMaxHeight() > 0 && computedHeight > getSkinnable().getMaxHeight()) {
|
||||
return getSkinnable().getMaxHeight();
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
private double estimateHeight() {
|
||||
// compute the border/padding for the list
|
||||
double borderWidth = snapVerticalInsets();
|
||||
// compute the gap between list cells
|
||||
|
||||
JFXListView<T> listview = (JFXListView<T>) getSkinnable();
|
||||
double gap = listview.isExpanded() ? ((JFXListView<T>) getSkinnable()).getVerticalGap() * (getSkinnable().getItems()
|
||||
.size()) : 0;
|
||||
// compute the height of each list cell
|
||||
double cellsHeight = 0;
|
||||
for (int i = 0; i < flow.getCellCount(); i++) {
|
||||
ListCell<T> cell = flow.getCell(i);
|
||||
cellsHeight += cell.getHeight();
|
||||
}
|
||||
return cellsHeight + gap + borderWidth;
|
||||
}
|
||||
|
||||
private double snapVerticalInsets() {
|
||||
return getSkinnable().snappedBottomInset() + getSkinnable().snappedTopInset();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (c) 2016 JFoenix
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXPopup.PopupHPosition;
|
||||
import com.jfoenix.controls.JFXPopup.PopupVPosition;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.animation.*;
|
||||
import javafx.animation.Animation.Status;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/// # Material Design Popup Skin
|
||||
/// TODO: REWORK
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 2.0
|
||||
/// @since 2017-03-01
|
||||
public class JFXPopupSkin implements Skin<JFXPopup> {
|
||||
|
||||
protected JFXPopup control;
|
||||
protected StackPane container = new StackPane();
|
||||
protected Region popupContent;
|
||||
protected Node root;
|
||||
|
||||
private Animation animation;
|
||||
protected Scale scale;
|
||||
|
||||
public JFXPopupSkin(JFXPopup control) {
|
||||
this.control = control;
|
||||
// set scale y to 0.01 instead of 0 to allow layout of the content,
|
||||
// otherwise it will cause exception in traverse engine, when focusing the 1st node
|
||||
scale = new Scale(1.0, 0.01, 0, 0);
|
||||
popupContent = control.getPopupContent();
|
||||
container.getStyleClass().add("jfx-popup-container");
|
||||
container.getChildren().add(popupContent);
|
||||
container.getTransforms().add(scale);
|
||||
container.setOpacity(0);
|
||||
root = JFXDepthManager.createMaterialNode(container, 4);
|
||||
animation = AnimationUtils.isAnimationEnabled() ? getAnimation() : null;
|
||||
}
|
||||
|
||||
public void reset(PopupVPosition vAlign, PopupHPosition hAlign, double offsetX, double offsetY) {
|
||||
// postion the popup according to its animation
|
||||
scale.setPivotX(hAlign == PopupHPosition.RIGHT ? container.getWidth() : 0);
|
||||
scale.setPivotY(vAlign == PopupVPosition.BOTTOM ? container.getHeight() : 0);
|
||||
control.setX(control.getX() + (hAlign == PopupHPosition.RIGHT ? -container.getWidth() + offsetX : offsetX));
|
||||
control.setY(control.getY() + (vAlign == PopupVPosition.BOTTOM ? -container.getHeight() + offsetY : offsetY));
|
||||
}
|
||||
|
||||
public final void animate() {
|
||||
if (animation != null) {
|
||||
if (animation.getStatus() == Status.STOPPED) {
|
||||
container.setOpacity(1);
|
||||
animation.playFromStart();
|
||||
}
|
||||
} else {
|
||||
container.setOpacity(1);
|
||||
popupContent.setOpacity(1);
|
||||
scale.setX(1.0);
|
||||
scale.setY(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JFXPopup getSkinnable() {
|
||||
return control;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getNode() {
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (animation != null) {
|
||||
animation.stop();
|
||||
animation = null;
|
||||
}
|
||||
container = null;
|
||||
control = null;
|
||||
popupContent = null;
|
||||
root = null;
|
||||
}
|
||||
|
||||
protected Animation getAnimation() {
|
||||
Interpolator interpolator = Motion.EASE;
|
||||
return new Timeline(
|
||||
new KeyFrame(
|
||||
Duration.ZERO,
|
||||
new KeyValue(popupContent.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(scale.xProperty(), 0, interpolator),
|
||||
new KeyValue(scale.yProperty(), 0, interpolator)
|
||||
),
|
||||
new KeyFrame(Motion.SHORT4,
|
||||
new KeyValue(popupContent.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(scale.xProperty(), 1, interpolator)
|
||||
),
|
||||
new KeyFrame(Motion.MEDIUM2,
|
||||
new KeyValue(popupContent.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(scale.yProperty(), 1, interpolator)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
if (animation != null)
|
||||
animation.stop();
|
||||
container.setOpacity(0);
|
||||
scale.setX(1.0);
|
||||
scale.setY(0.01);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.VPos;
|
||||
@@ -23,6 +24,7 @@ import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
public class JFXRadioButtonSkin extends RadioButtonSkin {
|
||||
@@ -58,7 +60,12 @@ public class JFXRadioButtonSkin extends RadioButtonSkin {
|
||||
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
|
||||
|
||||
this.updateChildren();
|
||||
control.focusedProperty().addListener((o, oldVal, newVal) -> {
|
||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
|
||||
if (focusVisibleProperty == null) {
|
||||
focusVisibleProperty = control.focusedProperty();
|
||||
}
|
||||
|
||||
focusVisibleProperty.addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
if (!this.getSkinnable().isPressed()) {
|
||||
this.rippler.showOverlay();
|
||||
@@ -66,7 +73,6 @@ public class JFXRadioButtonSkin extends RadioButtonSkin {
|
||||
} else {
|
||||
this.rippler.hideOverlay();
|
||||
}
|
||||
|
||||
});
|
||||
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
|
||||
this.registerChangeListener(control.selectedColorProperty(), ignored -> updateColors());
|
||||
|
||||
@@ -174,7 +174,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
Files.copy(fromJson, toJson);
|
||||
|
||||
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId));
|
||||
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId).setJar(dstId));
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
|
||||
@@ -79,7 +79,7 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
|
||||
private final static class HMCLModpack extends Modpack {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name) {
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
|
||||
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,21 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -43,7 +41,9 @@ public final class LogExporter {
|
||||
private LogExporter() {
|
||||
}
|
||||
|
||||
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
|
||||
public static CompletableFuture<Void> exportLogs(
|
||||
Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript,
|
||||
PathMatcher logMatcher) {
|
||||
Path runDirectory = gameRepository.getRunDirectory(versionId);
|
||||
Path baseDirectory = gameRepository.getBaseDirectory();
|
||||
List<String> versions = new ArrayList<>();
|
||||
@@ -65,10 +65,10 @@ public final class LogExporter {
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Zipper zipper = new Zipper(zipFile)) {
|
||||
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper);
|
||||
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper);
|
||||
processLogs(runDirectory, "*.log", "runDirectory", zipper);
|
||||
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper);
|
||||
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper, logMatcher);
|
||||
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper, logMatcher);
|
||||
processLogs(runDirectory, "*.log", "runDirectory", zipper, logMatcher);
|
||||
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper, logMatcher);
|
||||
|
||||
zipper.putTextFile(LOG.getLogs(), "hmcl.log");
|
||||
zipper.putTextFile(logs, "minecraft.log");
|
||||
@@ -86,14 +86,11 @@ public final class LogExporter {
|
||||
});
|
||||
}
|
||||
|
||||
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper) {
|
||||
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper, PathMatcher logMatcher) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, fileExtension)) {
|
||||
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
|
||||
|
||||
for (Path file : stream) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
|
||||
if (time.toMillis() >= processStartTime) {
|
||||
if (logMatcher == null || logMatcher.matches(file)) {
|
||||
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
|
||||
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -188,7 +188,7 @@ public final class ModpackHelper {
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack) {
|
||||
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack, String iconUrl) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
@@ -208,17 +208,17 @@ public final class ModpackHelper {
|
||||
};
|
||||
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
else
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.javafx(), success, failure)
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
@@ -152,6 +152,12 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
@Override
|
||||
public void openBrowser(String url) throws IOException {
|
||||
lastlyOpenedURL = url;
|
||||
|
||||
try {
|
||||
Thread.sleep(1500);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
|
||||
FXUtils.openLink(url);
|
||||
|
||||
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
||||
|
||||
@@ -414,6 +414,12 @@ public final class Accounts {
|
||||
return i18n("account.failed.no_character");
|
||||
} else if (exception instanceof ServerDisconnectException) {
|
||||
if (exception.getCause() instanceof SSLException) {
|
||||
if (exception.getCause().getMessage() != null && exception.getCause().getMessage().contains("Remote host terminated")) {
|
||||
return i18n("account.failed.connect_authentication_server");
|
||||
}
|
||||
if (exception.getCause().getMessage() != null && (exception.getCause().getMessage().contains("No name matching") || exception.getCause().getMessage().contains("No subject alternative DNS name matching"))) {
|
||||
return i18n("account.failed.dns");
|
||||
}
|
||||
return i18n("account.failed.ssl");
|
||||
} else {
|
||||
return i18n("account.failed.connect_authentication_server");
|
||||
@@ -465,7 +471,7 @@ public final class Accounts {
|
||||
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
|
||||
return i18n("account.methods.microsoft.error.no_character");
|
||||
} else if (exception instanceof MicrosoftService.NoXuiException) {
|
||||
return i18n("account.methods.microsoft.error.add_family_probably");
|
||||
return i18n("account.methods.microsoft.error.add_family");
|
||||
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
|
||||
return i18n("account.methods.microsoft.snapshot");
|
||||
} else if (exception instanceof OAuthAccount.WrongAccountException) {
|
||||
|
||||
@@ -134,7 +134,10 @@ public final class DownloadProviders {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
} else if (exception.getCause() instanceof ArtifactMalformedException) {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException) {
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException && !(exception.getCause().getMessage() != null && exception.getCause().getMessage().contains("Remote host terminated"))) {
|
||||
if (exception.getCause().getMessage() != null && (exception.getCause().getMessage().contains("No name matching") || exception.getCause().getMessage().contains("No subject alternative DNS name matching"))) {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.dns.pollution");
|
||||
}
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -94,31 +95,26 @@ public final class FontManager {
|
||||
return null;
|
||||
});
|
||||
|
||||
private static final ObjectProperty<FontReference> fontProperty;
|
||||
private static final ObjectProperty<FontReference> font = new SimpleObjectProperty<>();
|
||||
|
||||
static {
|
||||
updateFont();
|
||||
LOG.info("Font: " + (font.get() != null ? font.get().family() : "System"));
|
||||
}
|
||||
|
||||
private static void updateFont() {
|
||||
String fontFamily = config().getLauncherFontFamily();
|
||||
if (fontFamily == null)
|
||||
fontFamily = System.getProperty("hmcl.font.override");
|
||||
if (fontFamily == null)
|
||||
fontFamily = System.getenv("HMCL_FONT");
|
||||
|
||||
FontReference fontReference;
|
||||
if (fontFamily == null) {
|
||||
Font defaultFont = DEFAULT_FONT.get();
|
||||
fontReference = defaultFont != null ? new FontReference(defaultFont) : null;
|
||||
} else
|
||||
fontReference = new FontReference(fontFamily);
|
||||
|
||||
fontProperty = new SimpleObjectProperty<>(fontReference);
|
||||
|
||||
LOG.info("Font: " + (fontReference != null ? fontReference.getFamily() : "System"));
|
||||
fontProperty.addListener((obs, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
config().setLauncherFontFamily(newValue.getFamily());
|
||||
else
|
||||
config().setLauncherFontFamily(null);
|
||||
});
|
||||
font.set(defaultFont != null ? new FontReference(defaultFont) : null);
|
||||
} else {
|
||||
font.set(new FontReference(fontFamily));
|
||||
}
|
||||
}
|
||||
|
||||
private static Font tryLoadDefaultFont(Path dir) {
|
||||
@@ -231,64 +227,35 @@ public final class FontManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjectProperty<FontReference> fontProperty() {
|
||||
return fontProperty;
|
||||
public static ReadOnlyObjectProperty<FontReference> fontProperty() {
|
||||
return font;
|
||||
}
|
||||
|
||||
public static FontReference getFont() {
|
||||
return fontProperty.get();
|
||||
}
|
||||
|
||||
public static void setFont(FontReference font) {
|
||||
fontProperty.set(font);
|
||||
return font.get();
|
||||
}
|
||||
|
||||
public static void setFontFamily(String fontFamily) {
|
||||
setFont(fontFamily != null ? new FontReference(fontFamily) : null);
|
||||
config().setLauncherFontFamily(fontFamily);
|
||||
updateFont();
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4072
|
||||
public static final class FontReference {
|
||||
private final @NotNull String family;
|
||||
private final @Nullable String style;
|
||||
public record FontReference(@NotNull String family, @Nullable String style) {
|
||||
public FontReference {
|
||||
Objects.requireNonNull(family);
|
||||
}
|
||||
|
||||
public FontReference(@NotNull String family) {
|
||||
this.family = Objects.requireNonNull(family);
|
||||
this.style = null;
|
||||
this(family, null);
|
||||
}
|
||||
|
||||
public FontReference(@NotNull Font font) {
|
||||
this.family = font.getFamily();
|
||||
this.style = font.getStyle();
|
||||
}
|
||||
|
||||
public @NotNull String getFamily() {
|
||||
return family;
|
||||
}
|
||||
|
||||
public @Nullable String getStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof FontReference))
|
||||
return false;
|
||||
FontReference that = (FontReference) o;
|
||||
return Objects.equals(family, that.family) && Objects.equals(style, that.style);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(family, style);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("FontReference[family='%s', style='%s']", family, style);
|
||||
this(font.getFamily(), font.getStyle());
|
||||
}
|
||||
}
|
||||
|
||||
private FontManager() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,131 +19,109 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jackhuang.hmcl.util.gson.ObservableSetting;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(GlobalConfig.Serializer.class)
|
||||
public final class GlobalConfig implements Observable {
|
||||
@JsonAdapter(GlobalConfig.Adapter.class)
|
||||
public final class GlobalConfig extends ObservableSetting {
|
||||
|
||||
@Nullable
|
||||
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
||||
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
||||
if (loaded == null) {
|
||||
return null;
|
||||
}
|
||||
GlobalConfig instance = new GlobalConfig();
|
||||
PropertyUtils.copyProperties(loaded, instance);
|
||||
instance.unknownFields.putAll(loaded.unknownFields);
|
||||
return instance;
|
||||
return Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
||||
}
|
||||
|
||||
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty logRetention = new SimpleIntegerProperty();
|
||||
|
||||
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
|
||||
|
||||
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
|
||||
|
||||
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
private final Map<String, Object> unknownFields = new HashMap<>();
|
||||
|
||||
private final transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public GlobalConfig() {
|
||||
PropertyUtils.attachListener(this, helper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
helper.removeListener(listener);
|
||||
register();
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
return Config.CONFIG_GSON.toJson(this);
|
||||
}
|
||||
|
||||
public int getAgreementVersion() {
|
||||
return agreementVersion.get();
|
||||
}
|
||||
@SerializedName("agreementVersion")
|
||||
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
public IntegerProperty agreementVersionProperty() {
|
||||
return agreementVersion;
|
||||
}
|
||||
|
||||
public int getAgreementVersion() {
|
||||
return agreementVersion.get();
|
||||
}
|
||||
|
||||
public void setAgreementVersion(int agreementVersion) {
|
||||
this.agreementVersion.set(agreementVersion);
|
||||
}
|
||||
|
||||
public int getTerracottaAgreementVersion() {
|
||||
return terracottaAgreementVersion.get();
|
||||
}
|
||||
@SerializedName("terracottaAgreementVersion")
|
||||
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
public IntegerProperty terracottaAgreementVersionProperty() {
|
||||
return terracottaAgreementVersion;
|
||||
}
|
||||
|
||||
public int getTerracottaAgreementVersion() {
|
||||
return terracottaAgreementVersion.get();
|
||||
}
|
||||
|
||||
public void setTerracottaAgreementVersion(int terracottaAgreementVersion) {
|
||||
this.terracottaAgreementVersion.set(terracottaAgreementVersion);
|
||||
}
|
||||
|
||||
public int getPlatformPromptVersion() {
|
||||
return platformPromptVersion.get();
|
||||
}
|
||||
@SerializedName("platformPromptVersion")
|
||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
public IntegerProperty platformPromptVersionProperty() {
|
||||
return platformPromptVersion;
|
||||
}
|
||||
|
||||
public int getPlatformPromptVersion() {
|
||||
return platformPromptVersion.get();
|
||||
}
|
||||
|
||||
public void setPlatformPromptVersion(int platformPromptVersion) {
|
||||
this.platformPromptVersion.set(platformPromptVersion);
|
||||
}
|
||||
|
||||
public int getLogRetention() {
|
||||
return logRetention.get();
|
||||
}
|
||||
@SerializedName("logRetention")
|
||||
private final IntegerProperty logRetention = new SimpleIntegerProperty(20);
|
||||
|
||||
public IntegerProperty logRetentionProperty() {
|
||||
return logRetention;
|
||||
}
|
||||
|
||||
public int getLogRetention() {
|
||||
return logRetention.get();
|
||||
}
|
||||
|
||||
public void setLogRetention(int logRetention) {
|
||||
this.logRetention.set(logRetention);
|
||||
}
|
||||
|
||||
public boolean isEnableOfflineAccount() {
|
||||
return enableOfflineAccount.get();
|
||||
}
|
||||
@SerializedName("enableOfflineAccount")
|
||||
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
|
||||
|
||||
public BooleanProperty enableOfflineAccountProperty() {
|
||||
return enableOfflineAccount;
|
||||
}
|
||||
|
||||
public boolean isEnableOfflineAccount() {
|
||||
return enableOfflineAccount.get();
|
||||
}
|
||||
|
||||
public void setEnableOfflineAccount(boolean value) {
|
||||
enableOfflineAccount.set(value);
|
||||
}
|
||||
|
||||
@SerializedName("fontAntiAliasing")
|
||||
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
|
||||
|
||||
public StringProperty fontAntiAliasingProperty() {
|
||||
return fontAntiAliasing;
|
||||
}
|
||||
@@ -156,89 +134,24 @@ public final class GlobalConfig implements Observable {
|
||||
this.fontAntiAliasing.set(value);
|
||||
}
|
||||
|
||||
@SerializedName("userJava")
|
||||
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
public ObservableSet<String> getUserJava() {
|
||||
return userJava;
|
||||
}
|
||||
|
||||
@SerializedName("disabledJava")
|
||||
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
public ObservableSet<String> getDisabledJava() {
|
||||
return disabledJava;
|
||||
}
|
||||
|
||||
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||
"agreementVersion",
|
||||
"terracottaAgreementVersion",
|
||||
"platformPromptVersion",
|
||||
"logRetention",
|
||||
"userJava",
|
||||
"disabledJava",
|
||||
"enableOfflineAccount",
|
||||
"fontAntiAliasing"
|
||||
));
|
||||
|
||||
static final class Adapter extends ObservableSetting.Adapter<GlobalConfig> {
|
||||
@Override
|
||||
public JsonElement serialize(GlobalConfig src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null) {
|
||||
return JsonNull.INSTANCE;
|
||||
}
|
||||
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("terracottaAgreementVersion", context.serialize(src.getTerracottaAgreementVersion()));
|
||||
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
|
||||
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
|
||||
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
|
||||
if (src.enableOfflineAccount.get())
|
||||
jsonObject.addProperty("enableOfflineAccount", true);
|
||||
|
||||
if (!src.getUserJava().isEmpty())
|
||||
jsonObject.add("userJava", context.serialize(src.getUserJava()));
|
||||
|
||||
if (!src.getDisabledJava().isEmpty())
|
||||
jsonObject.add("disabledJava", context.serialize(src.getDisabledJava()));
|
||||
|
||||
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
|
||||
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GlobalConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!(json instanceof JsonObject)) return null;
|
||||
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
GlobalConfig config = new GlobalConfig();
|
||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setTerracottaAgreementVersion(Optional.ofNullable(obj.get("terracottaAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
|
||||
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
config.setFontAntiAliasing(Optional.ofNullable(obj.get("fontAntiAliasing")).map(JsonElement::getAsString).orElse(null));
|
||||
|
||||
JsonElement userJava = obj.get("userJava");
|
||||
if (userJava != null && userJava.isJsonArray()) {
|
||||
for (JsonElement element : userJava.getAsJsonArray()) {
|
||||
config.userJava.add(element.getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
JsonElement disabledJava = obj.get("disabledJava");
|
||||
if (disabledJava != null && disabledJava.isJsonArray()) {
|
||||
for (JsonElement element : disabledJava.getAsJsonArray()) {
|
||||
config.disabledJava.add(element.getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
|
||||
if (!knownFields.contains(entry.getKey())) {
|
||||
config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
protected GlobalConfig createInstance() {
|
||||
return new GlobalConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -114,6 +115,8 @@ public final class ProxyManager {
|
||||
config().hasProxyAuthProperty().addListener(updateAuthenticator);
|
||||
config().proxyUserProperty().addListener(updateAuthenticator);
|
||||
config().proxyPassProperty().addListener(updateAuthenticator);
|
||||
|
||||
FetchTask.notifyInitialized();
|
||||
}
|
||||
|
||||
private static abstract class AbstractProxySelector extends ProxySelector {
|
||||
|
||||
@@ -90,11 +90,11 @@ public final class StyleSheets {
|
||||
final String defaultCss = "/assets/css/font.css";
|
||||
final FontManager.FontReference font = FontManager.getFont();
|
||||
|
||||
if (font == null || "System".equals(font.getFamily()))
|
||||
if (font == null || "System".equals(font.family()))
|
||||
return defaultCss;
|
||||
|
||||
String fontFamily = font.getFamily();
|
||||
String style = font.getStyle();
|
||||
String fontFamily = font.family();
|
||||
String style = font.style();
|
||||
String weight = null;
|
||||
String posture = null;
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.terracotta;
|
||||
|
||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class TerracottaBundle {
|
||||
private final Path root;
|
||||
|
||||
private final List<URI> links;
|
||||
|
||||
private final FileDownloadTask.IntegrityCheck hash;
|
||||
|
||||
private final Map<String, FileDownloadTask.IntegrityCheck> files;
|
||||
|
||||
public TerracottaBundle(Path root, List<URI> links, FileDownloadTask.IntegrityCheck hash, Map<String, FileDownloadTask.IntegrityCheck> files) {
|
||||
this.root = root;
|
||||
this.links = links;
|
||||
this.hash = hash;
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public Task<Path> download(AbstractTerracottaProvider.DownloadContext context) {
|
||||
return Task.supplyAsync(() -> Files.createTempFile("terracotta-", ".tar.gz"))
|
||||
.thenComposeAsync(Schedulers.javafx(), pkg -> {
|
||||
FileDownloadTask download = new FileDownloadTask(links, pkg, hash) {
|
||||
@Override
|
||||
protected Context getContext(HttpResponse<?> response, boolean checkETag, String bmclapiHash) throws IOException {
|
||||
FetchTask.Context delegate = super.getContext(response, checkETag, bmclapiHash);
|
||||
return new Context() {
|
||||
@Override
|
||||
public void withResult(boolean success) {
|
||||
delegate.withResult(success);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int len) throws IOException {
|
||||
context.checkCancellation();
|
||||
delegate.write(buffer, offset, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (isSuccess()) {
|
||||
context.checkCancellation();
|
||||
}
|
||||
|
||||
delegate.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
context.bindProgress(download.progressProperty());
|
||||
return download.thenSupplyAsync(() -> pkg);
|
||||
});
|
||||
}
|
||||
|
||||
public Task<?> install(Path pkg) {
|
||||
return Task.runAsync(() -> {
|
||||
Files.createDirectories(root);
|
||||
FileUtils.cleanDirectory(root);
|
||||
|
||||
try (TarFileTree tree = TarFileTree.open(pkg)) {
|
||||
for (Map.Entry<String, FileDownloadTask.IntegrityCheck> entry : files.entrySet()) {
|
||||
String file = entry.getKey();
|
||||
FileDownloadTask.IntegrityCheck check = entry.getValue();
|
||||
|
||||
Path path = root.resolve(file);
|
||||
TarArchiveEntry archive = tree.getEntry("/" + file);
|
||||
if (archive == null) {
|
||||
throw new ArtifactMalformedException(String.format("Expecting %s file in terracotta bundle.", file));
|
||||
}
|
||||
|
||||
MessageDigest digest = DigestUtils.getDigest(check.getAlgorithm());
|
||||
try (
|
||||
InputStream is = tree.getInputStream(archive);
|
||||
OutputStream os = new DigestOutputStream(Files.newOutputStream(path), digest)
|
||||
) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
|
||||
String hash = HexFormat.of().formatHex(digest.digest());
|
||||
if (!check.getChecksum().equalsIgnoreCase(hash)) {
|
||||
throw new ChecksumMismatchException(check.getAlgorithm(), check.getChecksum(), hash);
|
||||
}
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case LINUX, MACOS, FREEBSD -> Files.setPosixFilePermissions(path, FileUtils.parsePosixFilePermission(archive.getMode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}).whenComplete(exception -> {
|
||||
if (exception != null) {
|
||||
FileUtils.deleteDirectory(root);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Path locate(String file) {
|
||||
FileDownloadTask.IntegrityCheck check = files.get(file);
|
||||
if (check == null) {
|
||||
throw new AssertionError(String.format("Expecting %s file in terracotta bundle.", file));
|
||||
}
|
||||
return root.resolve(file).toAbsolutePath();
|
||||
}
|
||||
|
||||
public AbstractTerracottaProvider.Status status() throws IOException {
|
||||
if (Files.exists(root) && isLocalBundleValid()) {
|
||||
return AbstractTerracottaProvider.Status.READY;
|
||||
}
|
||||
|
||||
try {
|
||||
if (TerracottaMetadata.hasLegacyVersionFiles()) {
|
||||
return AbstractTerracottaProvider.Status.LEGACY_VERSION;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.LOG.warning("Cannot determine whether legacy versions exist.", e);
|
||||
}
|
||||
return AbstractTerracottaProvider.Status.NOT_EXIST;
|
||||
}
|
||||
|
||||
private boolean isLocalBundleValid() throws IOException { // FIXME: Make control flow clearer.
|
||||
long total = 0;
|
||||
byte[] buffer = new byte[8192];
|
||||
|
||||
for (Map.Entry<String, FileDownloadTask.IntegrityCheck> entry : files.entrySet()) {
|
||||
Path path = root.resolve(entry.getKey());
|
||||
FileDownloadTask.IntegrityCheck check = entry.getValue();
|
||||
if (!Files.isReadable(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MessageDigest digest = DigestUtils.getDigest(check.getAlgorithm());
|
||||
try (InputStream is = new DigestInputStream(Files.newInputStream(path), digest)) {
|
||||
int n;
|
||||
while ((n = is.read(buffer)) >= 0) {
|
||||
total += n;
|
||||
if (total >= 50 * 1024 * 1024) { // >=50MB
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!HexFormat.of().formatHex(digest.digest()).equalsIgnoreCase(check.getChecksum())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
@@ -29,27 +28,32 @@ import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
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;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
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;
|
||||
|
||||
@@ -70,9 +74,10 @@ public final class TerracottaManager {
|
||||
switch (TerracottaMetadata.PROVIDER.status()) {
|
||||
case NOT_EXIST -> setState(new TerracottaState.Uninitialized(false));
|
||||
case LEGACY_VERSION -> setState(new TerracottaState.Uninitialized(true));
|
||||
case READY -> launch(setState(new TerracottaState.Launching()));
|
||||
case READY -> launch(setState(new TerracottaState.Launching()), false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Cannot initialize Terracotta.", e);
|
||||
compareAndSet(TerracottaState.Bootstrap.INSTANCE, new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
});
|
||||
@@ -82,114 +87,145 @@ public final class TerracottaManager {
|
||||
return STATE.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
static {
|
||||
Lang.thread(() -> {
|
||||
while (true) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.PortSpecific portSpecific)) {
|
||||
LockSupport.parkNanos(500_000);
|
||||
private static final Thread DAEMON = Lang.thread(TerracottaManager::runBackground, "Terracotta Background Daemon", true);
|
||||
|
||||
@FXThread // Written in FXThread, read-only on background daemon
|
||||
private static volatile boolean daemonRunning = false;
|
||||
|
||||
private static void runBackground() {
|
||||
final long ACTIVE = TimeUnit.MILLISECONDS.toNanos(500);
|
||||
final long BACKGROUND = TimeUnit.SECONDS.toMillis(15);
|
||||
|
||||
while (true) {
|
||||
if (daemonRunning) {
|
||||
LockSupport.parkNanos(ACTIVE);
|
||||
} else {
|
||||
long deadline = System.currentTimeMillis() + BACKGROUND;
|
||||
do {
|
||||
LockSupport.parkUntil(deadline);
|
||||
} while (!daemonRunning && System.currentTimeMillis() < deadline - 100);
|
||||
}
|
||||
|
||||
if (!(STATE_V.get() instanceof TerracottaState.PortSpecific state)) {
|
||||
continue;
|
||||
}
|
||||
int port = state.port;
|
||||
int index = state instanceof TerracottaState.Ready ready ? ready.index : Integer.MIN_VALUE;
|
||||
|
||||
TerracottaState next;
|
||||
try {
|
||||
TerracottaState.Ready object = HttpRequest.GET(String.format("http://127.0.0.1:%d/state", port))
|
||||
.retry(5)
|
||||
.getJson(TerracottaState.Ready.class);
|
||||
if (object.index <= index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int port = portSpecific.port;
|
||||
int index = state instanceof TerracottaState.Ready ready ? ready.index : Integer.MIN_VALUE;
|
||||
|
||||
TerracottaState next;
|
||||
try {
|
||||
next = new GetTask(URI.create(String.format("http://127.0.0.1:%d/state", port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(jsonString -> {
|
||||
TerracottaState.Ready object = JsonUtils.fromNonNullJson(jsonString, TypeToken.get(TerracottaState.Ready.class));
|
||||
if (object.index <= index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
object.port = port;
|
||||
return object;
|
||||
})
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.run();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Cannot fetch state from Terracotta.", e);
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
|
||||
if (next != null) {
|
||||
compareAndSet(state, next);
|
||||
}
|
||||
|
||||
LockSupport.parkNanos(500_000);
|
||||
object.port = port;
|
||||
next = object;
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Cannot fetch state from Terracotta.", e);
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
}, "Terracotta Background Daemon", true);
|
||||
|
||||
compareAndSet(state, next);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validate(Path file) {
|
||||
return FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
public static TerracottaState.Preparing install(@Nullable Path file) {
|
||||
@FXThread
|
||||
public static void switchDaemon(boolean active) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.Uninitialized ||
|
||||
state instanceof TerracottaState.Preparing preparing && preparing.hasInstallFence() ||
|
||||
state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (file != null && !FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TerracottaState.Preparing preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1));
|
||||
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
return file != null ? TarFileTree.open(file) : null;
|
||||
}).thenComposeAsync(Schedulers.javafx(), tree -> {
|
||||
return getProvider().install(preparing, tree).whenComplete(exception -> {
|
||||
if (tree != null) {
|
||||
tree.close();
|
||||
}
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
});
|
||||
}).whenComplete(exception -> {
|
||||
if (exception == null) {
|
||||
try {
|
||||
TerracottaMetadata.removeLegacyVersionFiles();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to remove legacy terracotta files.", e);
|
||||
}
|
||||
|
||||
TerracottaState.Launching launching = new TerracottaState.Launching();
|
||||
if (compareAndSet(preparing, launching)) {
|
||||
launch(launching);
|
||||
}
|
||||
} else if (exception instanceof CancellationException) {
|
||||
} else if (exception instanceof ITerracottaProvider.ArchiveFileMissingException) {
|
||||
LOG.warning("Cannot install terracotta from local package.", exception);
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
} else if (exception instanceof DownloadException) {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.NETWORK));
|
||||
} else {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
boolean dr = daemonRunning;
|
||||
if (dr != active) {
|
||||
daemonRunning = active;
|
||||
if (active) {
|
||||
LockSupport.unpark(DAEMON);
|
||||
}
|
||||
}).start();
|
||||
|
||||
return compareAndSet(state, preparing) ? preparing : null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ITerracottaProvider getProvider() {
|
||||
ITerracottaProvider provider = TerracottaMetadata.PROVIDER;
|
||||
private static AbstractTerracottaProvider getProvider() {
|
||||
AbstractTerracottaProvider provider = TerracottaMetadata.PROVIDER;
|
||||
if (provider == null) {
|
||||
throw new AssertionError("Terracotta Provider must NOT be null.");
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static TerracottaState recover(@Nullable Path file) {
|
||||
public static boolean isInvalidBundle(Path file) {
|
||||
return !FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static TerracottaState.Preparing download() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.Uninitialized || state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TerracottaState.Preparing preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1), true);
|
||||
|
||||
Task.composeAsync(() -> getProvider().download(preparing))
|
||||
.thenComposeAsync(pkg -> {
|
||||
if (!preparing.requestInstallFence()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getProvider().install(pkg).thenRunAsync(() -> {
|
||||
TerracottaState.Launching launching = new TerracottaState.Launching();
|
||||
if (compareAndSet(preparing, launching)) {
|
||||
launch(launching, true);
|
||||
}
|
||||
});
|
||||
}).whenComplete(exception -> {
|
||||
if (exception instanceof CancellationException) {
|
||||
// no-op
|
||||
} else if (exception instanceof DownloadException) {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.NETWORK));
|
||||
} else {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
}
|
||||
}).start();
|
||||
|
||||
return compareAndSet(state, preparing) ? preparing : null;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static TerracottaState.Preparing install(Path bundle) {
|
||||
FXUtils.checkFxUserThread();
|
||||
if (isInvalidBundle(bundle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
TerracottaState.Preparing preparing;
|
||||
if (state instanceof TerracottaState.Preparing previousPreparing && previousPreparing.requestInstallFence()) {
|
||||
preparing = previousPreparing;
|
||||
} else if (state instanceof TerracottaState.Uninitialized || state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable()) {
|
||||
preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1), false);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
Task.composeAsync(() -> getProvider().install(bundle))
|
||||
.thenRunAsync(() -> {
|
||||
TerracottaState.Launching launching = new TerracottaState.Launching();
|
||||
if (compareAndSet(preparing, launching)) {
|
||||
launch(launching, true);
|
||||
}
|
||||
})
|
||||
.whenComplete(exception -> {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
}).start();
|
||||
|
||||
return state != preparing && compareAndSet(state, preparing) ? preparing : null;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static TerracottaState recover() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
@@ -198,21 +234,23 @@ public final class TerracottaManager {
|
||||
}
|
||||
|
||||
try {
|
||||
// FIXME: A temporary limit has been employed in TerracottaBundle#checkExisting, making
|
||||
// hash check accept 50MB at most. Calling it on JavaFX should be safe.
|
||||
return switch (getProvider().status()) {
|
||||
case NOT_EXIST, LEGACY_VERSION -> install(file);
|
||||
case NOT_EXIST, LEGACY_VERSION -> download();
|
||||
case READY -> {
|
||||
TerracottaState.Launching launching = setState(new TerracottaState.Launching());
|
||||
launch(launching);
|
||||
launch(launching, false);
|
||||
yield launching;
|
||||
}
|
||||
};
|
||||
} catch (NullPointerException | IOException e) {
|
||||
} catch (RuntimeException | IOException e) {
|
||||
LOG.warning("Cannot determine Terracotta state.", e);
|
||||
return setState(new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
}
|
||||
|
||||
private static void launch(TerracottaState.Launching state) {
|
||||
private static void launch(TerracottaState.Launching state, boolean removeLegacy) {
|
||||
Task.supplyAsync(() -> {
|
||||
Path path = Files.createTempDirectory(String.format("hmcl-terracotta-%d", ThreadLocalRandom.current().nextLong())).resolve("http").toAbsolutePath();
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(getProvider().ofCommandLine(path)));
|
||||
@@ -230,7 +268,7 @@ public final class TerracottaManager {
|
||||
if (exitTime == -1) {
|
||||
exitTime = System.currentTimeMillis();
|
||||
} else if (System.currentTimeMillis() - exitTime >= 10000) {
|
||||
throw new IllegalStateException("Process has exited for 10s.");
|
||||
throw new IllegalStateException(String.format("Process has exited for 10s, code = %s", process.getExitCode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,6 +276,9 @@ public final class TerracottaManager {
|
||||
TerracottaState next;
|
||||
if (exception == null) {
|
||||
next = new TerracottaState.Unknown(port);
|
||||
if (removeLegacy) {
|
||||
TerracottaMetadata.removeLegacyVersionFiles();
|
||||
}
|
||||
} else {
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
@@ -272,24 +313,39 @@ public final class TerracottaManager {
|
||||
public static TerracottaState.HostScanning setScanning() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
new GetTask(NetworkUtils.toURI(String.format(
|
||||
"http://127.0.0.1:%d/state/scanning?player=%s", portSpecific.port, getPlayerName()))
|
||||
).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||
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();
|
||||
|
||||
return new TerracottaState.HostScanning(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Task<TerracottaState.GuestStarting> setGuesting(String room) {
|
||||
public static Task<TerracottaState.GuestConnecting> setGuesting(String room) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
return new GetTask(NetworkUtils.toURI(String.format(
|
||||
"http://127.0.0.1:%d/state/guesting?room=%s&player=%s", portSpecific.port, room, getPlayerName()
|
||||
)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestStarting(-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;
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ package org.jackhuang.hmcl.terracotta;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.GeneralProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.MacOSProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
@@ -32,6 +32,8 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionRange;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -40,11 +42,11 @@ import java.net.URI;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -52,46 +54,60 @@ public final class TerracottaMetadata {
|
||||
private TerracottaMetadata() {
|
||||
}
|
||||
|
||||
public record Link(@SerializedName("desc") LocalizedText description, String link) {
|
||||
private record Options(String version, String classifier) {
|
||||
public String replace(String value) {
|
||||
return value.replace("${version}", version).replace("${classifier}", classifier);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
public record Link(
|
||||
@SerializedName("desc") LocalizedText description,
|
||||
@SerializedName("link") String link
|
||||
) {
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
private record Package(
|
||||
@SerializedName("hash") String hash,
|
||||
@SerializedName("files") Map<String, String> files
|
||||
) {
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
private record Config(
|
||||
@SerializedName("version_legacy") String legacy,
|
||||
@SerializedName("version_recent") List<String> recent,
|
||||
@SerializedName("version_latest") String latest,
|
||||
|
||||
@SerializedName("classifiers") Map<String, String> classifiers,
|
||||
@SerializedName("packages") Map<String, Package> pkgs,
|
||||
@SerializedName("downloads") List<String> downloads,
|
||||
@SerializedName("downloads_CN") List<String> downloadsCN,
|
||||
@SerializedName("links") List<Link> links
|
||||
) {
|
||||
private @Nullable TerracottaNative of(String classifier) {
|
||||
String hash = this.classifiers.get(classifier);
|
||||
if (hash == null)
|
||||
private @Nullable TerracottaBundle resolve(Options options) {
|
||||
Package pkg = pkgs.get(options.classifier);
|
||||
if (pkg == null) {
|
||||
return null;
|
||||
|
||||
if (!hash.startsWith("sha256:"))
|
||||
throw new IllegalArgumentException(String.format("Invalid hash value %s for classifier %s.", hash, classifier));
|
||||
hash = hash.substring("sha256:".length());
|
||||
|
||||
List<URI> links = new ArrayList<>(this.downloads.size() + this.downloadsCN.size());
|
||||
for (String download : LocaleUtils.IS_CHINA_MAINLAND
|
||||
? Lang.merge(this.downloadsCN, this.downloads)
|
||||
: Lang.merge(this.downloads, this.downloadsCN)) {
|
||||
links.add(URI.create(download.replace("${version}", this.latest).replace("${classifier}", classifier)));
|
||||
}
|
||||
|
||||
return new TerracottaNative(
|
||||
Collections.unmodifiableList(links),
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve(
|
||||
String.format("terracotta/%s/terracotta-%s-%s", this.latest, this.latest, classifier)
|
||||
).toAbsolutePath(),
|
||||
new FileDownloadTask.IntegrityCheck("SHA-256", hash)
|
||||
Stream<String> stream = downloads.stream(), streamCN = downloadsCN.stream();
|
||||
List<URI> links = (LocaleUtils.IS_CHINA_MAINLAND ? Stream.concat(streamCN, stream) : Stream.concat(stream, streamCN))
|
||||
.map(link -> URI.create(options.replace(link)))
|
||||
.toList();
|
||||
|
||||
Map<String, FileDownloadTask.IntegrityCheck> files = pkg.files.entrySet().stream().collect(Collectors.toUnmodifiableMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> new FileDownloadTask.IntegrityCheck("SHA-512", entry.getValue())
|
||||
));
|
||||
|
||||
return new TerracottaBundle(
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve(options.replace("terracotta/${version}")).toAbsolutePath(),
|
||||
links, new FileDownloadTask.IntegrityCheck("SHA-512", pkg.hash),
|
||||
files
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static final ITerracottaProvider PROVIDER;
|
||||
public static final AbstractTerracottaProvider PROVIDER;
|
||||
public static final String PACKAGE_NAME;
|
||||
public static final List<Link> PACKAGE_LINKS;
|
||||
public static final String FEEDBACK_LINK = NetworkUtils.withQuery("https://docs.hmcl.net/multiplayer/feedback.html", Map.of(
|
||||
@@ -99,8 +115,6 @@ public final class TerracottaMetadata {
|
||||
"launcher_version", Metadata.VERSION
|
||||
));
|
||||
|
||||
private static final Pattern LEGACY;
|
||||
private static final List<String> RECENT;
|
||||
private static final String LATEST;
|
||||
|
||||
static {
|
||||
@@ -111,95 +125,80 @@ public final class TerracottaMetadata {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
|
||||
LEGACY = Pattern.compile(config.legacy);
|
||||
RECENT = config.recent;
|
||||
LATEST = config.latest;
|
||||
|
||||
ProviderContext context = locateProvider(config);
|
||||
PROVIDER = context != null ? context.provider() : null;
|
||||
PACKAGE_NAME = context != null ? String.format("terracotta-%s-%s-pkg.tar.gz", config.latest, context.branch) : null;
|
||||
|
||||
if (context != null) {
|
||||
List<Link> packageLinks = new ArrayList<>(config.links.size());
|
||||
for (Link link : config.links) {
|
||||
packageLinks.add(new Link(
|
||||
link.description,
|
||||
link.link.replace("${version}", LATEST)
|
||||
.replace("${classifier}", context.branch)
|
||||
));
|
||||
}
|
||||
Options options = new Options(config.latest, OperatingSystem.CURRENT_OS.getCheckedName() + "-" + Architecture.SYSTEM_ARCH.getCheckedName());
|
||||
TerracottaBundle bundle = config.resolve(options);
|
||||
AbstractTerracottaProvider provider;
|
||||
if (bundle == null || (provider = locateProvider(bundle, options)) == null) {
|
||||
PROVIDER = null;
|
||||
PACKAGE_NAME = null;
|
||||
PACKAGE_LINKS = null;
|
||||
} else {
|
||||
PROVIDER = provider;
|
||||
PACKAGE_NAME = options.replace("terracotta-${version}-${classifier}-pkg.tar.gz");
|
||||
|
||||
List<Link> packageLinks = config.links.stream()
|
||||
.map(link -> new Link(link.description, options.replace(link.link)))
|
||||
.collect(Collectors.toList());
|
||||
Collections.shuffle(packageLinks);
|
||||
PACKAGE_LINKS = Collections.unmodifiableList(packageLinks);
|
||||
} else {
|
||||
PACKAGE_LINKS = null;
|
||||
}
|
||||
}
|
||||
|
||||
private record ProviderContext(ITerracottaProvider provider, String branch) {
|
||||
ProviderContext(ITerracottaProvider provider, String system, String arch) {
|
||||
this(provider, system + "-" + arch);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ProviderContext locateProvider(Config config) {
|
||||
String arch = Architecture.SYSTEM_ARCH.getCheckedName();
|
||||
private static AbstractTerracottaProvider locateProvider(TerracottaBundle bundle, Options options) {
|
||||
String prefix = options.replace("terracotta-${version}-${classifier}");
|
||||
|
||||
// FIXME: As HMCL is a cross-platform application, developers may mistakenly locate
|
||||
// non-existent files in non-native platform logic without assertion errors during debugging.
|
||||
return switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS -> {
|
||||
if (!OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_10))
|
||||
yield null;
|
||||
|
||||
TerracottaNative target = config.of("windows-%s.exe".formatted(arch));
|
||||
yield target != null
|
||||
? new ProviderContext(new GeneralProvider(target), "windows", arch)
|
||||
: null;
|
||||
}
|
||||
case LINUX -> {
|
||||
TerracottaNative target = config.of("linux-%s".formatted(arch));
|
||||
yield target != null
|
||||
? new ProviderContext(new GeneralProvider(target), "linux", arch)
|
||||
: null;
|
||||
}
|
||||
case MACOS -> {
|
||||
TerracottaNative installer = config.of("macos-%s.pkg".formatted(arch));
|
||||
TerracottaNative binary = config.of("macos-%s".formatted(arch));
|
||||
|
||||
yield installer != null && binary != null
|
||||
? new ProviderContext(new MacOSProvider(installer, binary), "macos", arch)
|
||||
: null;
|
||||
yield new GeneralProvider(bundle, bundle.locate(prefix + ".exe"));
|
||||
}
|
||||
case LINUX, FREEBSD -> new GeneralProvider(bundle, bundle.locate(prefix));
|
||||
case MACOS -> new MacOSProvider(
|
||||
bundle, bundle.locate(prefix), bundle.locate(prefix + ".pkg")
|
||||
);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static void removeLegacyVersionFiles() throws IOException {
|
||||
try (DirectoryStream<Path> terracotta = Files.newDirectoryStream(Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta").toAbsolutePath())) {
|
||||
for (Path path : terracotta) {
|
||||
String name = FileUtils.getName(path);
|
||||
if (LATEST.equals(name) || RECENT.contains(name) || !LEGACY.matcher(name).matches()) {
|
||||
continue;
|
||||
}
|
||||
public static void removeLegacyVersionFiles() {
|
||||
try (DirectoryStream<Path> terracotta = collectLegacyVersionFiles()) {
|
||||
if (terracotta == null)
|
||||
return;
|
||||
|
||||
for (Path path : terracotta) {
|
||||
try {
|
||||
FileUtils.deleteDirectory(path);
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Unable to remove legacy terracotta files: %s", path), e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to remove legacy terracotta files.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasLegacyVersionFiles() throws IOException {
|
||||
try (DirectoryStream<Path> terracotta = Files.newDirectoryStream(Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta").toAbsolutePath())) {
|
||||
for (Path path : terracotta) {
|
||||
String name = FileUtils.getName(path);
|
||||
if (!LATEST.equals(name) && (RECENT.contains(name) || LEGACY.matcher(name).matches())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
try (DirectoryStream<Path> terracotta = collectLegacyVersionFiles()) {
|
||||
return terracotta != null && terracotta.iterator().hasNext();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
private static @Nullable DirectoryStream<Path> collectLegacyVersionFiles() throws IOException {
|
||||
Path terracottaDir = Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta");
|
||||
if (Files.notExists(terracottaDir))
|
||||
return null;
|
||||
|
||||
VersionRange<VersionNumber> range = VersionNumber.atMost(LATEST);
|
||||
return Files.newDirectoryStream(terracottaDir, path -> {
|
||||
String name = FileUtils.getName(path);
|
||||
return !LATEST.equals(name) && range.contains(VersionNumber.asVersion(name));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.terracotta;
|
||||
|
||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
public final class TerracottaNative {
|
||||
private final List<URI> links;
|
||||
private final FileDownloadTask.IntegrityCheck checking;
|
||||
private final Path path;
|
||||
|
||||
public TerracottaNative(List<URI> links, Path path, FileDownloadTask.IntegrityCheck checking) {
|
||||
this.links = links;
|
||||
this.path = path;
|
||||
this.checking = checking;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public Task<?> install(ITerracottaProvider.Context context, @Nullable TarFileTree tree) {
|
||||
if (tree == null) {
|
||||
return new FileDownloadTask(links, path, checking) {
|
||||
@Override
|
||||
protected Context getContext(HttpResponse<?> response, boolean checkETag, String bmclapiHash) throws IOException {
|
||||
Context delegate = super.getContext(response, checkETag, bmclapiHash);
|
||||
return new Context() {
|
||||
@Override
|
||||
public void withResult(boolean success) {
|
||||
delegate.withResult(success);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int len) throws IOException {
|
||||
if (!context.hasInstallFence()) {
|
||||
throw new CancellationException("User has installed terracotta from local archives.");
|
||||
}
|
||||
delegate.write(buffer, offset, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (isSuccess() && !context.requestInstallFence()) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
|
||||
delegate.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return Task.runAsync(() -> {
|
||||
String name = FileUtils.getName(path);
|
||||
TarArchiveEntry entry = tree.getRoot().getFiles().get(name);
|
||||
if (entry == null) {
|
||||
throw new ITerracottaProvider.ArchiveFileMissingException("Cannot exact entry: " + name);
|
||||
}
|
||||
|
||||
if (!context.requestInstallFence()) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
|
||||
Files.createDirectories(path.toAbsolutePath().getParent());
|
||||
|
||||
MessageDigest digest = DigestUtils.getDigest(checking.getAlgorithm());
|
||||
try (
|
||||
InputStream stream = tree.getInputStream(entry);
|
||||
OutputStream os = Files.newOutputStream(path)
|
||||
) {
|
||||
stream.transferTo(new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
os.write(b);
|
||||
digest.update((byte) b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte @NotNull [] buffer, int offset, int len) throws IOException {
|
||||
os.write(buffer, offset, len);
|
||||
digest.update(buffer, offset, len);
|
||||
}
|
||||
});
|
||||
}
|
||||
String checksum = HexFormat.of().formatHex(digest.digest());
|
||||
if (!checksum.equalsIgnoreCase(checking.getChecksum())) {
|
||||
Files.delete(path);
|
||||
throw new ITerracottaProvider.ArchiveFileMissingException("Incorrect checksum (" + checking.getAlgorithm() + "), expected: " + checking.getChecksum() + ", actual: " + checksum);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ITerracottaProvider.Status status() throws IOException {
|
||||
if (Files.exists(path)) {
|
||||
if (DigestUtils.digestToString(checking.getAlgorithm(), path).equalsIgnoreCase(checking.getChecksum())) {
|
||||
return ITerracottaProvider.Status.READY;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (TerracottaMetadata.hasLegacyVersionFiles()) {
|
||||
return ITerracottaProvider.Status.LEGACY_VERSION;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.LOG.warning("Cannot determine whether legacy versions exist.");
|
||||
}
|
||||
return ITerracottaProvider.Status.NOT_EXIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.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() {
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,16 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.ObservableDoubleValue;
|
||||
import org.jackhuang.hmcl.terracotta.profile.TerracottaProfile;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSubtype;
|
||||
import org.jackhuang.hmcl.util.gson.JsonType;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract sealed class TerracottaState {
|
||||
@@ -63,32 +64,38 @@ public abstract sealed class TerracottaState {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Preparing extends TerracottaState implements ITerracottaProvider.Context {
|
||||
public static final class Preparing extends TerracottaState implements AbstractTerracottaProvider.DownloadContext {
|
||||
private final ReadOnlyDoubleWrapper progress;
|
||||
|
||||
private final AtomicBoolean installFence = new AtomicBoolean(false);
|
||||
private final AtomicBoolean hasInstallFence;
|
||||
|
||||
Preparing(ReadOnlyDoubleWrapper progress) {
|
||||
Preparing(ReadOnlyDoubleWrapper progress, boolean hasInstallFence) {
|
||||
this.progress = progress;
|
||||
this.hasInstallFence = new AtomicBoolean(hasInstallFence);
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty progressProperty() {
|
||||
return progress.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindProgress(ObservableValue<? extends Number> value) {
|
||||
progress.bind(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestInstallFence() {
|
||||
return installFence.compareAndSet(false, true);
|
||||
return hasInstallFence.compareAndSet(true, false);
|
||||
}
|
||||
|
||||
public boolean hasInstallFence() {
|
||||
return hasInstallFence.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasInstallFence() {
|
||||
return !installFence.get();
|
||||
public void bindProgress(ObservableDoubleValue progress) {
|
||||
this.progress.bind(progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkCancellation() {
|
||||
if (!hasInstallFence()) {
|
||||
throw new CancellationException("User has installed terracotta from local archives.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +119,7 @@ public abstract sealed class TerracottaState {
|
||||
@JsonSubtype(clazz = HostScanning.class, name = "host-scanning"),
|
||||
@JsonSubtype(clazz = HostStarting.class, name = "host-starting"),
|
||||
@JsonSubtype(clazz = HostOK.class, name = "host-ok"),
|
||||
@JsonSubtype(clazz = GuestStarting.class, name = "guest-connecting"),
|
||||
@JsonSubtype(clazz = GuestConnecting.class, name = "guest-connecting"),
|
||||
@JsonSubtype(clazz = GuestStarting.class, name = "guest-starting"),
|
||||
@JsonSubtype(clazz = GuestOK.class, name = "guest-ok"),
|
||||
@JsonSubtype(clazz = Exception.class, name = "exception"),
|
||||
@@ -202,12 +209,34 @@ public abstract sealed class TerracottaState {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestStarting extends Ready {
|
||||
GuestStarting(int port, int index, String state) {
|
||||
public static final class GuestConnecting extends Ready {
|
||||
GuestConnecting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestStarting extends Ready {
|
||||
public enum Difficulty {
|
||||
UNKNOWN,
|
||||
EASIEST,
|
||||
SIMPLE,
|
||||
MEDIUM,
|
||||
TOUGH
|
||||
}
|
||||
|
||||
@SerializedName("difficulty")
|
||||
private final Difficulty difficulty;
|
||||
|
||||
GuestStarting(int port, int index, String state, Difficulty difficulty) {
|
||||
super(port, index, state);
|
||||
this.difficulty = difficulty;
|
||||
}
|
||||
|
||||
public Difficulty getDifficulty() {
|
||||
return difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestOK extends Ready implements Validation {
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.terracotta.provider;
|
||||
|
||||
import javafx.beans.value.ObservableDoubleValue;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaBundle;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
public abstract class AbstractTerracottaProvider {
|
||||
public enum Status {
|
||||
NOT_EXIST,
|
||||
LEGACY_VERSION,
|
||||
READY
|
||||
}
|
||||
|
||||
public interface DownloadContext {
|
||||
@FXThread
|
||||
void bindProgress(ObservableDoubleValue progress);
|
||||
|
||||
void checkCancellation() throws CancellationException;
|
||||
}
|
||||
|
||||
protected final TerracottaBundle bundle;
|
||||
|
||||
protected AbstractTerracottaProvider(TerracottaBundle bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
public Status status() throws IOException {
|
||||
return bundle.status();
|
||||
}
|
||||
|
||||
public final Task<Path> download(DownloadContext progress) {
|
||||
return bundle.download(progress);
|
||||
}
|
||||
|
||||
public Task<?> install(Path pkg) throws IOException {
|
||||
return bundle.install(pkg);
|
||||
}
|
||||
|
||||
public abstract List<String> ofCommandLine(Path portTransfer);
|
||||
}
|
||||
@@ -17,51 +17,21 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.provider;
|
||||
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaNative;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaBundle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class GeneralProvider implements ITerracottaProvider {
|
||||
private final TerracottaNative target;
|
||||
public final class GeneralProvider extends AbstractTerracottaProvider {
|
||||
private final Path executable;
|
||||
|
||||
public GeneralProvider(TerracottaNative target) {
|
||||
this.target = target;
|
||||
public GeneralProvider(TerracottaBundle bundle, Path executable) {
|
||||
super(bundle);
|
||||
this.executable = executable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status status() throws IOException {
|
||||
return target.status();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException {
|
||||
Task<?> task = target.install(context, tree);
|
||||
context.bindProgress(task.progressProperty());
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
task = task.thenRunAsync(() -> Files.setPosixFilePermissions(target.getPath(), Set.of(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE,
|
||||
PosixFilePermission.OWNER_EXECUTE,
|
||||
PosixFilePermission.GROUP_READ,
|
||||
PosixFilePermission.GROUP_EXECUTE,
|
||||
PosixFilePermission.OTHERS_READ,
|
||||
PosixFilePermission.OTHERS_EXECUTE
|
||||
)));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> ofCommandLine(Path path) {
|
||||
return List.of(target.getPath().toString(), "--hmcl", path.toString());
|
||||
public List<String> ofCommandLine(Path portTransfer) {
|
||||
return List.of(executable.toString(), "--hmcl", portTransfer.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.terracotta.provider;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface ITerracottaProvider {
|
||||
enum Status {
|
||||
NOT_EXIST,
|
||||
LEGACY_VERSION,
|
||||
READY
|
||||
}
|
||||
|
||||
interface Context {
|
||||
void bindProgress(ObservableValue<? extends Number> value);
|
||||
|
||||
boolean requestInstallFence();
|
||||
|
||||
boolean hasInstallFence();
|
||||
}
|
||||
|
||||
abstract class ProviderException extends IOException {
|
||||
public ProviderException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ProviderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ProviderException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
final class ArchiveFileMissingException extends ProviderException {
|
||||
public ArchiveFileMissingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ArchiveFileMissingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ArchiveFileMissingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
Status status() throws IOException;
|
||||
|
||||
Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException;
|
||||
|
||||
List<String> ofCommandLine(Path path);
|
||||
}
|
||||
@@ -19,103 +19,77 @@ package org.jackhuang.hmcl.terracotta.provider;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaNative;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaBundle;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class MacOSProvider implements ITerracottaProvider {
|
||||
public final TerracottaNative installer, binary;
|
||||
public final class MacOSProvider extends AbstractTerracottaProvider {
|
||||
private final Path executable, installer;
|
||||
|
||||
public MacOSProvider(TerracottaNative installer, TerracottaNative binary) {
|
||||
public MacOSProvider(TerracottaBundle bundle, Path executable, Path installer) {
|
||||
super(bundle);
|
||||
this.executable = executable;
|
||||
this.installer = installer;
|
||||
this.binary = binary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status status() throws IOException {
|
||||
assert binary != null;
|
||||
|
||||
if (!Files.exists(Path.of("/Applications/terracotta.app"))) {
|
||||
return Status.NOT_EXIST;
|
||||
}
|
||||
|
||||
return binary.status();
|
||||
return bundle.status();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException {
|
||||
assert installer != null && binary != null;
|
||||
public Task<?> install(Path pkg) throws IOException {
|
||||
return super.install(pkg).thenComposeAsync(() -> {
|
||||
Path osascript = SystemUtils.which("osascript");
|
||||
if (osascript == null) {
|
||||
throw new IllegalStateException("Cannot locate 'osascript' system executable on MacOS for installing Terracotta.");
|
||||
}
|
||||
|
||||
Task<?> installerTask = installer.install(context, tree);
|
||||
Task<?> binaryTask = binary.install(context, tree);
|
||||
context.bindProgress(installerTask.progressProperty().add(binaryTask.progressProperty()).multiply(0.4)); // (1 + 1) * 0.4 = 0.8
|
||||
Path movedInstaller = Files.createTempDirectory(Metadata.HMCL_GLOBAL_DIRECTORY, "terracotta-pkg")
|
||||
.toRealPath()
|
||||
.resolve(FileUtils.getName(installer));
|
||||
Files.copy(installer, movedInstaller, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return Task.allOf(
|
||||
installerTask.thenComposeAsync(() -> {
|
||||
Path osascript = SystemUtils.which("osascript");
|
||||
if (osascript == null) {
|
||||
throw new IllegalStateException("Cannot locate 'osascript' system executable on MacOS for installing Terracotta.");
|
||||
}
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
|
||||
osascript.toString(), "-e", String.format(
|
||||
"do shell script \"installer -pkg '%s' -target /\" with prompt \"%s\" with administrator privileges",
|
||||
movedInstaller, i18n("terracotta.sudo_installing")
|
||||
)));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
Path pkg = Files.createTempDirectory(Metadata.HMCL_GLOBAL_DIRECTORY, "terracotta-pkg")
|
||||
.toRealPath()
|
||||
.resolve(FileUtils.getName(installer.getPath()));
|
||||
Files.copy(installer.getPath(), pkg, StandardCopyOption.REPLACE_EXISTING);
|
||||
return Task.fromCompletableFuture(process.getProcess().onExit()).thenRunAsync(() -> {
|
||||
try {
|
||||
FileUtils.cleanDirectory(movedInstaller.getParent());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Cannot remove temporary Terracotta package file.", e);
|
||||
}
|
||||
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
|
||||
osascript.toString(), "-e", String.format(
|
||||
"do shell script \"installer -pkg '%s' -target /\" with prompt \"%s\" with administrator privileges",
|
||||
pkg, i18n("terracotta.sudo_installing")
|
||||
)));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
return Task.fromCompletableFuture(process.getProcess().onExit()).thenRunAsync(() -> {
|
||||
try {
|
||||
FileUtils.cleanDirectory(pkg.getParent());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Cannot remove temporary Terracotta package file.", e);
|
||||
}
|
||||
|
||||
if (process.getExitCode() != 0) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Cannot install Terracotta %s: system installer exited with code %d",
|
||||
pkg,
|
||||
process.getExitCode()
|
||||
));
|
||||
}
|
||||
});
|
||||
}),
|
||||
binaryTask.thenRunAsync(() -> Files.setPosixFilePermissions(binary.getPath(), Set.of(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE,
|
||||
PosixFilePermission.OWNER_EXECUTE,
|
||||
PosixFilePermission.GROUP_READ,
|
||||
PosixFilePermission.GROUP_EXECUTE,
|
||||
PosixFilePermission.OTHERS_READ,
|
||||
PosixFilePermission.OTHERS_EXECUTE
|
||||
)))
|
||||
);
|
||||
if (process.getExitCode() != 0) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Cannot install Terracotta %s: system installer exited with code %d", movedInstaller, process.getExitCode()
|
||||
));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> ofCommandLine(Path path) {
|
||||
assert binary != null;
|
||||
|
||||
return List.of(binary.getPath().toString(), "--hmcl", path.toString());
|
||||
return List.of(executable.toString(), "--hmcl", path.toString());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
@@ -72,9 +73,10 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class Controllers {
|
||||
public static final String JAVA_VERSION_TIP = "javaVersion";
|
||||
@@ -505,8 +507,8 @@ public final class Controllers {
|
||||
return prompt(title, onResult, "");
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult, String initialValue) {
|
||||
InputDialogPane pane = new InputDialogPane(title, initialValue, onResult);
|
||||
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult, String initialValue, ValidatorBase... validators) {
|
||||
InputDialogPane pane = new InputDialogPane(title, initialValue, onResult, validators);
|
||||
dialog(pane);
|
||||
return pane.getCompletableFuture();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.JFXDialog;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.Decorator;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class DialogUtils {
|
||||
private DialogUtils() {
|
||||
}
|
||||
|
||||
public static final String PROPERTY_DIALOG_INSTANCE = DialogUtils.class.getName() + ".dialog.instance";
|
||||
public static final String PROPERTY_DIALOG_PANE_INSTANCE = DialogUtils.class.getName() + ".dialog.pane.instance";
|
||||
public static final String PROPERTY_DIALOG_CLOSE_HANDLER = DialogUtils.class.getName() + ".dialog.closeListener";
|
||||
|
||||
public static final String PROPERTY_PARENT_PANE_REF = DialogUtils.class.getName() + ".dialog.parentPaneRef";
|
||||
public static final String PROPERTY_PARENT_DIALOG_REF = DialogUtils.class.getName() + ".dialog.parentDialogRef";
|
||||
|
||||
public static void show(Decorator decorator, Node content) {
|
||||
if (decorator.getDrawerWrapper() == null) {
|
||||
Platform.runLater(() -> show(decorator, content));
|
||||
return;
|
||||
}
|
||||
|
||||
show(decorator.getDrawerWrapper(), content, (dialog) -> {
|
||||
JFXDialogPane pane = (JFXDialogPane) dialog.getContent();
|
||||
decorator.capableDraggingWindow(dialog);
|
||||
decorator.forbidDraggingWindow(pane);
|
||||
dialog.setDialogContainer(decorator.getDrawerWrapper());
|
||||
});
|
||||
}
|
||||
|
||||
public static void show(StackPane container, Node content) {
|
||||
show(container, content, null);
|
||||
}
|
||||
|
||||
public static void show(StackPane container, Node content, @Nullable Consumer<JFXDialog> onDialogCreated) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
JFXDialog dialog = (JFXDialog) container.getProperties().get(PROPERTY_DIALOG_INSTANCE);
|
||||
JFXDialogPane dialogPane = (JFXDialogPane) container.getProperties().get(PROPERTY_DIALOG_PANE_INSTANCE);
|
||||
|
||||
if (dialog == null) {
|
||||
dialog = new JFXDialog(AnimationUtils.isAnimationEnabled()
|
||||
? JFXDialog.DialogTransition.CENTER
|
||||
: JFXDialog.DialogTransition.NONE);
|
||||
dialogPane = new JFXDialogPane();
|
||||
|
||||
dialog.setContent(dialogPane);
|
||||
dialog.setDialogContainer(container);
|
||||
dialog.setOverlayClose(false);
|
||||
|
||||
container.getProperties().put(PROPERTY_DIALOG_INSTANCE, dialog);
|
||||
container.getProperties().put(PROPERTY_DIALOG_PANE_INSTANCE, dialogPane);
|
||||
|
||||
if (onDialogCreated != null) {
|
||||
onDialogCreated.accept(dialog);
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
content.getProperties().put(PROPERTY_PARENT_PANE_REF, dialogPane);
|
||||
content.getProperties().put(PROPERTY_PARENT_DIALOG_REF, dialog);
|
||||
|
||||
dialogPane.push(content);
|
||||
|
||||
EventHandler<DialogCloseEvent> handler = event -> close(content);
|
||||
content.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
|
||||
content.addEventHandler(DialogCloseEvent.CLOSE, handler);
|
||||
|
||||
handleDialogShown(dialog, content);
|
||||
}
|
||||
|
||||
private static void handleDialogShown(JFXDialog dialog, Node node) {
|
||||
if (dialog.isVisible()) {
|
||||
dialog.requestFocus();
|
||||
if (node instanceof DialogAware dialogAware)
|
||||
dialogAware.onDialogShown();
|
||||
} else {
|
||||
dialog.visibleProperty().addListener(new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
if (newValue) {
|
||||
dialog.requestFocus();
|
||||
if (node instanceof DialogAware dialogAware)
|
||||
dialogAware.onDialogShown();
|
||||
observable.removeListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void close(Node content) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
Optional.ofNullable(content.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
|
||||
.ifPresent(handler -> content.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
|
||||
|
||||
JFXDialogPane pane = (JFXDialogPane) content.getProperties().get(PROPERTY_PARENT_PANE_REF);
|
||||
JFXDialog dialog = (JFXDialog) content.getProperties().get(PROPERTY_PARENT_DIALOG_REF);
|
||||
|
||||
if (dialog != null && pane != null) {
|
||||
if (pane.size() == 1 && pane.peek().orElse(null) == content) {
|
||||
dialog.setOnDialogClosed(e -> pane.pop(content));
|
||||
dialog.close();
|
||||
|
||||
StackPane container = dialog.getDialogContainer();
|
||||
if (container != null) {
|
||||
container.getProperties().remove(PROPERTY_DIALOG_INSTANCE);
|
||||
container.getProperties().remove(PROPERTY_DIALOG_PANE_INSTANCE);
|
||||
container.getProperties().remove(PROPERTY_PARENT_DIALOG_REF);
|
||||
container.getProperties().remove(PROPERTY_PARENT_PANE_REF);
|
||||
}
|
||||
} else {
|
||||
pane.pop(content);
|
||||
}
|
||||
|
||||
if (content instanceof DialogAware dialogAware) {
|
||||
dialogAware.onDialogClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,7 @@ public final class FXUtils {
|
||||
public static final @Nullable ReadOnlyObjectProperty<Color> ACCENT_COLOR;
|
||||
|
||||
public static final @Nullable MethodHandle TEXT_TRUNCATED_PROPERTY;
|
||||
public static final @Nullable MethodHandle FOCUS_VISIBLE_PROPERTY;
|
||||
|
||||
static {
|
||||
String jfxVersion = System.getProperty("javafx.version");
|
||||
@@ -203,6 +204,20 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
TEXT_TRUNCATED_PROPERTY = textTruncatedProperty;
|
||||
|
||||
MethodHandle focusVisibleProperty = null;
|
||||
if (JAVAFX_MAJOR_VERSION >= 19) {
|
||||
try {
|
||||
focusVisibleProperty = MethodHandles.publicLookup().findVirtual(
|
||||
Node.class,
|
||||
"focusVisibleProperty",
|
||||
MethodType.methodType(ReadOnlyBooleanProperty.class)
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to lookup focusVisibleProperty", e);
|
||||
}
|
||||
}
|
||||
FOCUS_VISIBLE_PROPERTY = focusVisibleProperty;
|
||||
}
|
||||
|
||||
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
@@ -423,6 +438,20 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable ReadOnlyBooleanProperty focusVisibleProperty(Node node) {
|
||||
if (FOCUS_VISIBLE_PROPERTY != null) {
|
||||
try {
|
||||
return (ReadOnlyBooleanProperty) FOCUS_VISIBLE_PROPERTY.invokeExact(node);
|
||||
} catch (RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(50);
|
||||
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
|
||||
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
|
||||
@@ -1188,12 +1217,14 @@ public final class FXUtils {
|
||||
|
||||
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(url)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
|
||||
public static Task<Image> getRemoteImageTask(URI uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(uri)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
@@ -1208,6 +1239,7 @@ public final class FXUtils {
|
||||
LOG.warning("An exception encountered while loading remote image: " + url, exception);
|
||||
}
|
||||
})
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.start();
|
||||
return image;
|
||||
}
|
||||
@@ -1309,17 +1341,11 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
return view -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
if (!empty) {
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(graphicBuilder.apply(item));
|
||||
@@ -1364,7 +1390,7 @@ public final class FXUtils {
|
||||
|
||||
public static void onClicked(Node node, Runnable action) {
|
||||
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 1) {
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
action.run();
|
||||
e.consume();
|
||||
}
|
||||
@@ -1410,16 +1436,6 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static void clearFocus(Node node) {
|
||||
Scene scene = node.getScene();
|
||||
if (scene != null) {
|
||||
Parent root = scene.getRoot();
|
||||
if (root != null) {
|
||||
root.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyOnDoubleClick(Labeled label) {
|
||||
label.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
|
||||
|
||||
@@ -24,11 +24,11 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
@@ -40,6 +40,7 @@ import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
@@ -50,9 +51,12 @@ import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
@@ -81,6 +85,7 @@ public class GameCrashWindow extends Stage {
|
||||
private final ProcessListener.ExitType exitType;
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
private final StackPane stackPane;
|
||||
|
||||
private final List<Log> logs;
|
||||
|
||||
@@ -93,7 +98,7 @@ public class GameCrashWindow extends Stage {
|
||||
this.logs = logs;
|
||||
this.analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null));
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " " + i18n("settings.memory.unit.mib")).orElse("-");
|
||||
|
||||
total_memory = MEGABYTES.formatBytes(SystemInfo.getTotalMemorySize());
|
||||
|
||||
@@ -103,9 +108,10 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
this.view = new View();
|
||||
|
||||
this.stackPane = new StackPane(view);
|
||||
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
|
||||
|
||||
setScene(new Scene(view, 800, 480));
|
||||
setScene(new Scene(stackPane, 800, 480));
|
||||
StyleSheets.init(getScene());
|
||||
setTitle(i18n("game.crash.title"));
|
||||
FXUtils.setIcon(this);
|
||||
@@ -269,22 +275,41 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
CompletableFuture.supplyAsync(() ->
|
||||
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.handleAsync((result, exception) -> {
|
||||
Alert alert;
|
||||
.thenComposeAsync(logs -> {
|
||||
long processStartTime = managedProcess.getProcess().info()
|
||||
.startInstant()
|
||||
.map(Instant::toEpochMilli).orElseGet(() -> {
|
||||
try {
|
||||
return ManagementFactory.getRuntimeMXBean().getStartTime();
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get process start time", e);
|
||||
return 0L;
|
||||
}
|
||||
});
|
||||
|
||||
return LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs,
|
||||
new CommandBuilder().addAll(managedProcess.getCommands()).toString(),
|
||||
path -> {
|
||||
try {
|
||||
FileTime lastModifiedTime = Files.getLastModifiedTime(path);
|
||||
return lastModifiedTime.toMillis() >= processStartTime;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to read file attributes", e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})
|
||||
.handleAsync((result, exception) -> {
|
||||
if (exception == null) {
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
} else {
|
||||
LOG.warning("Failed to export game crash info", exception);
|
||||
alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception));
|
||||
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
}
|
||||
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
|
||||
return null;
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -62,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 {
|
||||
@@ -72,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 {
|
||||
@@ -335,10 +300,15 @@ public class InstallerItem extends Control {
|
||||
}
|
||||
pane.getStyleClass().add("installer-item");
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
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());
|
||||
@@ -367,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());
|
||||
}
|
||||
@@ -402,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());
|
||||
|
||||
@@ -33,7 +33,6 @@ import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
@@ -44,12 +43,10 @@ import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Dwmapi;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
|
||||
@@ -66,8 +63,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -200,10 +197,12 @@ public final class LogWindow extends Stage {
|
||||
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
|
||||
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
|
||||
private final JFXComboBox<Integer> cboLines = new JFXComboBox<>();
|
||||
private final StackPane stackPane = new StackPane();
|
||||
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
listView.getProperties().put("no-smooth-scrolling", true);
|
||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
|
||||
|
||||
for (int i = 0; i < LEVELS.length; i++) {
|
||||
@@ -241,9 +240,8 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
});
|
||||
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
@@ -267,9 +265,8 @@ public final class LogWindow extends Stage {
|
||||
LOG.warning("Failed to create minecraft jstack dump", e);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump"));
|
||||
alert.setTitle(i18n("message.error"));
|
||||
alert.showAndWait();
|
||||
var dialog = new MessageDialogPane.Builder(i18n("logwindow.export_dump") + "\n" + StringUtils.getStackTrace(e), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -300,7 +297,9 @@ public final class LogWindow extends Stage {
|
||||
|
||||
VBox vbox = new VBox(3);
|
||||
vbox.setPadding(new Insets(3, 0, 3, 0));
|
||||
getChildren().setAll(vbox);
|
||||
getSkinnable().stackPane.getChildren().setAll(vbox);
|
||||
getChildren().setAll(getSkinnable().stackPane);
|
||||
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
@@ -343,8 +342,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
listView.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT)
|
||||
+ "\"; -fx-font-size: " + config().getFontSize() + "px;");
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||
listView.setCellFactory(x -> new ListCell<>() {
|
||||
{
|
||||
x.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
@@ -388,11 +386,6 @@ public final class LogWindow extends Stage {
|
||||
protected void updateItem(Log item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
pseudoClassStateChanged(EMPTY, empty);
|
||||
pseudoClassStateChanged(FATAL, !empty && item.getLevel() == Log4jLevel.FATAL);
|
||||
pseudoClassStateChanged(ERROR, !empty && item.getLevel() == Log4jLevel.ERROR);
|
||||
|
||||
@@ -25,7 +25,6 @@ import javafx.animation.Animation.Status;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.IndexedCell;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
@@ -55,6 +54,8 @@ final class ScrollUtils {
|
||||
private static final double DEFAULT_SPEED = 1.0;
|
||||
private static final double DEFAULT_TRACK_PAD_ADJUSTMENT = 7.0;
|
||||
|
||||
private static final double CUTOFF_DELTA = 0.01;
|
||||
|
||||
/**
|
||||
* Determines if the given ScrollEvent comes from a trackpad.
|
||||
* <p></p>
|
||||
@@ -211,7 +212,7 @@ final class ScrollUtils {
|
||||
break;
|
||||
}
|
||||
|
||||
if (Math.abs(dy) < 0.001) {
|
||||
if (Math.abs(dy) < CUTOFF_DELTA) {
|
||||
timeline.stop();
|
||||
}
|
||||
}));
|
||||
@@ -226,7 +227,6 @@ final class ScrollUtils {
|
||||
final double[] derivatives = new double[FRICTIONS.length];
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
Holder<ScrollDirection> scrollDirectionHolder = new Holder<>();
|
||||
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
|
||||
final EventHandler<ScrollEvent> scrollHandler = event -> {
|
||||
if (event.getEventType() == ScrollEvent.SCROLL) {
|
||||
@@ -234,7 +234,6 @@ final class ScrollUtils {
|
||||
if (scrollDirection == ScrollDirection.LEFT || scrollDirection == ScrollDirection.RIGHT) {
|
||||
return;
|
||||
}
|
||||
scrollDirectionHolder.value = scrollDirection;
|
||||
double currentSpeed = isTrackPad(event, scrollDirection) ? speed / trackPadAdjustment : speed;
|
||||
|
||||
derivatives[0] += scrollDirection.intDirection * currentSpeed;
|
||||
@@ -256,17 +255,9 @@ final class ScrollUtils {
|
||||
}
|
||||
|
||||
double dy = derivatives[derivatives.length - 1];
|
||||
virtualFlow.scrollPixels(dy);
|
||||
|
||||
int cellCount = virtualFlow.getCellCount();
|
||||
IndexedCell<?> firstVisibleCell = virtualFlow.getFirstVisibleCell();
|
||||
double height = firstVisibleCell != null ? firstVisibleCell.getHeight() * cellCount : 0.0;
|
||||
|
||||
double delta = height > 0.0
|
||||
? dy / height
|
||||
: (scrollDirectionHolder.value == ScrollDirection.DOWN ? 0.001 : -0.001);
|
||||
virtualFlow.setPosition(Math.min(Math.max(virtualFlow.getPosition() + delta, 0), 1));
|
||||
|
||||
if (Math.abs(dy) < 0.001) {
|
||||
if (Math.abs(dy) < CUTOFF_DELTA) {
|
||||
timeline.stop();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -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);
|
||||
@@ -89,10 +85,7 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(svg.createIcon()));
|
||||
ret.setText(text);
|
||||
ret.setOnAction(e -> {
|
||||
onClick.run();
|
||||
FXUtils.clearFocus(ret);
|
||||
});
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -105,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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -55,4 +55,8 @@ public class WeakListenerHolder {
|
||||
public boolean remove(Object obj) {
|
||||
return refs.remove(obj);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
refs.clear();
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
|
||||
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
btnRefresh.setDisable(true);
|
||||
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot"));
|
||||
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot.tooltip"));
|
||||
}
|
||||
btnRefresh.setOnAction(e -> {
|
||||
spinnerRefresh.showSpinner();
|
||||
|
||||
@@ -25,11 +25,10 @@ import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
@@ -88,6 +87,10 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
|
||||
addServerPane.setBody(txtServerUrl);
|
||||
addServerPane.setActions(lblCreationWarning, actions);
|
||||
|
||||
txtServerUrl.getValidators().addAll(new RequiredValidator(), new URLValidator());
|
||||
FXUtils.setValidateWhileTextChanged(txtServerUrl, true);
|
||||
btnAddNext.disableProperty().bind(txtServerUrl.activeValidatorProperty().isNotNull());
|
||||
}
|
||||
|
||||
confirmServerPane = new JFXDialogLayout();
|
||||
@@ -149,7 +152,6 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
this.setContent(addServerPane, ContainerAnimations.NONE);
|
||||
|
||||
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
|
||||
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
|
||||
nextPane.hideSpinner();
|
||||
|
||||
onEscPressed(this, this::onAddCancel);
|
||||
@@ -162,6 +164,12 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
|
||||
private String resolveFetchExceptionMessage(Throwable exception) {
|
||||
if (exception instanceof SSLException) {
|
||||
if (exception.getMessage() != null && exception.getMessage().contains("Remote host terminated")) {
|
||||
return i18n("account.failed.connect_injector_server");
|
||||
}
|
||||
if (exception.getMessage() != null && (exception.getMessage().contains("No name matching") || exception.getMessage().contains("No subject alternative DNS name matching"))) {
|
||||
return i18n("account.failed.dns");
|
||||
}
|
||||
return i18n("account.failed.ssl");
|
||||
} else if (exception instanceof IOException) {
|
||||
return i18n("account.failed.connect_injector_server");
|
||||
|
||||
@@ -35,8 +35,6 @@ import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
|
||||
@@ -64,11 +62,7 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -286,72 +280,95 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
btnAccept.disableProperty().unbind();
|
||||
detailsContainer.getChildren().remove(detailsPane);
|
||||
lblErrorMessage.setText("");
|
||||
lblErrorMessage.setVisible(true);
|
||||
}
|
||||
|
||||
if (factory == Accounts.FACTORY_MICROSOFT) {
|
||||
VBox vbox = new VBox(8);
|
||||
if (!Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
}
|
||||
});
|
||||
FXUtils.onClicked(hintPane, () -> {
|
||||
if (deviceCode.get() != null) {
|
||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
}
|
||||
});
|
||||
detailsPane = vbox;
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> {
|
||||
runInFX(() -> deviceCode.set(value));
|
||||
}));
|
||||
FlowPane box = new FlowPane();
|
||||
box.setHgap(8);
|
||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
||||
birthLink.setExternalLink("https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a");
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
|
||||
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
|
||||
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
|
||||
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
|
||||
JFXHyperlink createProfileLink = new JFXHyperlink(i18n("account.methods.microsoft.makegameidsettings"));
|
||||
createProfileLink.setExternalLink("https://www.minecraft.net/msaprofile/mygames/editprofile");
|
||||
JFXHyperlink bannedQueryLink = new JFXHyperlink(i18n("account.methods.ban_query"));
|
||||
bannedQueryLink.setExternalLink("https://enforcement.xbox.com/enforcement/showenforcementhistory");
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink, bannedQueryLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
HintPane unofficialHint = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
unofficialHint.setText(i18n("unofficial.hint"));
|
||||
vbox.getChildren().add(unofficialHint);
|
||||
}
|
||||
|
||||
vbox.getChildren().addAll(hintPane, box);
|
||||
|
||||
btnAccept.setDisable(false);
|
||||
} else {
|
||||
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
|
||||
|
||||
JFXHyperlink officialWebsite = new JFXHyperlink(i18n("account.methods.microsoft.snapshot.website"));
|
||||
officialWebsite.setExternalLink(Metadata.PUBLISH_URL);
|
||||
|
||||
vbox.getChildren().setAll(hintPane, officialWebsite);
|
||||
btnAccept.setDisable(true);
|
||||
vbox.getChildren().add(hintPane);
|
||||
return;
|
||||
}
|
||||
|
||||
detailsPane = vbox;
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
hintPane.setSegment(i18n("unofficial.hint"));
|
||||
vbox.getChildren().add(hintPane);
|
||||
}
|
||||
|
||||
VBox codeBox = new VBox(8);
|
||||
Label hint = new Label(i18n("account.methods.microsoft.code"));
|
||||
Label code = new Label();
|
||||
code.setMouseTransparent(true);
|
||||
code.setStyle("-fx-font-size: 24");
|
||||
codeBox.getChildren().addAll(hint, code);
|
||||
codeBox.setAlignment(Pos.CENTER);
|
||||
vbox.getChildren().add(codeBox);
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
|
||||
errHintPane.setVisible(false);
|
||||
errHintPane.setManaged(false);
|
||||
|
||||
codeBox.setVisible(false);
|
||||
codeBox.setManaged(false);
|
||||
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
code.setText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getVerificationUri()));
|
||||
codeBox.setVisible(true);
|
||||
codeBox.setManaged(true);
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
codeBox.setVisible(false);
|
||||
codeBox.setManaged(false);
|
||||
}
|
||||
});
|
||||
|
||||
lblErrorMessage.setVisible(false);
|
||||
lblErrorMessage.textProperty().addListener((obs, oldVal, newVal) -> {
|
||||
boolean hasError = !newVal.isEmpty();
|
||||
errHintPane.setSegment(newVal);
|
||||
errHintPane.setVisible(hasError);
|
||||
errHintPane.setManaged(hasError);
|
||||
hintPane.setVisible(!hasError);
|
||||
hintPane.setManaged(!hasError);
|
||||
codeBox.setVisible(!hasError && deviceCode.get() != null);
|
||||
codeBox.setManaged(!hasError && deviceCode.get() != null);
|
||||
});
|
||||
|
||||
FXUtils.onClicked(codeBox, () -> {
|
||||
if (deviceCode.get() != null) FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
});
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value ->
|
||||
runInFX(() -> deviceCode.set(value))
|
||||
));
|
||||
|
||||
HBox linkBox = new HBox();
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
|
||||
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
|
||||
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
|
||||
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
|
||||
linkBox.getChildren().setAll(profileLink, purchaseLink, deauthorizeLink, forgotpasswordLink);
|
||||
|
||||
vbox.getChildren().addAll(hintPane, errHintPane, linkBox);
|
||||
btnAccept.setDisable(false);
|
||||
} else {
|
||||
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
|
||||
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
|
||||
}
|
||||
|
||||
detailsContainer.getChildren().add(detailsPane);
|
||||
}
|
||||
|
||||
@@ -460,6 +477,9 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
onChangeAndOperate(cboServers.valueProperty(), server -> {
|
||||
this.server = server;
|
||||
linksContainer.getChildren().setAll(createHyperlinks(server));
|
||||
|
||||
if (txtUsername != null)
|
||||
txtUsername.validate();
|
||||
});
|
||||
linksContainer.setMinWidth(USE_PREF_SIZE);
|
||||
|
||||
@@ -590,6 +610,9 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
if ((factory instanceof AuthlibInjectorAccountFactory) && this.server != null) {
|
||||
return !server.isNonEmailLogin();
|
||||
}
|
||||
if (factory instanceof BoundAuthlibInjectorAccountFactory bound) {
|
||||
return !bound.getServer().isNonEmailLogin();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,11 +66,11 @@ public class OAuthAccountLoginDialog extends DialogPane {
|
||||
|
||||
HBox box = new HBox(8);
|
||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
||||
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a"));
|
||||
birthLink.setExternalLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a");
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
@@ -159,7 +160,9 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
|
||||
FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> {
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setPadding(new Insets(0, 0, 0, 10));
|
||||
// Increase bottom padding to prevent the prompt from overlapping with the dialog action area
|
||||
|
||||
gridPane.setPadding(new Insets(0, 0, 45, 10));
|
||||
gridPane.setHgap(16);
|
||||
gridPane.setVgap(8);
|
||||
gridPane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
|
||||
@@ -172,9 +175,23 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
case LITTLE_SKIN:
|
||||
HintPane hint = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hint.setText(i18n("account.skin.type.little_skin.hint"));
|
||||
|
||||
// Spanning two columns and expanding horizontally
|
||||
GridPane.setColumnSpan(hint, 2);
|
||||
GridPane.setHgrow(hint, Priority.ALWAYS);
|
||||
hint.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
// Force top alignment within cells (to avoid vertical offset caused by the baseline)
|
||||
GridPane.setValignment(hint, VPos.TOP);
|
||||
|
||||
// Set a fixed height as the preferred height to prevent the GridPane from stretching or leaving empty space.
|
||||
hint.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
hint.setMinHeight(Region.USE_PREF_SIZE);
|
||||
|
||||
gridPane.addRow(0, hint);
|
||||
break;
|
||||
case LOCAL_FILE:
|
||||
gridPane.setPadding(new Insets(0, 0, 0, 10));
|
||||
gridPane.addRow(0, new Label(i18n("account.skin.model")), modelCombobox);
|
||||
gridPane.addRow(1, new Label(i18n("account.skin")), skinSelector);
|
||||
gridPane.addRow(2, new Label(i18n("account.cape")), capeSelector);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -23,7 +23,6 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
@@ -45,7 +44,6 @@ public class FileItem extends BorderPane {
|
||||
|
||||
private final SimpleStringProperty name = new SimpleStringProperty(this, "name");
|
||||
private final SimpleStringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final SimpleStringProperty tooltip = new SimpleStringProperty(this, "tooltip");
|
||||
private final SimpleStringProperty path = new SimpleStringProperty(this, "path");
|
||||
private final SimpleBooleanProperty convertToRelativePath = new SimpleBooleanProperty(this, "convertToRelativePath");
|
||||
|
||||
@@ -65,10 +63,6 @@ public class FileItem extends BorderPane {
|
||||
FXUtils.installFastTooltip(right, i18n("button.edit"));
|
||||
setRight(right);
|
||||
|
||||
Tooltip tip = new Tooltip();
|
||||
tip.textProperty().bind(tooltipProperty());
|
||||
Tooltip.install(this, tip);
|
||||
|
||||
convertToRelativePath.addListener(onInvalidating(() -> path.set(processPath(path.get()))));
|
||||
}
|
||||
|
||||
@@ -142,18 +136,6 @@ public class FileItem extends BorderPane {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getTooltip() {
|
||||
return tooltip.get();
|
||||
}
|
||||
|
||||
public StringProperty tooltipProperty() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
public void setTooltip(String tooltip) {
|
||||
this.tooltip.set(tooltip);
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path.get();
|
||||
}
|
||||
|
||||
@@ -58,11 +58,12 @@ public final class ImagePickerItem extends BorderPane {
|
||||
selectButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
JFXButton deleteButton = new JFXButton();
|
||||
deleteButton.setGraphic(SVG.CLOSE.createIcon(20));
|
||||
deleteButton.setGraphic(SVG.RESTORE.createIcon(20));
|
||||
deleteButton.onActionProperty().bind(onDeleteButtonClicked);
|
||||
deleteButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
|
||||
FXUtils.installFastTooltip(deleteButton, i18n("button.reset"));
|
||||
|
||||
HBox hBox = new HBox();
|
||||
hBox.getChildren().setAll(imageView, selectButton, deleteButton);
|
||||
|
||||
@@ -20,9 +20,11 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -30,12 +32,22 @@ import java.util.concurrent.CompletableFuture;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class InputDialogPane extends JFXDialogLayout {
|
||||
public class InputDialogPane extends JFXDialogLayout implements DialogAware {
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
|
||||
private final JFXTextField textField;
|
||||
private final Label lblCreationWarning;
|
||||
private final SpinnerPane acceptPane;
|
||||
private final JFXButton acceptButton;
|
||||
|
||||
public InputDialogPane(String text, String initialValue, FutureCallback<String> onResult, ValidatorBase... validators) {
|
||||
this(text, initialValue, onResult);
|
||||
if (validators != null && validators.length > 0) {
|
||||
textField.getValidators().addAll(validators);
|
||||
FXUtils.setValidateWhileTextChanged(textField, true);
|
||||
acceptButton.disableProperty().bind(textField.activeValidatorProperty().isNotNull());
|
||||
}
|
||||
}
|
||||
|
||||
public InputDialogPane(String text, String initialValue, FutureCallback<String> onResult) {
|
||||
textField = new JFXTextField(initialValue);
|
||||
@@ -47,7 +59,7 @@ public class InputDialogPane extends JFXDialogLayout {
|
||||
|
||||
acceptPane = new SpinnerPane();
|
||||
acceptPane.getStyleClass().add("small-spinner-pane");
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.getStyleClass().add("dialog-accept");
|
||||
acceptPane.setContent(acceptButton);
|
||||
|
||||
@@ -69,10 +81,15 @@ public class InputDialogPane extends JFXDialogLayout {
|
||||
lblCreationWarning.setText(msg);
|
||||
});
|
||||
});
|
||||
|
||||
textField.setOnAction(event -> acceptButton.fire());
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogShown() {
|
||||
textField.requestFocus();
|
||||
}
|
||||
|
||||
public CompletableFuture<String> getCompletableFuture() {
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -24,17 +24,14 @@ import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
|
||||
public abstract class MDListCell<T> extends ListCell<T> {
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
private final Holder<Object> lastCell;
|
||||
|
||||
public MDListCell(JFXListView<T> listView, Holder<Object> lastCell) {
|
||||
this.lastCell = lastCell;
|
||||
public MDListCell(JFXListView<T> listView) {
|
||||
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
@@ -58,13 +55,6 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (lastCell != null) {
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
}
|
||||
|
||||
updateControl(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
|
||||
@@ -87,7 +87,7 @@ public final class MessageDialogPane extends HBox {
|
||||
{
|
||||
StackPane titlePane = new StackPane();
|
||||
titlePane.getStyleClass().addAll("jfx-layout-heading", "title");
|
||||
titlePane.getChildren().setAll(new Label(title != null ? title : i18n("message.info")));
|
||||
titlePane.getChildren().setAll(new Label(title != null ? title : type.getDisplayName()));
|
||||
|
||||
StackPane content = new StackPane();
|
||||
content.getStyleClass().add("jfx-layout-body");
|
||||
@@ -190,6 +190,7 @@ public final class MessageDialogPane extends HBox {
|
||||
|
||||
public Builder addCancel(String cancelText, @Nullable Runnable cancel) {
|
||||
JFXButton btnCancel = new JFXButton(cancelText);
|
||||
btnCancel.setButtonType(JFXButton.ButtonType.FLAT);
|
||||
btnCancel.getStyleClass().add("dialog-cancel");
|
||||
if (cancel != null) {
|
||||
btnCancel.setOnAction(e -> cancel.run());
|
||||
|
||||
@@ -181,6 +181,7 @@ public final class MultiFileItem<T> extends VBox {
|
||||
center.setWrapText(true);
|
||||
center.getStyleClass().add("subtitle-label");
|
||||
center.setStyle("-fx-font-size: 10;");
|
||||
center.setPadding(new Insets(0, 0, 0, 15));
|
||||
pane.setCenter(center);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,6 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -59,17 +57,14 @@ public class OptionToggleButton extends StackPane {
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
JFXToggleButton toggleButton = new JFXToggleButton();
|
||||
pane.setRight(toggleButton);
|
||||
StackPane right = new StackPane(toggleButton);
|
||||
right.setAlignment(Pos.CENTER);
|
||||
pane.setRight(right);
|
||||
toggleButton.selectedProperty().bindBidirectional(selected);
|
||||
toggleButton.setSize(8);
|
||||
FXUtils.setLimitHeight(toggleButton, 30);
|
||||
|
||||
container.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
toggleButton.setSelected(!toggleButton.isSelected());
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
|
||||
|
||||
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
|
||||
if (StringUtils.isNotBlank(subtitle)) {
|
||||
|
||||
@@ -144,12 +144,20 @@ public class RipplerContainer extends StackPane {
|
||||
}
|
||||
|
||||
protected void updateChildren() {
|
||||
getChildren().addAll(buttonContainer, getContainer());
|
||||
if (buttonRippler.getPosition() == JFXRippler.RipplerPos.BACK)
|
||||
getChildren().setAll(buttonContainer, getContainer());
|
||||
else
|
||||
getChildren().setAll(getContainer(), buttonContainer);
|
||||
|
||||
for (int i = 1; i < getChildren().size(); ++i)
|
||||
getChildren().get(i).setPickOnBounds(false);
|
||||
}
|
||||
|
||||
public void setPosition(JFXRippler.RipplerPos pos) {
|
||||
buttonRippler.setPosition(pos);
|
||||
updateChildren();
|
||||
}
|
||||
|
||||
public Node getContainer() {
|
||||
return container.get();
|
||||
}
|
||||
|
||||
@@ -92,8 +92,6 @@ public final class TaskListPane extends StackPane {
|
||||
private final ObjectProperty<Insets> progressNodePadding = new SimpleObjectProperty<>(Insets.EMPTY);
|
||||
private final DoubleProperty cellWidth = new SimpleDoubleProperty();
|
||||
|
||||
private Cell lastCell;
|
||||
|
||||
public TaskListPane() {
|
||||
listView.setPadding(new Insets(12, 0, 0, 0));
|
||||
listView.setCellFactory(l -> new Cell());
|
||||
@@ -316,11 +314,6 @@ public final class TaskListPane extends StackPane {
|
||||
protected void updateItem(Node item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell && !isVisible())
|
||||
return;
|
||||
lastCell = this;
|
||||
|
||||
pane.paddingProperty().unbind();
|
||||
title.textProperty().unbind();
|
||||
message.textProperty().unbind();
|
||||
|
||||
@@ -17,18 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXDialog;
|
||||
import com.jfoenix.controls.JFXSnackbar;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -48,14 +43,15 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogUtils;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
|
||||
import org.jackhuang.hmcl.ui.animation.*;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -66,7 +62,9 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
@@ -77,14 +75,9 @@ import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class DecoratorController {
|
||||
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
|
||||
|
||||
private final Decorator decorator;
|
||||
private final Navigator navigator;
|
||||
|
||||
private JFXDialog dialog;
|
||||
private JFXDialogPane dialogPane;
|
||||
|
||||
public DecoratorController(Stage stage, Node mainPage) {
|
||||
decorator = new Decorator(stage);
|
||||
decorator.setOnCloseButtonAction(() -> {
|
||||
@@ -134,19 +127,24 @@ public class DecoratorController {
|
||||
|
||||
// pass key events to current dialog / current page
|
||||
decorator.addEventFilter(KeyEvent.ANY, e -> {
|
||||
if (!(e.getTarget() instanceof Node)) {
|
||||
return; // event source can't be determined
|
||||
if (!(e.getTarget() instanceof Node t)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Node newTarget;
|
||||
if (dialogPane != null && dialogPane.peek().isPresent()) {
|
||||
newTarget = dialogPane.peek().get(); // current dialog
|
||||
} else {
|
||||
newTarget = navigator.getCurrentPage(); // current page
|
||||
|
||||
JFXDialogPane currentDialogPane = null;
|
||||
if (decorator.getDrawerWrapper() != null) {
|
||||
currentDialogPane = (JFXDialogPane) decorator.getDrawerWrapper().getProperties().get(DialogUtils.PROPERTY_DIALOG_PANE_INSTANCE);
|
||||
}
|
||||
|
||||
if (currentDialogPane != null && currentDialogPane.peek().isPresent()) {
|
||||
newTarget = currentDialogPane.peek().get();
|
||||
} else {
|
||||
newTarget = navigator.getCurrentPage();
|
||||
}
|
||||
boolean needsRedirect = true;
|
||||
Node t = (Node) e.getTarget();
|
||||
|
||||
while (t != null) {
|
||||
if (t == newTarget) {
|
||||
// current event target is in newTarget
|
||||
@@ -266,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,
|
||||
@@ -445,81 +443,12 @@ public class DecoratorController {
|
||||
}
|
||||
|
||||
// ==== Dialog ====
|
||||
|
||||
public void showDialog(Node node) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
if (dialog == null) {
|
||||
if (decorator.getDrawerWrapper() == null) {
|
||||
// Sometimes showDialog will be invoked before decorator was initialized.
|
||||
// Keep trying again.
|
||||
Platform.runLater(() -> showDialog(node));
|
||||
return;
|
||||
}
|
||||
dialog = new JFXDialog(AnimationUtils.isAnimationEnabled()
|
||||
? JFXDialog.DialogTransition.CENTER
|
||||
: JFXDialog.DialogTransition.NONE);
|
||||
dialogPane = new JFXDialogPane();
|
||||
|
||||
dialog.setContent(dialogPane);
|
||||
decorator.capableDraggingWindow(dialog);
|
||||
decorator.forbidDraggingWindow(dialogPane);
|
||||
dialog.setDialogContainer(decorator.getDrawerWrapper());
|
||||
dialog.setOverlayClose(false);
|
||||
dialog.show();
|
||||
|
||||
navigator.setDisable(true);
|
||||
}
|
||||
dialogPane.push(node);
|
||||
|
||||
EventHandler<DialogCloseEvent> handler = event -> closeDialog(node);
|
||||
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
|
||||
node.addEventHandler(DialogCloseEvent.CLOSE, handler);
|
||||
|
||||
if (dialog.isVisible()) {
|
||||
dialog.requestFocus();
|
||||
if (node instanceof DialogAware)
|
||||
((DialogAware) node).onDialogShown();
|
||||
} else {
|
||||
dialog.visibleProperty().addListener(new ChangeListener<Boolean>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
if (newValue) {
|
||||
dialog.requestFocus();
|
||||
if (node instanceof DialogAware)
|
||||
((DialogAware) node).onDialogShown();
|
||||
observable.removeListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
DialogUtils.show(decorator, node);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void closeDialog(Node node) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
|
||||
.ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
|
||||
|
||||
if (dialog != null) {
|
||||
JFXDialogPane pane = dialogPane;
|
||||
|
||||
if (pane.size() == 1 && pane.peek().orElse(null) == node) {
|
||||
dialog.setOnDialogClosed(e -> pane.pop(node));
|
||||
dialog.close();
|
||||
dialog = null;
|
||||
dialogPane = null;
|
||||
|
||||
navigator.setDisable(false);
|
||||
} else {
|
||||
pane.pop(node);
|
||||
}
|
||||
|
||||
if (node instanceof DialogAware) {
|
||||
((DialogAware) node).onDialogClosed();
|
||||
}
|
||||
}
|
||||
DialogUtils.close(node);
|
||||
}
|
||||
|
||||
// ==== Toast ====
|
||||
|
||||
@@ -68,7 +68,7 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
gameVersion,
|
||||
downloadProvider,
|
||||
libraryId,
|
||||
() -> controller.onPrev(false, Navigation.NavigationDirection.NEXT)
|
||||
() -> controller.onPrev(false, Navigation.NavigationDirection.PREVIOUS)
|
||||
), Navigation.NavigationDirection.NEXT
|
||||
);
|
||||
});
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
@@ -40,6 +39,7 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
|
||||
@@ -84,8 +84,8 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
|
||||
"game", versionPageNavigator::onGameSelected)));
|
||||
modpackTab.setNodeSupplier(loadVersionFor(() -> {
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((profile, __, file) -> {
|
||||
Versions.downloadModpackImpl(profile, uploadVersion, file);
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((profile, __, mod, file) -> {
|
||||
Versions.downloadModpackImpl(profile, uploadVersion, mod, file);
|
||||
}, false);
|
||||
|
||||
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
|
||||
@@ -94,9 +94,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
page.getActions().add(installLocalModpackButton);
|
||||
return page;
|
||||
}));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, file) -> download(profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
shaderTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(ModrinthRemoteModRepository.SHADER_PACKS, (profile, version, file) -> download(profile, version, file, "shaderpacks"), true)));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, mod, file) -> download(profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, mod, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((profile, version, mod, file) -> download(profile, version, file, "shaderpacks"), true)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
|
||||
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
|
||||
|
||||
@@ -135,10 +135,6 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
|
||||
|
||||
Controllers.prompt(i18n("archive.file.name"), (result, resolve, reject) -> {
|
||||
if (!FileUtils.isNameValid(result)) {
|
||||
reject.accept(i18n("install.new_game.malformed"));
|
||||
return;
|
||||
}
|
||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||
|
||||
Controllers.taskDialog(Task.composeAsync(() -> {
|
||||
@@ -156,9 +152,8 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
Controllers.showToast(i18n("install.success"));
|
||||
}
|
||||
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
|
||||
resolve.run();
|
||||
}, file.getFile().getFilename());
|
||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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(), "");
|
||||
|
||||
@@ -180,4 +180,5 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
public static final SettingsMap.Key<Modpack> MODPACK_MANIFEST = new SettingsMap.Key<>("MODPACK_MANIFEST");
|
||||
public static final SettingsMap.Key<Charset> MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET");
|
||||
public static final SettingsMap.Key<Boolean> MODPACK_MANUALLY_CREATED = new SettingsMap.Key<>("MODPACK_MANUALLY_CREATED");
|
||||
public static final SettingsMap.Key<String> MODPACK_ICON_URL = new SettingsMap.Key<>("MODPACK_ICON_URL");
|
||||
}
|
||||
+10
-1
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@@ -45,6 +46,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
private final Profile profile;
|
||||
private final Path file;
|
||||
private final String updateVersion;
|
||||
private String iconUrl;
|
||||
|
||||
public ModpackInstallWizardProvider(Profile profile) {
|
||||
this(profile, null, null);
|
||||
@@ -64,12 +66,18 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
this.updateVersion = updateVersion;
|
||||
}
|
||||
|
||||
public void setIconUrl(String iconUrl) {
|
||||
this.iconUrl = iconUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(SettingsMap settings) {
|
||||
if (file != null)
|
||||
settings.put(LocalModpackPage.MODPACK_FILE, file);
|
||||
if (updateVersion != null)
|
||||
settings.put(LocalModpackPage.MODPACK_NAME, updateVersion);
|
||||
if (StringUtils.isNotBlank(iconUrl))
|
||||
settings.put(LocalModpackPage.MODPACK_ICON_URL, iconUrl);
|
||||
settings.put(ModpackPage.PROFILE, profile);
|
||||
}
|
||||
|
||||
@@ -78,6 +86,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
ServerModpackManifest serverModpackManifest = settings.get(RemoteModpackPage.MODPACK_SERVER_MANIFEST);
|
||||
Modpack modpack = settings.get(LocalModpackPage.MODPACK_MANIFEST);
|
||||
String name = settings.get(LocalModpackPage.MODPACK_NAME);
|
||||
String iconUrl = settings.get(LocalModpackPage.MODPACK_ICON_URL);
|
||||
Charset charset = settings.get(LocalModpackPage.MODPACK_CHARSET);
|
||||
boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false);
|
||||
|
||||
@@ -111,7 +120,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
return ModpackHelper.getInstallTask(profile, serverModpackManifest, name, modpack)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
|
||||
} else {
|
||||
return ModpackHelper.getInstallTask(profile, selected, name, modpack)
|
||||
return ModpackHelper.getInstallTask(profile, selected, name, modpack, iconUrl)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,14 @@ import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.URLValidator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
@@ -136,7 +139,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
} else {
|
||||
reject.accept(e.getMessage());
|
||||
}
|
||||
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
} else {
|
||||
// otherwise we still consider the file as modpack zip file
|
||||
// since casually the url may not ends with ".zip"
|
||||
@@ -153,7 +156,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
} else {
|
||||
reject.accept(e.getMessage());
|
||||
}
|
||||
}).executor(true),
|
||||
}),
|
||||
i18n("message.downloading"),
|
||||
TaskCancellationAction.NORMAL
|
||||
);
|
||||
@@ -161,7 +164,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
} catch (IOException e) {
|
||||
reject.accept(e.getMessage());
|
||||
}
|
||||
});
|
||||
}, "", new URLValidator());
|
||||
}
|
||||
|
||||
public void onChooseRepository() {
|
||||
|
||||
@@ -57,7 +57,6 @@ import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.NativePatcher;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -164,10 +163,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
private final ImageView imageView = new ImageView();
|
||||
private final StackPane pane = new StackPane();
|
||||
|
||||
private final Holder<RemoteVersionListCell> lastCell;
|
||||
|
||||
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell, VersionsPage control) {
|
||||
this.lastCell = lastCell;
|
||||
RemoteVersionListCell(VersionsPage control) {
|
||||
this.control = control;
|
||||
|
||||
HBox hbox = new HBox(16);
|
||||
@@ -179,10 +175,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
{
|
||||
if ("game".equals(control.libraryId)) {
|
||||
JFXButton wikiButton = newToggleButton4(SVG.GLOBE_BOOK);
|
||||
wikiButton.setOnAction(event -> {
|
||||
onOpenWiki();
|
||||
FXUtils.clearFocus(wikiButton);
|
||||
});
|
||||
wikiButton.setOnAction(event -> onOpenWiki());
|
||||
FXUtils.installFastTooltip(wikiButton, i18n("wiki.tooltip"));
|
||||
actions.getChildren().add(wikiButton);
|
||||
}
|
||||
@@ -222,11 +215,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
super.updateItem(remoteVersion, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
return;
|
||||
@@ -398,8 +386,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|
||||
control.versions.addListener((InvalidationListener) o -> updateList());
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control));
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(control));
|
||||
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXTreeView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.CheckBoxTreeItem;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeItem;
|
||||
@@ -43,8 +43,8 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -68,6 +68,8 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
rootNode = getTreeItem(profile.getRepository().getRunDirectory(version), "minecraft");
|
||||
treeView.setRoot(rootNode);
|
||||
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
onEscPressed(treeView, () -> controller.onPrev(true));
|
||||
setMargin(treeView, new Insets(10, 10, 5, 10));
|
||||
this.setCenter(treeView);
|
||||
|
||||
HBox nextPane = new HBox();
|
||||
@@ -93,9 +95,22 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
ModAdviser.ModSuggestion state = ModAdviser.ModSuggestion.SUGGESTED;
|
||||
if (basePath.length() > "minecraft/".length()) {
|
||||
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (isDirectory ? "/" : ""), isDirectory);
|
||||
if (!isDirectory && Objects.equals(FileUtils.getNameWithoutExtension(file), version))
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
if (isDirectory && Objects.equals(FileUtils.getName(file), version + "-natives")) // Ignore <version>-natives
|
||||
|
||||
String fileName = FileUtils.getName(file);
|
||||
|
||||
if (!isDirectory) {
|
||||
switch (fileName) {
|
||||
case ".DS_Store", // macOS system file
|
||||
"desktop.ini", "Thumbs.db" // Windows system files
|
||||
-> state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
}
|
||||
if (fileName.startsWith("._")) // macOS system file
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
if (FileUtils.getNameWithoutExtension(file).equals(version))
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
}
|
||||
|
||||
if (isDirectory && fileName.equals(version + "-natives")) // Ignore <version>-natives
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
if (state == ModAdviser.ModSuggestion.HIDDEN)
|
||||
return null;
|
||||
@@ -130,15 +145,14 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
}
|
||||
|
||||
HBox graphic = new HBox();
|
||||
CheckBox checkBox = new CheckBox();
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
checkBox.selectedProperty().bindBidirectional(node.selectedProperty());
|
||||
checkBox.indeterminateProperty().bindBidirectional(node.indeterminateProperty());
|
||||
graphic.getChildren().add(checkBox);
|
||||
|
||||
if (TRANSLATION.containsKey(basePath)) {
|
||||
Label comment = new Label();
|
||||
comment.setText(TRANSLATION.get(basePath));
|
||||
comment.setStyle("-fx-text-fill: gray;");
|
||||
Label comment = new Label(TRANSLATION.get(basePath));
|
||||
comment.setStyle("-fx-text-fill: -monet-on-surface-variant;");
|
||||
comment.setMouseTransparent(true);
|
||||
graphic.getChildren().add(comment);
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ package org.jackhuang.hmcl.ui.export;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
@@ -77,8 +77,7 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
graphic.setMouseTransparent(true);
|
||||
graphic.setLeft(new TwoLineListItem(i18n("modpack.type." + type), i18n("modpack.type." + type + ".export")));
|
||||
|
||||
SVGPath arrow = new SVGPath();
|
||||
arrow.setContent(SVG.ARROW_FORWARD.getPath());
|
||||
Node arrow = SVG.ARROW_FORWARD.createIcon();
|
||||
BorderPane.setAlignment(arrow, Pos.CENTER);
|
||||
graphic.setRight(arrow);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -22,14 +22,12 @@ import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -39,7 +37,6 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -60,10 +57,10 @@ import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameItem;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
@@ -74,7 +71,6 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -93,16 +89,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
private final PopupMenu menu = new PopupMenu();
|
||||
|
||||
private final StackPane popupWrapper = new StackPane(menu);
|
||||
private final JFXPopup popup = new JFXPopup(popupWrapper);
|
||||
|
||||
private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame");
|
||||
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
|
||||
private final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>(this, "latestVersion");
|
||||
private final ObservableList<Version> versions = FXCollections.observableArrayList();
|
||||
private final ObservableList<Node> versionNodes;
|
||||
private Profile profile;
|
||||
|
||||
private TransitionPane announcementPane;
|
||||
@@ -207,10 +197,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
updatePane.getChildren().setAll(hBox, closeUpdateButton);
|
||||
}
|
||||
|
||||
StackPane launchPane = new StackPane();
|
||||
HBox launchPane = new HBox();
|
||||
launchPane.getStyleClass().add("launch-pane");
|
||||
launchPane.setMaxWidth(230);
|
||||
launchPane.setMaxHeight(55);
|
||||
FXUtils.onScroll(launchPane, versions, list -> {
|
||||
String currentId = getCurrentGame();
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
@@ -219,16 +207,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
|
||||
{
|
||||
JFXButton launchButton = new JFXButton();
|
||||
launchButton.setPrefWidth(230);
|
||||
launchButton.setPrefHeight(55);
|
||||
//launchButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
launchButton.getStyleClass().add("launch-button");
|
||||
launchButton.setDefaultButton(true);
|
||||
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
|
||||
{
|
||||
VBox graphic = new VBox();
|
||||
graphic.setAlignment(Pos.CENTER);
|
||||
graphic.setTranslateX(-7);
|
||||
graphic.setMaxWidth(200);
|
||||
Label launchLabel = new Label();
|
||||
launchLabel.setStyle("-fx-font-size: 16px;");
|
||||
Label currentLabel = new Label();
|
||||
@@ -261,26 +244,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
launchButton.setGraphic(graphic);
|
||||
}
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
separator.setWidth(1);
|
||||
separator.setHeight(57);
|
||||
separator.setTranslateX(95);
|
||||
separator.setMouseTransparent(true);
|
||||
|
||||
menuButton = new JFXButton();
|
||||
menuButton.setPrefHeight(55);
|
||||
menuButton.setPrefWidth(230);
|
||||
//menuButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
menuButton.setStyle("-fx-font-size: 15px;");
|
||||
menuButton.getStyleClass().add("menu-button");
|
||||
menuButton.setOnAction(e -> onMenu());
|
||||
menuButton.setClip(new Rectangle(211, -100, 100, 200));
|
||||
StackPane graphic = new StackPane();
|
||||
Node svg = SVG.ARROW_DROP_UP.createIcon(30);
|
||||
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
|
||||
graphic.getChildren().setAll(svg);
|
||||
graphic.setTranslateX(6);
|
||||
FXUtils.installFastTooltip(menuButton, i18n("version.switch"));
|
||||
menuButton.setGraphic(graphic);
|
||||
menuButton.setGraphic(SVG.ARROW_DROP_UP.createIcon(30));
|
||||
|
||||
EventHandler<MouseEvent> secondaryClickHandle = event -> {
|
||||
if (event.getButton() == MouseButton.SECONDARY && event.getClickCount() == 1) {
|
||||
@@ -291,24 +259,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
launchButton.addEventHandler(MouseEvent.MOUSE_CLICKED, secondaryClickHandle);
|
||||
menuButton.addEventHandler(MouseEvent.MOUSE_CLICKED, secondaryClickHandle);
|
||||
|
||||
launchPane.getChildren().setAll(launchButton, separator, menuButton);
|
||||
launchPane.getChildren().setAll(launchButton, menuButton);
|
||||
}
|
||||
|
||||
getChildren().addAll(updatePane, launchPane);
|
||||
|
||||
menu.setMaxHeight(365);
|
||||
menu.setMaxWidth(545);
|
||||
menu.setAlwaysShowingVBar(true);
|
||||
FXUtils.onClicked(menu, popup::hide);
|
||||
versionNodes = MappedObservableList.create(versions, version -> {
|
||||
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
|
||||
FXUtils.onClicked(node, () -> {
|
||||
profile.setSelectedVersion(version.getId());
|
||||
popup.hide();
|
||||
});
|
||||
return node;
|
||||
});
|
||||
Bindings.bindContent(menu.getContent(), versionNodes);
|
||||
}
|
||||
|
||||
private void showUpdate(boolean show) {
|
||||
@@ -388,20 +343,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
private void onMenu() {
|
||||
Node contentNode;
|
||||
if (menu.getContent().isEmpty()) {
|
||||
Label placeholder = new Label(i18n("version.empty"));
|
||||
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: gray; -fx-font-style: italic;");
|
||||
contentNode = placeholder;
|
||||
} else {
|
||||
contentNode = menu;
|
||||
}
|
||||
|
||||
popupWrapper.getChildren().setAll(contentNode);
|
||||
|
||||
if (popup.isShowing()) {
|
||||
popup.hide();
|
||||
}
|
||||
GameListPopupMenu menu = new GameListPopupMenu();
|
||||
menu.getItems().setAll(versions.stream().map(it -> new GameItem(profile, it.getId())).toList());
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
popup.show(
|
||||
menuButton,
|
||||
JFXPopup.PopupVPosition.BOTTOM,
|
||||
|
||||
@@ -126,6 +126,7 @@ public class PersonalizationPage extends StackPane {
|
||||
themeList.getContent().add(animationButton);
|
||||
animationButton.selectedProperty().bindBidirectional(config().animationDisabledProperty());
|
||||
animationButton.setTitle(i18n("settings.launcher.turn_off_animations"));
|
||||
animationButton.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
}
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")), themeList);
|
||||
|
||||
@@ -172,6 +173,7 @@ public class PersonalizationPage extends StackPane {
|
||||
slider.setMinorTickCount(1);
|
||||
slider.setBlockIncrement(5);
|
||||
slider.setSnapToTicks(true);
|
||||
slider.setPadding(new Insets(9, 0, 0, 0));
|
||||
HBox.setHgrow(slider, Priority.ALWAYS);
|
||||
|
||||
if (config().getBackgroundImageType() == EnumBackgroundImage.TRANSLUCENT) {
|
||||
@@ -236,7 +238,14 @@ public class PersonalizationPage extends StackPane {
|
||||
.fallbackTo(12.0)
|
||||
.asPredicate(Validator.addTo(txtLogFontSize)));
|
||||
|
||||
hBox.getChildren().setAll(cboLogFont, txtLogFontSize);
|
||||
JFXButton clearButton = new JFXButton();
|
||||
clearButton.getStyleClass().add("toggle-icon4");
|
||||
clearButton.setGraphic(SVG.RESTORE.createIcon());
|
||||
clearButton.setOnAction(e -> cboLogFont.setValue(null));
|
||||
|
||||
FXUtils.installFastTooltip(clearButton, i18n("button.reset"));
|
||||
|
||||
hBox.getChildren().setAll(cboLogFont, txtLogFontSize, clearButton);
|
||||
|
||||
borderPane.setRight(hBox);
|
||||
}
|
||||
@@ -285,6 +294,8 @@ public class PersonalizationPage extends StackPane {
|
||||
clearButton.setGraphic(SVG.RESTORE.createIcon());
|
||||
clearButton.setOnAction(e -> cboFont.setValue(null));
|
||||
|
||||
FXUtils.installFastTooltip(clearButton, i18n("button.reset"));
|
||||
|
||||
hBox.getChildren().setAll(cboFont, clearButton);
|
||||
|
||||
borderPane.setRight(hBox);
|
||||
@@ -299,8 +310,12 @@ public class PersonalizationPage extends StackPane {
|
||||
{
|
||||
BorderPane fontAntiAliasingPane = new BorderPane();
|
||||
{
|
||||
Label left = new Label(i18n("settings.launcher.font.anti_aliasing"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
VBox left = new VBox();
|
||||
Label title = new Label(i18n("settings.launcher.font.anti_aliasing"));
|
||||
title.getStyleClass().add("title");
|
||||
Label subtitle = new Label(i18n("settings.take_effect_after_restart"));
|
||||
subtitle.getStyleClass().add("subtitle");
|
||||
left.getChildren().setAll(title, subtitle);
|
||||
fontAntiAliasingPane.setLeft(left);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
@@ -149,8 +150,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
|
||||
accountListItem.setOnMouseClicked(e -> {
|
||||
if (e.getButton() == MouseButton.SECONDARY) {
|
||||
if (!Accounts.getAccounts().isEmpty())
|
||||
showAccountListPopupMenu(accountListItem);
|
||||
showAccountListPopupMenu(accountListItem);
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
@@ -265,13 +265,19 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
scrollPane.setPrefHeight(-1);
|
||||
scrollPane.setMaxHeight(260);
|
||||
|
||||
for (Account account : Accounts.getAccounts()) {
|
||||
AccountAdvancedListItem item = new AccountAdvancedListItem(account);
|
||||
item.setOnAction(e -> {
|
||||
Accounts.setSelectedAccount(account);
|
||||
popup.hide();
|
||||
});
|
||||
scrollPane.add(item);
|
||||
if (Accounts.getAccounts().isEmpty()) {
|
||||
Label placeholder = new Label(i18n("account.empty"));
|
||||
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;");
|
||||
scrollPane.add(placeholder);
|
||||
} else {
|
||||
for (Account account : Accounts.getAccounts()) {
|
||||
AccountAdvancedListItem item = new AccountAdvancedListItem(account);
|
||||
item.setOnAction(e -> {
|
||||
Accounts.setSelectedAccount(account);
|
||||
popup.hide();
|
||||
});
|
||||
scrollPane.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.getContent().add(scrollPane);
|
||||
@@ -293,7 +299,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
.thenApplyAsync(
|
||||
encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
|
||||
.thenApplyAsync(modpack -> ModpackHelper
|
||||
.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
|
||||
.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack, null)
|
||||
.executor())
|
||||
.thenAcceptAsync(Schedulers.javafx(), executor -> {
|
||||
Controllers.taskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL);
|
||||
|
||||
@@ -88,7 +88,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
this.setFitToWidth(true);
|
||||
|
||||
VBox rootPane = new VBox();
|
||||
rootPane.setPadding(new Insets(32, 10, 32, 10));
|
||||
rootPane.setPadding(new Insets(10));
|
||||
this.setContent(rootPane);
|
||||
FXUtils.smoothScrolling(this);
|
||||
|
||||
@@ -226,7 +226,6 @@ public final class SettingsPage extends ScrollPane {
|
||||
|
||||
{
|
||||
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
|
||||
fileCommonLocation.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
|
||||
@@ -234,6 +233,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
.setDirectory(true)
|
||||
.bindBidirectional(config().commonDirectoryProperty())
|
||||
));
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
|
||||
|
||||
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
|
||||
fileCommonLocationSublist.getContent().add(fileCommonLocation);
|
||||
@@ -254,8 +254,12 @@ public final class SettingsPage extends ScrollPane {
|
||||
{
|
||||
BorderPane languagePane = new BorderPane();
|
||||
|
||||
Label left = new Label(i18n("settings.launcher.language"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
VBox left = new VBox();
|
||||
Label title = new Label(i18n("settings.launcher.language"));
|
||||
title.getStyleClass().add("title");
|
||||
Label subtitle = new Label(i18n("settings.take_effect_after_restart"));
|
||||
subtitle.getStyleClass().add("subtitle");
|
||||
left.getChildren().setAll(title, subtitle);
|
||||
languagePane.setLeft(left);
|
||||
|
||||
SupportedLocale currentLocale = I18n.getLocale();
|
||||
|
||||
@@ -89,7 +89,10 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
setLoading(false);
|
||||
root.setCenter(new NBTTreeView(result));
|
||||
NBTTreeView view = new NBTTreeView(result);
|
||||
BorderPane.setMargin(view, new Insets(10));
|
||||
onEscPressed(view, cancelButton::fire);
|
||||
root.setCenter(view);
|
||||
} else {
|
||||
LOG.warning("Fail to open nbt file", exception);
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(exception), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
|
||||
|
||||
@@ -20,11 +20,13 @@ package org.jackhuang.hmcl.ui.nbt;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import javafx.scene.control.*;
|
||||
import com.jfoenix.controls.JFXTreeView;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.util.Callback;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.EnumMap;
|
||||
@@ -34,7 +36,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class NBTTreeView extends TreeView<Tag> {
|
||||
public final class NBTTreeView extends JFXTreeView<Tag> {
|
||||
|
||||
public NBTTreeView(NBTTreeView.Item tree) {
|
||||
this.setRoot(tree);
|
||||
@@ -42,10 +44,9 @@ public final class NBTTreeView extends TreeView<Tag> {
|
||||
}
|
||||
|
||||
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
|
||||
|
||||
return view -> new TreeCell<Tag>() {
|
||||
return view -> new TreeCell<>() {
|
||||
private void setTagText(String text) {
|
||||
String name = ((Item) getTreeItem()).getName();
|
||||
|
||||
@@ -66,11 +67,6 @@ public final class NBTTreeView extends TreeView<Tag> {
|
||||
public void updateItem(Tag item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
ImageView imageView = (ImageView) this.getGraphic();
|
||||
if (imageView == null) {
|
||||
imageView = new ImageView();
|
||||
@@ -85,6 +81,8 @@ public final class NBTTreeView extends TreeView<Tag> {
|
||||
|
||||
NBTTagType tagType = NBTTagType.typeOf(item);
|
||||
imageView.setImage(icons.computeIfAbsent(tagType, type -> new Image(type.getIconUrl())));
|
||||
imageView.setFitHeight(16);
|
||||
imageView.setFitWidth(16);
|
||||
|
||||
if (((Item) getTreeItem()).getText() != null) {
|
||||
setText(((Item) getTreeItem()).getText());
|
||||
|
||||
@@ -22,10 +22,9 @@ import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.RequiredFieldValidator;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
@@ -44,6 +43,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -53,7 +53,6 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
private final StringProperty location;
|
||||
private final Profile profile;
|
||||
|
||||
private final JFXTextField txtProfileName;
|
||||
private final FileItem gameDir;
|
||||
private final OptionToggleButton toggleUseRelativePath;
|
||||
@@ -147,6 +146,38 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
() -> !txtProfileName.validate() || StringUtils.isBlank(getLocation()),
|
||||
txtProfileName.textProperty(), location));
|
||||
}
|
||||
|
||||
ChangeListener<String> locationChangeListener = (observable, oldValue, newValue) -> {
|
||||
Path newPath;
|
||||
try {
|
||||
newPath = FileUtils.toAbsolute(Path.of(newValue));
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!".minecraft".equals(FileUtils.getName(newPath)))
|
||||
return;
|
||||
|
||||
Path parent = newPath.getParent();
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
String suggestedName = FileUtils.getName(parent);
|
||||
if (!suggestedName.isBlank()) {
|
||||
txtProfileName.setText(suggestedName);
|
||||
}
|
||||
};
|
||||
locationProperty().addListener(locationChangeListener);
|
||||
|
||||
txtProfileName.textProperty().addListener(new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||
if (txtProfileName.isFocused()) {
|
||||
txtProfileName.textProperty().removeListener(this);
|
||||
locationProperty().removeListener(locationChangeListener);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onSave() {
|
||||
|
||||
@@ -57,7 +57,13 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
@@ -96,6 +102,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
/* FIXME: It's sucked to have such a long logic, containing UI for all states defined in TerracottaState, with unclear control flows.
|
||||
Consider moving UI into multiple files for each state respectively. */
|
||||
public TerracottaControllerPage() {
|
||||
holder.add(FXUtils.observeWeak(() -> {
|
||||
// Run daemon process only if HMCL is focused and is displaying current node.
|
||||
TerracottaManager.switchDaemon(getScene() != null && Controllers.getStage().isFocused());
|
||||
}, this.sceneProperty(), Controllers.getStage().focusedProperty()));
|
||||
|
||||
TransitionPane transition = new TransitionPane();
|
||||
|
||||
ObjectProperty<String> statusProperty = new SimpleObjectProperty<>();
|
||||
@@ -107,7 +118,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
|
||||
if (state instanceof TerracottaState.Uninitialized ||
|
||||
state instanceof TerracottaState.Preparing preparing && preparing.hasInstallFence() ||
|
||||
state instanceof TerracottaState.Fatal fatal && fatal.getType() == TerracottaState.Fatal.Type.NETWORK
|
||||
state instanceof TerracottaState.Fatal fatal && fatal.isRecoverable()
|
||||
) {
|
||||
return Files.isReadable(path) && FileUtils.getName(path).toLowerCase(Locale.ROOT).endsWith(".tar.gz");
|
||||
} else {
|
||||
@@ -116,7 +127,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
}, files -> {
|
||||
Path path = files.get(0);
|
||||
|
||||
if (!TerracottaManager.validate(path)) {
|
||||
if (TerracottaManager.isInvalidBundle(path)) {
|
||||
Controllers.dialog(
|
||||
i18n("terracotta.from_local.file_name_mismatch", TerracottaMetadata.PACKAGE_NAME, FileUtils.getName(path)),
|
||||
i18n("message.error"),
|
||||
@@ -125,25 +136,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
return;
|
||||
}
|
||||
|
||||
TerracottaState state = UI_STATE.get(), next;
|
||||
if (state instanceof TerracottaState.Uninitialized || state instanceof TerracottaState.Preparing preparing && preparing.hasInstallFence()) {
|
||||
if (state instanceof TerracottaState.Uninitialized uninitialized && !uninitialized.hasLegacy()) {
|
||||
Controllers.confirmWithCountdown(i18n("terracotta.confirm.desc"), i18n("terracotta.confirm.title"), 5,
|
||||
MessageDialogPane.MessageType.INFO, () -> {
|
||||
TerracottaState.Preparing s = TerracottaManager.install(path);
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
}, null);
|
||||
return;
|
||||
}
|
||||
|
||||
next = TerracottaManager.install(path);
|
||||
} else if (state instanceof TerracottaState.Fatal fatal && fatal.getType() == TerracottaState.Fatal.Type.NETWORK) {
|
||||
next = TerracottaManager.recover(path);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
TerracottaState.Preparing next = TerracottaManager.install(path);
|
||||
if (next != null) {
|
||||
UI_STATE.set(next);
|
||||
}
|
||||
@@ -176,7 +169,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
download.setSubtitle(i18n("terracotta.status.uninitialized.desc"));
|
||||
download.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(download, () -> {
|
||||
TerracottaState.Preparing s = TerracottaManager.install(null);
|
||||
TerracottaState.Preparing s = TerracottaManager.download();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
@@ -253,7 +246,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
guest.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(guest, () -> {
|
||||
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, resolve, reject) -> {
|
||||
Task<TerracottaState.GuestStarting> task = TerracottaManager.setGuesting(code);
|
||||
Task<TerracottaState.GuestConnecting> task = TerracottaManager.setGuesting(code);
|
||||
if (task != null) {
|
||||
task.whenComplete(Schedulers.javafx(), (s, e) -> {
|
||||
if (e != null) {
|
||||
@@ -372,7 +365,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
nodesProperty.setAll(code, copy, back, new PlayerProfileUI(hostOK.getProfiles()));
|
||||
}
|
||||
}
|
||||
} else if (state instanceof TerracottaState.GuestStarting) {
|
||||
} else if (state instanceof TerracottaState.GuestConnecting || state instanceof TerracottaState.GuestStarting) {
|
||||
statusProperty.set(i18n("terracotta.status.guest_starting"));
|
||||
progressProperty.set(-1);
|
||||
|
||||
@@ -387,7 +380,26 @@ public class TerracottaControllerPage extends StackPane {
|
||||
}
|
||||
});
|
||||
|
||||
nodesProperty.setAll(room);
|
||||
nodesProperty.clear();
|
||||
if (state instanceof TerracottaState.GuestStarting) {
|
||||
TerracottaState.GuestStarting.Difficulty difficulty = ((TerracottaState.GuestStarting) state).getDifficulty();
|
||||
if (difficulty != null && difficulty != TerracottaState.GuestStarting.Difficulty.UNKNOWN) {
|
||||
LineButton info = LineButton.of();
|
||||
info.setLeftIcon(switch (difficulty) {
|
||||
case UNKNOWN -> throw new AssertionError();
|
||||
case EASIEST, SIMPLE -> SVG.INFO;
|
||||
case MEDIUM, TOUGH -> SVG.WARNING;
|
||||
});
|
||||
|
||||
String difficultyID = difficulty.name().toLowerCase(Locale.ROOT);
|
||||
info.setTitle(i18n(String.format("terracotta.difficulty.%s", difficultyID)));
|
||||
info.setSubtitle(i18n("terracotta.difficulty.estimate_only"));
|
||||
|
||||
nodesProperty.add(info);
|
||||
}
|
||||
}
|
||||
|
||||
nodesProperty.add(room);
|
||||
} else if (state instanceof TerracottaState.GuestOK guestOK) {
|
||||
if (guestOK.isForkOf(legacyState)) {
|
||||
if (nodesProperty.get(nodesProperty.size() - 1) instanceof PlayerProfileUI profileUI) {
|
||||
@@ -486,7 +498,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
retry.setTitle(i18n("terracotta.status.fatal.retry"));
|
||||
retry.setSubtitle(message);
|
||||
FXUtils.onClicked(retry, () -> {
|
||||
TerracottaState s = TerracottaManager.recover(null);
|
||||
TerracottaState s = TerracottaManager.recover();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
|
||||
@@ -85,8 +85,7 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
}, it -> mainPage.getProfile().setSelectedVersion(it.getId()));
|
||||
})
|
||||
.addNavigationDrawerItem(i18n("terracotta.feedback.title"), SVG.FEEDBACK, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK))
|
||||
.addNavigationDrawerItem(i18n("terracotta.easytier"), SVG.HOST, () -> FXUtils.openLink("https://easytier.cn/"));
|
||||
.addNavigationDrawerItem(i18n("terracotta.feedback.title"), SVG.FEEDBACK, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
left.setBottom(toolbar);
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
getChildren().setAll(scrollPane);
|
||||
|
||||
VBox rootPane = new VBox();
|
||||
rootPane.setFocusTraversable(true);
|
||||
rootPane.setFillWidth(true);
|
||||
scrollPane.setContent(rootPane);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
|
||||
@@ -48,14 +48,12 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
|
||||
public DatapackListPage(WorldManagePage worldManagePage) {
|
||||
this.worldDir = worldManagePage.getWorld().getFile();
|
||||
|
||||
datapack = new Datapack(worldDir.resolve("datapacks"));
|
||||
datapack.loadFromDir();
|
||||
|
||||
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
|
||||
|
||||
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
|
||||
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void installSingleDatapack(Path datapack) {
|
||||
@@ -74,9 +72,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
public void refresh() {
|
||||
setLoading(true);
|
||||
Task.runAsync(datapack::loadFromDir)
|
||||
.withRunAsync(Schedulers.javafx(), () -> {
|
||||
setLoading(false);
|
||||
})
|
||||
.withRunAsync(Schedulers.javafx(), () -> setLoading(false))
|
||||
.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.MDListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -180,8 +179,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView, lastCell));
|
||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
this.listView.setItems(filteredList);
|
||||
|
||||
@@ -304,8 +302,8 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
final TwoLineListItem content = new TwoLineListItem();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, Holder<Object> lastCell) {
|
||||
super(listView, lastCell);
|
||||
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView) {
|
||||
super(listView);
|
||||
|
||||
HBox container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
|
||||
@@ -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<>();
|
||||
@@ -208,6 +199,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
: i18n("curse.category." + category);
|
||||
}
|
||||
|
||||
protected boolean shouldDisplayCategory(String category) {
|
||||
return !"minecraft".equals(category);
|
||||
}
|
||||
|
||||
private String getLocalizedCategoryIndent(ModDownloadListPageSkin.CategoryIndented category) {
|
||||
return StringUtils.repeats(' ', category.indent * 4) +
|
||||
(category.getCategory() == null
|
||||
@@ -243,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);
|
||||
@@ -373,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);
|
||||
}
|
||||
@@ -532,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();
|
||||
@@ -556,69 +558,13 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : dataItem.getTitle());
|
||||
content.setSubtitle(dataItem.getDescription());
|
||||
content.getTags().clear();
|
||||
dataItem.getCategories().stream()
|
||||
.map(category -> getSkinnable().getLocalizedCategory(category))
|
||||
.forEach(content::addTag);
|
||||
loadIcon(dataItem);
|
||||
for (String category : dataItem.getCategories()) {
|
||||
if (getSkinnable().shouldDisplayCategory(category))
|
||||
content.addTag(getSkinnable().getLocalizedCategory(category));
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -157,15 +157,15 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
this.failed.set(failed);
|
||||
}
|
||||
|
||||
public void download(RemoteMod.Version file) {
|
||||
public void download(RemoteMod mod, RemoteMod.Version file) {
|
||||
if (this.callback == null) {
|
||||
saveAs(file);
|
||||
saveAs(mod, file);
|
||||
} else {
|
||||
this.callback.download(version.getProfile(), version.getVersion(), file);
|
||||
this.callback.download(version.getProfile(), version.getVersion(), mod, file);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveAs(RemoteMod.Version file) {
|
||||
public void saveAs(RemoteMod mod, RemoteMod.Version file) {
|
||||
String extension = StringUtils.substringAfterLast(file.getFile().getFilename(), '.');
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
@@ -287,7 +287,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
if (targetLoaders.contains(loader)) {
|
||||
list.getContent().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)),
|
||||
new ModItem(modVersion, control)
|
||||
new ModItem(control.addon, modVersion, control)
|
||||
);
|
||||
break resolve;
|
||||
}
|
||||
@@ -308,7 +308,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
ComponentList sublist = new ComponentList(() -> {
|
||||
ArrayList<ModItem> items = new ArrayList<>(versions.size());
|
||||
for (RemoteMod.Version v : versions) {
|
||||
items.add(new ModItem(v, control));
|
||||
items.add(new ModItem(control.addon, v, control));
|
||||
}
|
||||
return items;
|
||||
});
|
||||
@@ -357,9 +357,10 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.getTitle());
|
||||
content.setSubtitle(addon.getDescription());
|
||||
addon.getCategories().stream()
|
||||
.map(page::getLocalizedCategory)
|
||||
.forEach(content::addTag);
|
||||
for (String category : addon.getCategories()) {
|
||||
if (page.shouldDisplayCategory(category))
|
||||
content.addTag(page.getLocalizedCategory(category));
|
||||
}
|
||||
if (StringUtils.isNotBlank(addon.getIconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 80, 80, true, true));
|
||||
}
|
||||
@@ -373,7 +374,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
private static final class ModItem extends StackPane {
|
||||
|
||||
ModItem(RemoteMod.Version dataItem, DownloadPage selfPage) {
|
||||
ModItem(RemoteMod mod, RemoteMod.Version dataItem, DownloadPage selfPage) {
|
||||
VBox pane = new VBox(8);
|
||||
pane.setPadding(new Insets(8, 0, 8, 0));
|
||||
|
||||
@@ -435,7 +436,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
FXUtils.onClicked(container, () -> Controllers.dialog(new ModVersion(dataItem, selfPage)));
|
||||
FXUtils.onClicked(container, () -> Controllers.dialog(new ModVersion(mod, dataItem, selfPage)));
|
||||
getChildren().setAll(container);
|
||||
|
||||
// Workaround for https://github.com/HMCL-dev/HMCL/issues/2129
|
||||
@@ -444,30 +445,21 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
|
||||
private static final class ModVersion extends JFXDialogLayout {
|
||||
public ModVersion(RemoteMod.Version version, DownloadPage selfPage) {
|
||||
public ModVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfPage) {
|
||||
RemoteModRepository.Type type = selfPage.repository.getType();
|
||||
|
||||
String title;
|
||||
switch (type) {
|
||||
case WORLD:
|
||||
title = "world.download.title";
|
||||
break;
|
||||
case MODPACK:
|
||||
title = "modpack.download.title";
|
||||
break;
|
||||
case RESOURCE_PACK:
|
||||
title = "resourcepack.download.title";
|
||||
break;
|
||||
case MOD:
|
||||
default:
|
||||
title = "mods.download.title";
|
||||
break;
|
||||
}
|
||||
String title = switch (type) {
|
||||
case WORLD -> "world.download.title";
|
||||
case MODPACK -> "modpack.download.title";
|
||||
case RESOURCE_PACK -> "resourcepack.download.title";
|
||||
case SHADER_PACK -> "shaderpack.download.title";
|
||||
default -> "mods.download.title";
|
||||
};
|
||||
this.setHeading(new HBox(new Label(i18n(title, version.getName()))));
|
||||
|
||||
VBox box = new VBox(8);
|
||||
box.setPadding(new Insets(8));
|
||||
ModItem modItem = new ModItem(version, selfPage);
|
||||
ModItem modItem = new ModItem(mod, version, selfPage);
|
||||
modItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again
|
||||
box.getChildren().setAll(modItem);
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
@@ -493,7 +485,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
if (type == RemoteModRepository.Type.MODPACK || !spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
selfPage.download(version);
|
||||
selfPage.download(mod, version);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -503,7 +495,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
selfPage.saveAs(version);
|
||||
selfPage.saveAs(mod, version);
|
||||
});
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
@@ -557,6 +549,6 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
|
||||
public interface DownloadCallback {
|
||||
void download(Profile profile, @Nullable String version, RemoteMod.Version file);
|
||||
void download(Profile profile, @Nullable String version, RemoteMod mod, RemoteMod.Version file);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -17,107 +17,122 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class GameItem extends Control {
|
||||
public class GameItem {
|
||||
private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 10, TimeUnit.SECONDS);
|
||||
|
||||
private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 1, TimeUnit.SECONDS);
|
||||
protected final Profile profile;
|
||||
protected final String id;
|
||||
|
||||
private final Profile profile;
|
||||
private final String version;
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final StringProperty tag = new SimpleStringProperty();
|
||||
private final StringProperty subtitle = new SimpleStringProperty();
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
|
||||
private boolean initialized = false;
|
||||
private StringProperty title;
|
||||
private StringProperty tag;
|
||||
private StringProperty subtitle;
|
||||
private ObjectProperty<Image> image;
|
||||
|
||||
public GameItem(Profile profile, String id) {
|
||||
this.profile = profile;
|
||||
this.version = id;
|
||||
|
||||
// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
|
||||
CompletableFuture.supplyAsync(() -> profile.getRepository().getGameVersion(id), POOL_VERSION_RESOLVE)
|
||||
.thenAcceptAsync(game -> {
|
||||
StringBuilder libraries = new StringBuilder(game.orElse(i18n("message.unknown")));
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), game.orElse(null));
|
||||
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||
String libraryId = mark.getLibraryId();
|
||||
String libraryVersion = mark.getLibraryVersion();
|
||||
if (libraryId.equals(MINECRAFT.getPatchId())) continue;
|
||||
if (I18n.hasKey("install.installer." + libraryId)) {
|
||||
libraries.append(", ").append(i18n("install.installer." + libraryId));
|
||||
if (libraryVersion != null)
|
||||
libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, ""));
|
||||
}
|
||||
}
|
||||
|
||||
subtitle.set(libraries.toString());
|
||||
}, Platform::runLater)
|
||||
.exceptionally(handleUncaught);
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(version);
|
||||
if (config == null) return;
|
||||
tag.set(config.getVersion());
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Failed to read modpack configuration from " + version, e);
|
||||
}
|
||||
}, Platform::runLater)
|
||||
.exceptionally(handleUncaught);
|
||||
|
||||
title.set(id);
|
||||
image.set(profile.getRepository().getVersionIconImage(version));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new GameItemSkin(this);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Profile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
private void init() {
|
||||
if (initialized)
|
||||
return;
|
||||
|
||||
initialized = true;
|
||||
title = new SimpleStringProperty();
|
||||
tag = new SimpleStringProperty();
|
||||
subtitle = new SimpleStringProperty();
|
||||
image = new SimpleObjectProperty<>();
|
||||
|
||||
record Result(@Nullable String gameVersion, @Nullable String tag) {
|
||||
}
|
||||
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(id);
|
||||
String modPackVersion = null;
|
||||
try {
|
||||
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(id);
|
||||
modPackVersion = config != null ? config.getVersion() : null;
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read modpack configuration from " + id, e);
|
||||
}
|
||||
return new Result(gameVersion.orElse(null), modPackVersion);
|
||||
}, POOL_VERSION_RESOLVE).whenCompleteAsync((result, exception) -> {
|
||||
if (exception == null) {
|
||||
if (result.tag != null) {
|
||||
tag.set(result.tag);
|
||||
}
|
||||
|
||||
StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown")));
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), result.gameVersion);
|
||||
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||
String libraryId = mark.getLibraryId();
|
||||
String libraryVersion = mark.getLibraryVersion();
|
||||
if (libraryId.equals(MINECRAFT.getPatchId())) continue;
|
||||
if (I18n.hasKey("install.installer." + libraryId)) {
|
||||
libraries.append(", ").append(i18n("install.installer." + libraryId));
|
||||
if (libraryVersion != null)
|
||||
libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, ""));
|
||||
}
|
||||
}
|
||||
|
||||
subtitle.set(libraries.toString());
|
||||
} else {
|
||||
LOG.warning("Failed to read version info from " + id, exception);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
|
||||
title.set(id);
|
||||
image.set(profile.getRepository().getVersionIconImage(id));
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty titleProperty() {
|
||||
init();
|
||||
return title;
|
||||
}
|
||||
|
||||
public StringProperty tagProperty() {
|
||||
public ReadOnlyStringProperty tagProperty() {
|
||||
init();
|
||||
return tag;
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
public ReadOnlyStringProperty subtitleProperty() {
|
||||
init();
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public ObjectProperty<Image> imageProperty() {
|
||||
public ReadOnlyObjectProperty<Image> imageProperty() {
|
||||
init();
|
||||
return image;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +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.geometry.Pos;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class GameItemSkin extends SkinBase<GameItem> {
|
||||
public GameItemSkin(GameItem skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
HBox center = new HBox();
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
StackPane imageViewContainer = new StackPane();
|
||||
FXUtils.setLimitWidth(imageViewContainer, 32);
|
||||
FXUtils.setLimitHeight(imageViewContainer, 32);
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
imageView.imageProperty().bind(skinnable.imageProperty());
|
||||
imageViewContainer.getChildren().setAll(imageView);
|
||||
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.titleProperty().bind(skinnable.titleProperty());
|
||||
FXUtils.onChangeAndOperate(skinnable.tagProperty(), tag -> {
|
||||
item.getTags().clear();
|
||||
if (StringUtils.isNotBlank(tag))
|
||||
item.addTag(tag);
|
||||
});
|
||||
item.subtitleProperty().bind(skinnable.subtitleProperty());
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
center.getChildren().setAll(imageView, item);
|
||||
getChildren().setAll(center);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class GameListCell extends ListCell<GameListItem> {
|
||||
|
||||
private final Region graphic;
|
||||
|
||||
private final ImageView imageView;
|
||||
private final TwoLineListItem content;
|
||||
|
||||
private final JFXRadioButton chkSelected;
|
||||
private final JFXButton btnUpgrade;
|
||||
private final JFXButton btnLaunch;
|
||||
private final JFXButton btnManage;
|
||||
|
||||
private final HBox right;
|
||||
|
||||
private final StringProperty tag = new SimpleStringProperty();
|
||||
|
||||
public GameListCell() {
|
||||
BorderPane root = new BorderPane();
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
root.setPadding(new Insets(8, 8, 8, 0));
|
||||
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
this.graphic = container;
|
||||
|
||||
{
|
||||
this.chkSelected = new JFXRadioButton() {
|
||||
@Override
|
||||
public void fire() {
|
||||
if (!isDisable() && !isSelected()) {
|
||||
fireEvent(new ActionEvent());
|
||||
GameListItem item = GameListCell.this.getItem();
|
||||
if (item != null) {
|
||||
item.getProfile().setSelectedVersion(item.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
root.setLeft(chkSelected);
|
||||
BorderPane.setAlignment(chkSelected, Pos.CENTER);
|
||||
}
|
||||
|
||||
{
|
||||
HBox center = new HBox();
|
||||
center.setMouseTransparent(true);
|
||||
root.setCenter(center);
|
||||
center.setPrefWidth(Region.USE_PREF_SIZE);
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
this.imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
|
||||
this.content = new TwoLineListItem();
|
||||
BorderPane.setAlignment(content, Pos.CENTER);
|
||||
|
||||
FXUtils.onChangeAndOperate(tag, tag -> {
|
||||
content.getTags().clear();
|
||||
if (StringUtils.isNotBlank(tag))
|
||||
content.addTag(tag);
|
||||
});
|
||||
|
||||
center.getChildren().setAll(imageView, content);
|
||||
}
|
||||
|
||||
{
|
||||
this.right = new HBox();
|
||||
root.setRight(right);
|
||||
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
this.btnUpgrade = new JFXButton();
|
||||
btnUpgrade.setOnAction(e -> {
|
||||
GameListItem item = this.getItem();
|
||||
if (item != null)
|
||||
item.update();
|
||||
});
|
||||
btnUpgrade.getStyleClass().add("toggle-icon4");
|
||||
btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.UPDATE.createIcon(24), 24, 24));
|
||||
FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"));
|
||||
right.getChildren().add(btnUpgrade);
|
||||
|
||||
this.btnLaunch = new JFXButton();
|
||||
btnLaunch.setOnAction(e -> {
|
||||
GameListItem item = this.getItem();
|
||||
if (item != null)
|
||||
item.testGame();
|
||||
});
|
||||
btnLaunch.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
|
||||
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24));
|
||||
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"));
|
||||
right.getChildren().add(btnLaunch);
|
||||
|
||||
this.btnManage = new JFXButton();
|
||||
btnManage.setOnAction(e -> {
|
||||
GameListItem item = this.getItem();
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
JFXPopup popup = getPopup(item);
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
|
||||
popup.show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight());
|
||||
});
|
||||
btnManage.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnManage, Pos.CENTER);
|
||||
btnManage.setGraphic(FXUtils.limitingSize(SVG.MORE_VERT.createIcon(24), 24, 24));
|
||||
FXUtils.installFastTooltip(btnManage, i18n("settings.game.management"));
|
||||
right.getChildren().add(btnManage);
|
||||
}
|
||||
|
||||
root.setCursor(Cursor.HAND);
|
||||
container.setOnMouseClicked(e -> {
|
||||
GameListItem item = getItem();
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
if (e.getClickCount() == 1) {
|
||||
item.modifyGameSettings();
|
||||
}
|
||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||
JFXPopup popup = getPopup(item);
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
|
||||
popup.show(root, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - root.getHeight());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(GameListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
this.imageView.imageProperty().unbind();
|
||||
this.content.titleProperty().unbind();
|
||||
this.content.subtitleProperty().unbind();
|
||||
this.tag.unbind();
|
||||
this.right.getChildren().clear();
|
||||
this.chkSelected.selectedProperty().unbind();
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(this.graphic);
|
||||
|
||||
this.chkSelected.selectedProperty().bind(item.selectedProperty());
|
||||
this.imageView.imageProperty().bind(item.imageProperty());
|
||||
this.content.titleProperty().bind(item.titleProperty());
|
||||
this.content.subtitleProperty().bind(item.subtitleProperty());
|
||||
this.tag.bind(item.tagProperty());
|
||||
if (item.canUpdate())
|
||||
this.right.getChildren().add(btnUpgrade);
|
||||
this.right.getChildren().addAll(btnLaunch, btnManage);
|
||||
}
|
||||
}
|
||||
|
||||
private static JFXPopup getPopup(GameListItem item) {
|
||||
PopupMenu menu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
|
||||
menu.getContent().setAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), item::testGame, popup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), item::generateLaunchScript, popup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), item::modifyGameSettings, popup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), item::rename, popup),
|
||||
new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), item::duplicate, popup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), item::remove, popup),
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), item::export, popup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.game"), item::browse, popup));
|
||||
return popup;
|
||||
}
|
||||
}
|
||||
@@ -18,87 +18,58 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
|
||||
public class GameListItem extends Control {
|
||||
private final Profile profile;
|
||||
private final String version;
|
||||
public class GameListItem extends GameItem {
|
||||
private final boolean isModpack;
|
||||
private final ToggleGroup toggleGroup;
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty();
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
|
||||
|
||||
public GameListItem(ToggleGroup toggleGroup, Profile profile, String id) {
|
||||
this.profile = profile;
|
||||
this.version = id;
|
||||
this.toggleGroup = toggleGroup;
|
||||
public GameListItem(Profile profile, String id) {
|
||||
super(profile, id);
|
||||
this.isModpack = profile.getRepository().isModpack(id);
|
||||
|
||||
selected.set(id.equals(profile.getSelectedVersion()));
|
||||
selected.bind(profile.selectedVersionProperty().isEqualTo(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new GameListItemSkin(this);
|
||||
}
|
||||
|
||||
public ToggleGroup getToggleGroup() {
|
||||
return toggleGroup;
|
||||
}
|
||||
|
||||
public Profile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
public ReadOnlyBooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void checkSelection() {
|
||||
selected.set(version.equals(profile.getSelectedVersion()));
|
||||
}
|
||||
|
||||
public void rename() {
|
||||
Versions.renameVersion(profile, version);
|
||||
Versions.renameVersion(profile, id);
|
||||
}
|
||||
|
||||
public void duplicate() {
|
||||
Versions.duplicateVersion(profile, version);
|
||||
Versions.duplicateVersion(profile, id);
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
Versions.deleteVersion(profile, version);
|
||||
Versions.deleteVersion(profile, id);
|
||||
}
|
||||
|
||||
public void export() {
|
||||
Versions.exportVersion(profile, version);
|
||||
Versions.exportVersion(profile, id);
|
||||
}
|
||||
|
||||
public void browse() {
|
||||
Versions.openFolder(profile, version);
|
||||
Versions.openFolder(profile, id);
|
||||
}
|
||||
|
||||
public void testGame() {
|
||||
Versions.testGame(profile, version);
|
||||
Versions.testGame(profile, id);
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
Versions.launch(profile, version);
|
||||
Versions.launch(profile, id);
|
||||
}
|
||||
|
||||
public void modifyGameSettings() {
|
||||
Versions.modifyGameSettings(profile, version);
|
||||
Versions.modifyGameSettings(profile, id);
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Versions.generateLaunchScript(profile, version);
|
||||
Versions.generateLaunchScript(profile, id);
|
||||
}
|
||||
|
||||
public boolean canUpdate() {
|
||||
@@ -106,6 +77,6 @@ public class GameListItem extends Control {
|
||||
}
|
||||
|
||||
public void update() {
|
||||
Versions.updateVersion(profile, version);
|
||||
Versions.updateVersion(profile, id);
|
||||
}
|
||||
}
|
||||
@@ -1,135 +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 com.jfoenix.controls.JFXRadioButton;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||
private static GameListItem currentSkinnable;
|
||||
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> {
|
||||
PopupMenu menu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
|
||||
menu.getContent().setAll(
|
||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), () -> currentSkinnable.testGame(), popup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> currentSkinnable.generateLaunchScript(), popup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), () -> currentSkinnable.modifyGameSettings(), popup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), () -> currentSkinnable.rename(), popup),
|
||||
new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), () -> currentSkinnable.duplicate(), popup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), () -> currentSkinnable.remove(), popup),
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), () -> currentSkinnable.export(), popup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.game"), () -> currentSkinnable.browse(), popup));
|
||||
return popup;
|
||||
});
|
||||
|
||||
public GameListItemSkin(GameListItem skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
JFXRadioButton chkSelected = new JFXRadioButton();
|
||||
BorderPane.setAlignment(chkSelected, Pos.CENTER);
|
||||
chkSelected.setUserData(skinnable);
|
||||
chkSelected.selectedProperty().bindBidirectional(skinnable.selectedProperty());
|
||||
chkSelected.setToggleGroup(skinnable.getToggleGroup());
|
||||
root.setLeft(chkSelected);
|
||||
|
||||
GameItem gameItem = new GameItem(skinnable.getProfile(), skinnable.getVersion());
|
||||
gameItem.setMouseTransparent(true);
|
||||
root.setCenter(gameItem);
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
if (skinnable.canUpdate()) {
|
||||
JFXButton btnUpgrade = new JFXButton();
|
||||
btnUpgrade.setOnAction(e -> skinnable.update());
|
||||
btnUpgrade.getStyleClass().add("toggle-icon4");
|
||||
btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.UPDATE.createIcon(24), 24, 24));
|
||||
FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"));
|
||||
right.getChildren().add(btnUpgrade);
|
||||
}
|
||||
|
||||
{
|
||||
JFXButton btnLaunch = new JFXButton();
|
||||
btnLaunch.setOnAction(e -> skinnable.testGame());
|
||||
btnLaunch.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
|
||||
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24));
|
||||
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"));
|
||||
right.getChildren().add(btnLaunch);
|
||||
}
|
||||
|
||||
{
|
||||
JFXButton btnManage = new JFXButton();
|
||||
btnManage.setOnAction(e -> {
|
||||
currentSkinnable = skinnable;
|
||||
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup.get());
|
||||
popup.get().show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight());
|
||||
});
|
||||
btnManage.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnManage, Pos.CENTER);
|
||||
btnManage.setGraphic(FXUtils.limitingSize(SVG.MORE_VERT.createIcon(24), 24, 24));
|
||||
FXUtils.installFastTooltip(btnManage, i18n("settings.game.management"));
|
||||
right.getChildren().add(btnManage);
|
||||
}
|
||||
|
||||
root.setRight(right);
|
||||
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
root.setStyle("-fx-padding: 8 8 8 0");
|
||||
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
getChildren().setAll(container);
|
||||
|
||||
root.setCursor(Cursor.HAND);
|
||||
container.setOnMouseClicked(e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
if (e.getClickCount() == 1) {
|
||||
skinnable.modifyGameSettings();
|
||||
}
|
||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||
currentSkinnable = skinnable;
|
||||
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup.get());
|
||||
popup.get().show(root, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - root.getHeight());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,16 @@
|
||||
*/
|
||||
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.control.ToggleGroup;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
@@ -36,13 +36,12 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.profile.ProfileListItem;
|
||||
import org.jackhuang.hmcl.ui.profile.ProfilePage;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
|
||||
@@ -53,8 +52,6 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
private final ObservableList<ProfileListItem> profileListItems;
|
||||
private final ObjectProperty<Profile> selectedProfile;
|
||||
|
||||
private ToggleGroup toggleGroup;
|
||||
|
||||
public GameListPage() {
|
||||
profileListItems = MappedObservableList.create(profilesProperty(), profile -> {
|
||||
ProfileListItem item = new ProfileListItem(profile);
|
||||
@@ -123,50 +120,31 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private class GameList extends ListPageBase<GameListItem> {
|
||||
public GameList() {
|
||||
super();
|
||||
private static class GameList extends ListPageBase<GameListItem> {
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
|
||||
public GameList() {
|
||||
Profiles.registerVersionsListener(this::loadVersions);
|
||||
|
||||
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private void loadVersions(Profile profile) {
|
||||
listenerHolder.clear();
|
||||
setLoading(true);
|
||||
setFailedReason(null);
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
toggleGroup = new ToggleGroup();
|
||||
WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
toggleGroup.getProperties().put("ReferenceHolder", listenerHolder);
|
||||
runInFX(() -> {
|
||||
if (profile == Profiles.getSelectedProfile()) {
|
||||
setLoading(false);
|
||||
List<GameListItem> children = repository.getDisplayVersions()
|
||||
.map(version -> new GameListItem(toggleGroup, profile, version.getId()))
|
||||
.collect(Collectors.toList());
|
||||
itemsProperty().setAll(children);
|
||||
children.forEach(GameListItem::checkSelection);
|
||||
if (profile != Profiles.getSelectedProfile())
|
||||
return;
|
||||
|
||||
if (children.isEmpty()) {
|
||||
setFailedReason(i18n("version.empty.hint"));
|
||||
}
|
||||
|
||||
profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
|
||||
FXUtils.checkFxUserThread();
|
||||
children.forEach(it -> it.selectedProperty().set(false));
|
||||
children.stream()
|
||||
.filter(it -> it.getVersion().equals(newValue))
|
||||
.findFirst()
|
||||
.ifPresent(it -> it.selectedProperty().set(true));
|
||||
}));
|
||||
}
|
||||
toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> {
|
||||
if (toggle == null) return;
|
||||
GameListItem model = (GameListItem) toggle.getUserData();
|
||||
model.getProfile().setSelectedVersion(model.getVersion());
|
||||
});
|
||||
});
|
||||
ObservableList<GameListItem> children = FXCollections.observableList(profile.getRepository().getDisplayVersions()
|
||||
.map(instance -> new GameListItem(profile, instance.getId()))
|
||||
.toList());
|
||||
setItems(children);
|
||||
if (children.isEmpty()) {
|
||||
setFailedReason(i18n("version.empty.hint"));
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
public void refreshList() {
|
||||
@@ -178,7 +156,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
return new GameListSkin();
|
||||
}
|
||||
|
||||
private class GameListSkin extends ToolbarListPageSkin<GameList> {
|
||||
private class GameListSkin extends ToolbarListPageSkin<GameListItem, GameList> {
|
||||
|
||||
public GameListSkin() {
|
||||
super(GameList.this);
|
||||
@@ -188,6 +166,11 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
protected List<Node> initializeToolbar(GameList skinnable) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListCell<GameListItem> createListCell(JFXListView<GameListItem> listView) {
|
||||
return new GameListCell();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/// @author Glavo
|
||||
public final class GameListPopupMenu extends StackPane {
|
||||
private final JFXListView<GameItem> listView = new JFXListView<>();
|
||||
private final BooleanBinding isEmpty = Bindings.isEmpty(listView.getItems());
|
||||
|
||||
public GameListPopupMenu() {
|
||||
this.setMaxHeight(365);
|
||||
this.getStyleClass().add("popup-menu-content");
|
||||
|
||||
listView.setCellFactory(Cell::new);
|
||||
listView.setFixedCellSize(60);
|
||||
listView.setPrefWidth(300);
|
||||
|
||||
listView.prefHeightProperty().bind(Bindings.size(getItems()).multiply(60).add(2));
|
||||
|
||||
Label placeholder = new Label(i18n("version.empty"));
|
||||
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;");
|
||||
|
||||
FXUtils.onChangeAndOperate(isEmpty, empty -> {
|
||||
getChildren().setAll(empty ? placeholder : listView);
|
||||
});
|
||||
}
|
||||
|
||||
public ObservableList<GameItem> getItems() {
|
||||
return listView.getItems();
|
||||
}
|
||||
|
||||
private static final class Cell extends ListCell<GameItem> {
|
||||
|
||||
private final Region graphic;
|
||||
|
||||
private final ImageView imageView;
|
||||
private final TwoLineListItem content;
|
||||
|
||||
private final StringProperty tag = new SimpleStringProperty();
|
||||
|
||||
public Cell(ListView<GameItem> listView) {
|
||||
this.setPadding(Insets.EMPTY);
|
||||
HBox root = new HBox();
|
||||
|
||||
root.setSpacing(8);
|
||||
root.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
StackPane imageViewContainer = new StackPane();
|
||||
FXUtils.setLimitWidth(imageViewContainer, 32);
|
||||
FXUtils.setLimitHeight(imageViewContainer, 32);
|
||||
|
||||
this.imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
imageViewContainer.getChildren().setAll(imageView);
|
||||
|
||||
this.content = new TwoLineListItem();
|
||||
FXUtils.onChangeAndOperate(tag, tag -> {
|
||||
content.getTags().clear();
|
||||
if (StringUtils.isNotBlank(tag)) {
|
||||
content.addTag(tag);
|
||||
}
|
||||
});
|
||||
BorderPane.setAlignment(content, Pos.CENTER);
|
||||
root.getChildren().setAll(imageView, content);
|
||||
|
||||
StackPane pane = new StackPane();
|
||||
pane.getChildren().setAll(root);
|
||||
pane.getStyleClass().add("menu-container");
|
||||
root.setMouseTransparent(true);
|
||||
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(pane);
|
||||
FXUtils.onClicked(ripplerContainer, () -> {
|
||||
GameItem item = getItem();
|
||||
if (item != null) {
|
||||
item.getProfile().setSelectedVersion(item.getId());
|
||||
if (getScene().getWindow() instanceof JFXPopup popup)
|
||||
popup.hide();
|
||||
}
|
||||
});
|
||||
this.graphic = ripplerContainer;
|
||||
ripplerContainer.maxWidthProperty().bind(listView.widthProperty().subtract(5));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(GameItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
this.imageView.imageProperty().unbind();
|
||||
this.content.titleProperty().unbind();
|
||||
this.content.subtitleProperty().unbind();
|
||||
this.tag.unbind();
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(this.graphic);
|
||||
|
||||
this.imageView.imageProperty().bind(item.imageProperty());
|
||||
this.content.titleProperty().bind(item.titleProperty());
|
||||
this.content.subtitleProperty().bind(item.subtitleProperty());
|
||||
this.tag.bind(item.tagProperty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -25,8 +25,8 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
@@ -49,6 +49,10 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.RESOURCE_PACK, CurseForgeRemoteModRepository.RESOURCE_PACKS, ModrinthRemoteModRepository.RESOURCE_PACKS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.SHADER_PACK, null, ModrinthRemoteModRepository.SHADER_PACKS);
|
||||
}
|
||||
|
||||
private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection, RemoteModRepository.Type type, CurseForgeRemoteModRepository curseForge, ModrinthRemoteModRepository modrinth) {
|
||||
super(null, callback, versionSelection);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -229,12 +229,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
FXUtils.openFolder(profile.getRepository().getRunDirectory(instanceId).resolve("mods"));
|
||||
}
|
||||
|
||||
public void checkUpdates() {
|
||||
public void checkUpdates(Collection<LocalModFile> mods) {
|
||||
Objects.requireNonNull(mods);
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
|
||||
if (gameVersion.isPresent()) {
|
||||
return new ModCheckUpdatesTask(gameVersion.get(), modManager.getMods());
|
||||
return new ModCheckUpdatesTask(gameVersion.get(), mods);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
|
||||
@@ -23,7 +23,6 @@ import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -93,9 +92,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
// FXThread
|
||||
private boolean isSearching = false;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final ChangeListener<Boolean> holder;
|
||||
|
||||
ModListPageSkin(ModListPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
@@ -107,12 +103,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
|
||||
this.holder = FXUtils.onWeakChange(skinnable.loadingProperty(), loading -> {
|
||||
if (!loading) {
|
||||
listView.scrollTo(0);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
|
||||
@@ -151,7 +141,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add),
|
||||
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder),
|
||||
createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, skinnable::checkUpdates),
|
||||
createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, () ->
|
||||
skinnable.checkUpdates(
|
||||
listView.getItems().stream()
|
||||
.map(ModInfoObject::getModInfo)
|
||||
.toList()
|
||||
)
|
||||
),
|
||||
createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download),
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))
|
||||
);
|
||||
@@ -167,6 +163,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, () ->
|
||||
skinnable.checkUpdates(
|
||||
listView.getSelectionModel().getSelectedItems().stream()
|
||||
.map(ModInfoObject::getModInfo)
|
||||
.toList()
|
||||
)
|
||||
),
|
||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||
listView.getSelectionModel().selectAll()),
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
@@ -199,8 +202,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView, lastCell));
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
skinnable.getItems().addListener((ListChangeListener<? super ModInfoObject>) c -> {
|
||||
@@ -504,7 +506,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
|
||||
remoteMod,
|
||||
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()),
|
||||
(profile, version, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods")
|
||||
(profile, version, mod, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods")
|
||||
));
|
||||
});
|
||||
button.setDisable(false);
|
||||
@@ -572,8 +574,8 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
Tooltip warningTooltip;
|
||||
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView, Holder<Object> lastCell) {
|
||||
super(listView, lastCell);
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView) {
|
||||
super(listView);
|
||||
|
||||
this.getStyleClass().add("mod-info-list-cell");
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -156,8 +155,7 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(control.loadingProperty());
|
||||
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
listView.setCellFactory(x -> new ResourcepackListCell(listView, lastCell, control));
|
||||
listView.setCellFactory(x -> new ResourcepackListCell(listView, control));
|
||||
Bindings.bindContent(listView.getItems(), control.getItems());
|
||||
|
||||
center.setContent(listView);
|
||||
@@ -210,8 +208,8 @@ public final class ResourcepackListPage extends ListPageBase<ResourcepackListPag
|
||||
private final JFXButton btnDelete = new JFXButton();
|
||||
private final ResourcepackListPage page;
|
||||
|
||||
public ResourcepackListCell(JFXListView<ResourcepackInfoObject> listView, Holder<Object> lastCell, ResourcepackListPage page) {
|
||||
super(listView, lastCell);
|
||||
public ResourcepackListCell(JFXListView<ResourcepackInfoObject> listView, ResourcepackListPage page) {
|
||||
super(listView);
|
||||
|
||||
this.page = page;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -252,12 +252,12 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
browseList.getContent().setAll(
|
||||
new IconedMenuItem(SVG.STADIA_CONTROLLER, i18n("folder.game"), () -> control.onBrowse(""), browsePopup),
|
||||
new IconedMenuItem(SVG.EXTENSION, i18n("folder.mod"), () -> control.onBrowse("mods"), browsePopup),
|
||||
new IconedMenuItem(SVG.SETTINGS, i18n("folder.config"), () -> control.onBrowse("config"), browsePopup),
|
||||
new IconedMenuItem(SVG.TEXTURE, i18n("folder.resourcepacks"), () -> control.onBrowse("resourcepacks"), browsePopup),
|
||||
new IconedMenuItem(SVG.WB_SUNNY, i18n("folder.shaderpacks"), () -> control.onBrowse("shaderpacks"), browsePopup),
|
||||
new IconedMenuItem(SVG.SCHEMA, i18n("folder.schematics"), () -> control.onBrowse("schematics"), browsePopup),
|
||||
new IconedMenuItem(SVG.SCREENSHOT_MONITOR, i18n("folder.screenshots"), () -> control.onBrowse("screenshots"), browsePopup),
|
||||
new IconedMenuItem(SVG.PUBLIC, i18n("folder.saves"), () -> control.onBrowse("saves"), browsePopup),
|
||||
new IconedMenuItem(SVG.SCHEMA, i18n("folder.schematics"), () -> control.onBrowse("schematics"), browsePopup),
|
||||
new IconedMenuItem(SVG.WB_SUNNY, i18n("folder.shaderpacks"), () -> control.onBrowse("shaderpacks"), browsePopup),
|
||||
new IconedMenuItem(SVG.SCREENSHOT_MONITOR, i18n("folder.screenshots"), () -> control.onBrowse("screenshots"), browsePopup),
|
||||
new IconedMenuItem(SVG.SETTINGS, i18n("folder.config"), () -> control.onBrowse("config"), browsePopup),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("folder.logs"), () -> control.onBrowse("logs"), browsePopup)
|
||||
);
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.account.CreateAccountPane;
|
||||
@@ -50,8 +53,8 @@ import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class Versions {
|
||||
private Versions() {
|
||||
@@ -69,7 +72,7 @@ public final class Versions {
|
||||
}
|
||||
}
|
||||
|
||||
public static void downloadModpackImpl(Profile profile, String version, RemoteMod.Version file) {
|
||||
public static void downloadModpackImpl(Profile profile, String version, RemoteMod mod, RemoteMod.Version file) {
|
||||
Path modpack;
|
||||
URI downloadURL;
|
||||
try {
|
||||
@@ -85,11 +88,14 @@ public final class Versions {
|
||||
new FileDownloadTask(downloadURL, modpack)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
if (version != null) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, modpack, version));
|
||||
} else {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, modpack));
|
||||
}
|
||||
ModpackInstallWizardProvider installWizardProvider;
|
||||
if (version != null)
|
||||
installWizardProvider = new ModpackInstallWizardProvider(profile, modpack, version);
|
||||
else
|
||||
installWizardProvider = new ModpackInstallWizardProvider(profile, modpack);
|
||||
if (StringUtils.isNotBlank(mod.getIconUrl()))
|
||||
installWizardProvider.setIconUrl(mod.getIconUrl());
|
||||
Controllers.getDecorator().startWizard(installWizardProvider);
|
||||
} else if (e instanceof CancellationException) {
|
||||
Controllers.showToast(i18n("message.cancelled"));
|
||||
} else {
|
||||
@@ -97,7 +103,7 @@ public final class Versions {
|
||||
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
|
||||
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}).executor(true),
|
||||
}),
|
||||
i18n("message.downloading"),
|
||||
TaskCancellationAction.NORMAL
|
||||
);
|
||||
@@ -124,8 +130,8 @@ public final class Versions {
|
||||
|
||||
public static CompletableFuture<String> renameVersion(Profile profile, String version) {
|
||||
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, resolve, reject) -> {
|
||||
if (!HMCLGameRepository.isValidVersionId(newName)) {
|
||||
reject.accept(i18n("install.new_game.malformed"));
|
||||
if (newName.equals(version)) {
|
||||
resolve.run();
|
||||
return;
|
||||
}
|
||||
if (profile.getRepository().renameVersion(version, newName)) {
|
||||
@@ -139,7 +145,8 @@ public final class Versions {
|
||||
} else {
|
||||
reject.accept(i18n("version.manage.rename.fail"));
|
||||
}
|
||||
}, version);
|
||||
}, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
|
||||
}
|
||||
|
||||
public static void exportVersion(Profile profile, String version) {
|
||||
@@ -155,10 +162,6 @@ public final class Versions {
|
||||
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, resolve, reject) -> {
|
||||
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
|
||||
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue();
|
||||
if (!HMCLGameRepository.isValidVersionId(newVersionName)) {
|
||||
reject.accept(i18n("install.new_game.malformed"));
|
||||
return;
|
||||
}
|
||||
Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves))
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
@@ -174,6 +177,7 @@ public final class Versions {
|
||||
})
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("version.manage.duplicate.confirm")))
|
||||
.addQuestion(new PromptDialogPane.Builder.StringQuestion(null, version,
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName))))
|
||||
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false)));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -28,27 +29,36 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.effect.BoxBlur;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.glavo.png.javafx.PNGJavaFXUtils;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jetbrains.annotations.PropertyKey;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
@@ -58,6 +68,8 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
private final World world;
|
||||
private CompoundTag levelDat;
|
||||
|
||||
ImageView iconImageView = new ImageView();
|
||||
|
||||
public WorldInfoPage(WorldManagePage worldManagePage) {
|
||||
this.worldManagePage = worldManagePage;
|
||||
this.world = worldManagePage.getWorld();
|
||||
@@ -80,7 +92,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
if (!Files.isDirectory(world.getFile()))
|
||||
throw new IOException("Not a valid world directory");
|
||||
|
||||
return world.readLevelDat();
|
||||
return world.getLevelData();
|
||||
}
|
||||
|
||||
private void updateControls() {
|
||||
@@ -103,98 +115,132 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
{
|
||||
BorderPane worldNamePane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.name"));
|
||||
worldNamePane.setLeft(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
setLeftLabel(worldNamePane, "world.name");
|
||||
JFXTextField worldNameField = new JFXTextField();
|
||||
setRightTextField(worldNamePane, worldNameField, 200);
|
||||
|
||||
Label worldNameLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(worldNameLabel);
|
||||
worldNameLabel.setText(world.getWorldName());
|
||||
BorderPane.setAlignment(worldNameLabel, Pos.CENTER_RIGHT);
|
||||
worldNamePane.setRight(worldNameLabel);
|
||||
Tag tag = dataTag.get("LevelName");
|
||||
if (tag instanceof StringTag stringTag) {
|
||||
worldNameField.setText(stringTag.getValue());
|
||||
|
||||
worldNameField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
world.setWorldName(newValue);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to set world name", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
worldNameField.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
BorderPane gameVersionPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.game_version"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
gameVersionPane.setLeft(label);
|
||||
|
||||
setLeftLabel(gameVersionPane, "world.info.game_version");
|
||||
Label gameVersionLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(gameVersionLabel);
|
||||
if (world.getGameVersion() != null)
|
||||
gameVersionLabel.setText(world.getGameVersion().toNormalizedString());
|
||||
BorderPane.setAlignment(gameVersionLabel, Pos.CENTER_RIGHT);
|
||||
gameVersionPane.setRight(gameVersionLabel);
|
||||
setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
|
||||
}
|
||||
|
||||
BorderPane randomSeedPane = new BorderPane();
|
||||
BorderPane iconPane = new BorderPane();
|
||||
{
|
||||
setLeftLabel(iconPane, "world.icon");
|
||||
|
||||
HBox left = new HBox(8);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
randomSeedPane.setLeft(left);
|
||||
Runnable onClickAction = () -> Controllers.confirm(
|
||||
i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO,
|
||||
this::changeWorldIcon,
|
||||
null
|
||||
);
|
||||
|
||||
Label label = new Label(i18n("world.info.random_seed"));
|
||||
FXUtils.limitSize(iconImageView, 32, 32);
|
||||
{
|
||||
iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
|
||||
}
|
||||
|
||||
JFXButton editIconButton = new JFXButton();
|
||||
JFXButton resetIconButton = new JFXButton();
|
||||
{
|
||||
editIconButton.setGraphic(SVG.EDIT.createIcon(20));
|
||||
editIconButton.setDisable(worldManagePage.isReadOnly());
|
||||
FXUtils.onClicked(editIconButton, onClickAction);
|
||||
FXUtils.installFastTooltip(editIconButton, i18n("button.edit"));
|
||||
editIconButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
resetIconButton.setGraphic(SVG.RESTORE.createIcon(20));
|
||||
resetIconButton.setDisable(worldManagePage.isReadOnly());
|
||||
FXUtils.onClicked(resetIconButton, this::clearWorldIcon);
|
||||
FXUtils.installFastTooltip(resetIconButton, i18n("button.reset"));
|
||||
resetIconButton.getStyleClass().add("toggle-icon4");
|
||||
}
|
||||
|
||||
HBox hBox = new HBox(8);
|
||||
hBox.setAlignment(Pos.CENTER_LEFT);
|
||||
hBox.getChildren().addAll(iconImageView, editIconButton, resetIconButton);
|
||||
|
||||
iconPane.setRight(hBox);
|
||||
}
|
||||
|
||||
BorderPane seedPane = new BorderPane();
|
||||
{
|
||||
setLeftLabel(seedPane, "world.info.random_seed");
|
||||
|
||||
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
|
||||
StackPane visibilityButton = new StackPane();
|
||||
visibilityButton.setCursor(Cursor.HAND);
|
||||
FXUtils.setLimitWidth(visibilityButton, 12);
|
||||
FXUtils.setLimitHeight(visibilityButton, 12);
|
||||
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
||||
|
||||
left.getChildren().setAll(label, visibilityButton);
|
||||
|
||||
Label randomSeedLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(randomSeedLabel);
|
||||
BorderPane.setAlignment(randomSeedLabel, Pos.CENTER_RIGHT);
|
||||
randomSeedPane.setRight(randomSeedLabel);
|
||||
|
||||
Tag tag = worldGenSettings != null ? worldGenSettings.get("seed") : dataTag.get("RandomSeed");
|
||||
if (tag instanceof LongTag) {
|
||||
randomSeedLabel.setText(tag.getValue().toString());
|
||||
{
|
||||
visibilityButton.setCursor(Cursor.HAND);
|
||||
visibilityButton.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
FXUtils.setLimitWidth(visibilityButton, 12);
|
||||
FXUtils.setLimitHeight(visibilityButton, 12);
|
||||
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
||||
}
|
||||
|
||||
BoxBlur blur = new BoxBlur();
|
||||
blur.setIterations(3);
|
||||
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
|
||||
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
|
||||
visibilityButton.getChildren().setAll(icon.createIcon(12));
|
||||
randomSeedLabel.setEffect(isVisibility ? null : blur);
|
||||
});
|
||||
Label seedLabel = new Label();
|
||||
{
|
||||
FXUtils.copyOnDoubleClick(seedLabel);
|
||||
seedLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
seedLabel.setText(world.getSeed() != null ? world.getSeed().toString() : "");
|
||||
|
||||
BoxBlur blur = new BoxBlur();
|
||||
blur.setIterations(3);
|
||||
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
|
||||
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
|
||||
visibilityButton.getChildren().setAll(icon.createIcon(12));
|
||||
seedLabel.setEffect(isVisibility ? null : blur);
|
||||
});
|
||||
}
|
||||
|
||||
HBox right = new HBox(8);
|
||||
{
|
||||
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
|
||||
right.getChildren().setAll(visibilityButton, seedLabel);
|
||||
seedPane.setRight(right);
|
||||
}
|
||||
}
|
||||
|
||||
BorderPane lastPlayedPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.last_played"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
lastPlayedPane.setLeft(label);
|
||||
|
||||
setLeftLabel(lastPlayedPane, "world.info.last_played");
|
||||
Label lastPlayedLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(lastPlayedLabel);
|
||||
lastPlayedLabel.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
||||
BorderPane.setAlignment(lastPlayedLabel, Pos.CENTER_RIGHT);
|
||||
lastPlayedPane.setRight(lastPlayedLabel);
|
||||
setRightTextLabel(lastPlayedPane, lastPlayedLabel, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
||||
}
|
||||
|
||||
BorderPane timePane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.time"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
timePane.setLeft(label);
|
||||
setLeftLabel(timePane, "world.info.time");
|
||||
|
||||
Label timeLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(timeLabel);
|
||||
BorderPane.setAlignment(timeLabel, Pos.CENTER_RIGHT);
|
||||
timePane.setRight(timeLabel);
|
||||
|
||||
Tag tag = dataTag.get("Time");
|
||||
if (tag instanceof LongTag) {
|
||||
long days = ((LongTag) tag).getValue() / 24000;
|
||||
timeLabel.setText(i18n("world.info.time.format", days));
|
||||
}
|
||||
setRightTextLabel(timePane, timeLabel, () -> {
|
||||
Tag tag = dataTag.get("Time");
|
||||
if (tag instanceof LongTag) {
|
||||
long days = ((LongTag) tag).getValue() / 24000;
|
||||
return i18n("world.info.time.format", days);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
OptionToggleButton allowCheatsButton = new OptionToggleButton();
|
||||
@@ -203,21 +249,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
|
||||
Tag tag = dataTag.get("allowCommands");
|
||||
|
||||
if (tag instanceof ByteTag) {
|
||||
ByteTag byteTag = (ByteTag) tag;
|
||||
byte value = byteTag.getValue();
|
||||
if (value == 0 || value == 1) {
|
||||
allowCheatsButton.setSelected(value == 1);
|
||||
allowCheatsButton.selectedProperty().addListener((o, oldValue, newValue) -> {
|
||||
byteTag.setValue(newValue ? (byte) 1 : (byte) 0);
|
||||
saveLevelDat();
|
||||
});
|
||||
} else {
|
||||
allowCheatsButton.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
allowCheatsButton.setDisable(true);
|
||||
}
|
||||
checkTagAndSetListener(tag, allowCheatsButton);
|
||||
}
|
||||
|
||||
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
|
||||
@@ -226,28 +258,12 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
|
||||
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
|
||||
|
||||
if (tag instanceof ByteTag) {
|
||||
ByteTag byteTag = (ByteTag) tag;
|
||||
byte value = byteTag.getValue();
|
||||
if (value == 0 || value == 1) {
|
||||
generateFeaturesButton.setSelected(value == 1);
|
||||
generateFeaturesButton.selectedProperty().addListener((o, oldValue, newValue) -> {
|
||||
byteTag.setValue(newValue ? (byte) 1 : (byte) 0);
|
||||
saveLevelDat();
|
||||
});
|
||||
} else {
|
||||
generateFeaturesButton.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
generateFeaturesButton.setDisable(true);
|
||||
}
|
||||
checkTagAndSetListener(tag, generateFeaturesButton);
|
||||
}
|
||||
|
||||
BorderPane difficultyPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.difficulty"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
difficultyPane.setLeft(label);
|
||||
setLeftLabel(difficultyPane, "world.info.difficulty");
|
||||
|
||||
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
|
||||
difficultyBox.setDisable(worldManagePage.isReadOnly());
|
||||
@@ -255,8 +271,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
difficultyPane.setRight(difficultyBox);
|
||||
|
||||
Tag tag = dataTag.get("Difficulty");
|
||||
if (tag instanceof ByteTag) {
|
||||
ByteTag byteTag = (ByteTag) tag;
|
||||
if (tag instanceof ByteTag byteTag) {
|
||||
Difficulty difficulty = Difficulty.of(byteTag.getValue());
|
||||
if (difficulty != null) {
|
||||
difficultyBox.setValue(difficulty);
|
||||
@@ -274,86 +289,93 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
}
|
||||
|
||||
OptionToggleButton difficultyLockPane = new OptionToggleButton();
|
||||
{
|
||||
difficultyLockPane.setTitle(i18n("world.info.difficulty_lock"));
|
||||
difficultyLockPane.setDisable(worldManagePage.isReadOnly());
|
||||
|
||||
Tag tag = dataTag.get("DifficultyLocked");
|
||||
checkTagAndSetListener(tag, difficultyLockPane);
|
||||
}
|
||||
|
||||
basicInfo.getContent().setAll(
|
||||
worldNamePane, gameVersionPane, randomSeedPane, lastPlayedPane, timePane,
|
||||
allowCheatsButton, generateFeaturesButton, difficultyPane);
|
||||
worldNamePane, gameVersionPane, iconPane, seedPane, lastPlayedPane, timePane,
|
||||
allowCheatsButton, generateFeaturesButton, difficultyPane, difficultyLockPane);
|
||||
|
||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo);
|
||||
}
|
||||
|
||||
Tag playerTag = dataTag.get("Player");
|
||||
if (playerTag instanceof CompoundTag) {
|
||||
CompoundTag player = (CompoundTag) playerTag;
|
||||
if (playerTag instanceof CompoundTag player) {
|
||||
ComponentList playerInfo = new ComponentList();
|
||||
|
||||
BorderPane locationPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.player.location"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
locationPane.setLeft(label);
|
||||
|
||||
setLeftLabel(locationPane, "world.info.player.location");
|
||||
Label locationLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(locationLabel);
|
||||
BorderPane.setAlignment(locationLabel, Pos.CENTER_RIGHT);
|
||||
locationPane.setRight(locationLabel);
|
||||
|
||||
Dimension dim = Dimension.of(player.get("Dimension"));
|
||||
if (dim != null) {
|
||||
String posString = dim.formatPosition(player.get("Pos"));
|
||||
if (posString != null)
|
||||
locationLabel.setText(posString);
|
||||
}
|
||||
setRightTextLabel(locationPane, locationLabel, () -> {
|
||||
Dimension dim = Dimension.of(player.get("Dimension"));
|
||||
if (dim != null) {
|
||||
String posString = dim.formatPosition(player.get("Pos"));
|
||||
if (posString != null)
|
||||
return posString;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
BorderPane lastDeathLocationPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.player.last_death_location"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
lastDeathLocationPane.setLeft(label);
|
||||
|
||||
setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location");
|
||||
Label lastDeathLocationLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(lastDeathLocationLabel);
|
||||
BorderPane.setAlignment(lastDeathLocationLabel, Pos.CENTER_RIGHT);
|
||||
lastDeathLocationPane.setRight(lastDeathLocationLabel);
|
||||
|
||||
Tag tag = player.get("LastDeathLocation");
|
||||
if (tag instanceof CompoundTag) {
|
||||
Dimension dim = Dimension.of(((CompoundTag) tag).get("dimension"));
|
||||
if (dim != null) {
|
||||
String posString = dim.formatPosition(((CompoundTag) tag).get("pos"));
|
||||
if (posString != null)
|
||||
lastDeathLocationLabel.setText(posString);
|
||||
setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> {
|
||||
Tag tag = player.get("LastDeathLocation");// Valid after 22w14a; prior to this version, the game did not record the last death location data.
|
||||
if (tag instanceof CompoundTag compoundTag) {
|
||||
Dimension dim = Dimension.of(compoundTag.get("dimension"));
|
||||
if (dim != null) {
|
||||
String posString = dim.formatPosition(compoundTag.get("pos"));
|
||||
if (posString != null)
|
||||
return posString;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
BorderPane spawnPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.player.spawn"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
spawnPane.setLeft(label);
|
||||
|
||||
setLeftLabel(spawnPane, "world.info.player.spawn");
|
||||
Label spawnLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(spawnLabel);
|
||||
BorderPane.setAlignment(spawnLabel, Pos.CENTER_RIGHT);
|
||||
spawnPane.setRight(spawnLabel);
|
||||
setRightTextLabel(spawnPane, spawnLabel, () -> {
|
||||
|
||||
Dimension dim = Dimension.of(player.get("SpawnDimension"));
|
||||
if (dim != null) {
|
||||
Tag x = player.get("SpawnX");
|
||||
Tag y = player.get("SpawnY");
|
||||
Tag z = player.get("SpawnZ");
|
||||
Dimension dimension;
|
||||
if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { // Valid after 25w07a
|
||||
dimension = Dimension.of(respawnTag.get("dimension"));
|
||||
Tag posTag = respawnTag.get("pos");
|
||||
|
||||
if (x instanceof IntTag && y instanceof IntTag && z instanceof IntTag)
|
||||
spawnLabel.setText(dim.formatPosition(((IntTag) x).getValue(), ((IntTag) y).getValue(), ((IntTag) z).getValue()));
|
||||
}
|
||||
if (posTag instanceof IntArrayTag intArrayTag && intArrayTag.length() >= 3) {
|
||||
return dimension.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2));
|
||||
}
|
||||
} else if (player.get("SpawnX") instanceof IntTag intX
|
||||
&& player.get("SpawnY") instanceof IntTag intY
|
||||
&& player.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a
|
||||
// SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld.
|
||||
dimension = Dimension.of(player.get("SpawnDimension") == null ? new IntTag("SpawnDimension", 0) : player.get("SpawnDimension"));
|
||||
if (dimension == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return dimension.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
BorderPane playerGameTypePane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.player.game_type"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
playerGameTypePane.setLeft(label);
|
||||
setLeftLabel(playerGameTypePane, "world.info.player.game_type");
|
||||
|
||||
JFXComboBox<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
|
||||
gameTypeBox.setDisable(worldManagePage.isReadOnly());
|
||||
@@ -364,8 +386,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
Tag hardcoreTag = dataTag.get("hardcore");
|
||||
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
|
||||
|
||||
if (tag instanceof IntTag) {
|
||||
IntTag intTag = (IntTag) tag;
|
||||
if (tag instanceof IntTag intTag) {
|
||||
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
|
||||
if (gameType != null) {
|
||||
gameTypeBox.setValue(gameType);
|
||||
@@ -395,33 +416,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
|
||||
BorderPane healthPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.player.health"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
healthPane.setLeft(label);
|
||||
|
||||
setLeftLabel(healthPane, "world.info.player.health");
|
||||
JFXTextField healthField = new JFXTextField();
|
||||
healthField.setDisable(worldManagePage.isReadOnly());
|
||||
healthField.setPrefWidth(50);
|
||||
healthField.setAlignment(Pos.CENTER_RIGHT);
|
||||
BorderPane.setAlignment(healthField, Pos.CENTER_RIGHT);
|
||||
healthPane.setRight(healthField);
|
||||
setRightTextField(healthPane, healthField, 50);
|
||||
|
||||
Tag tag = player.get("Health");
|
||||
if (tag instanceof FloatTag) {
|
||||
FloatTag floatTag = (FloatTag) tag;
|
||||
healthField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue()));
|
||||
|
||||
healthField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
floatTag.setValue(Float.parseFloat(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(healthField, true);
|
||||
healthField.setValidators(new DoubleValidator(i18n("input.number"), true));
|
||||
if (tag instanceof FloatTag floatTag) {
|
||||
setTagAndTextField(floatTag, healthField);
|
||||
} else {
|
||||
healthField.setDisable(true);
|
||||
}
|
||||
@@ -429,33 +430,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
|
||||
BorderPane foodLevelPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.player.food_level"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
foodLevelPane.setLeft(label);
|
||||
|
||||
setLeftLabel(foodLevelPane, "world.info.player.food_level");
|
||||
JFXTextField foodLevelField = new JFXTextField();
|
||||
foodLevelField.setDisable(worldManagePage.isReadOnly());
|
||||
foodLevelField.setPrefWidth(50);
|
||||
foodLevelField.setAlignment(Pos.CENTER_RIGHT);
|
||||
BorderPane.setAlignment(foodLevelField, Pos.CENTER_RIGHT);
|
||||
foodLevelPane.setRight(foodLevelField);
|
||||
setRightTextField(foodLevelPane, foodLevelField, 50);
|
||||
|
||||
Tag tag = player.get("foodLevel");
|
||||
if (tag instanceof IntTag) {
|
||||
IntTag intTag = (IntTag) tag;
|
||||
foodLevelField.setText(String.valueOf(intTag.getValue()));
|
||||
|
||||
foodLevelField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
intTag.setValue(Integer.parseInt(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(foodLevelField, true);
|
||||
foodLevelField.setValidators(new NumberValidator(i18n("input.number"), true));
|
||||
if (tag instanceof IntTag intTag) {
|
||||
setTagAndTextField(intTag, foodLevelField);
|
||||
} else {
|
||||
foodLevelField.setDisable(true);
|
||||
}
|
||||
@@ -463,33 +444,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
|
||||
BorderPane xpLevelPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.player.xp_level"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
xpLevelPane.setLeft(label);
|
||||
|
||||
setLeftLabel(xpLevelPane, "world.info.player.xp_level");
|
||||
JFXTextField xpLevelField = new JFXTextField();
|
||||
xpLevelField.setDisable(worldManagePage.isReadOnly());
|
||||
xpLevelField.setPrefWidth(50);
|
||||
xpLevelField.setAlignment(Pos.CENTER_RIGHT);
|
||||
BorderPane.setAlignment(xpLevelField, Pos.CENTER_RIGHT);
|
||||
xpLevelPane.setRight(xpLevelField);
|
||||
setRightTextField(xpLevelPane, xpLevelField, 50);
|
||||
|
||||
Tag tag = player.get("XpLevel");
|
||||
if (tag instanceof IntTag) {
|
||||
IntTag intTag = (IntTag) tag;
|
||||
xpLevelField.setText(String.valueOf(intTag.getValue()));
|
||||
|
||||
xpLevelField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
intTag.setValue(Integer.parseInt(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(xpLevelField, true);
|
||||
xpLevelField.setValidators(new NumberValidator(i18n("input.number"), true));
|
||||
if (tag instanceof IntTag intTag) {
|
||||
setTagAndTextField(intTag, xpLevelField);
|
||||
} else {
|
||||
xpLevelField.setDisable(true);
|
||||
}
|
||||
@@ -504,6 +465,88 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void setLeftLabel(BorderPane borderPane, @PropertyKey(resourceBundle = "assets.lang.I18N") String key) {
|
||||
Label label = new Label(i18n(key));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
borderPane.setLeft(label);
|
||||
}
|
||||
|
||||
private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) {
|
||||
textField.setDisable(worldManagePage.isReadOnly());
|
||||
textField.setPrefWidth(perfWidth);
|
||||
textField.setAlignment(Pos.CENTER_RIGHT);
|
||||
borderPane.setRight(textField);
|
||||
}
|
||||
|
||||
private void setRightTextLabel(BorderPane borderPane, Label label, Callable<String> setNameCall) {
|
||||
FXUtils.copyOnDoubleClick(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_RIGHT);
|
||||
try {
|
||||
label.setText(setNameCall.call());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Exception happened when setting name", e);
|
||||
}
|
||||
borderPane.setRight(label);
|
||||
}
|
||||
|
||||
private void checkTagAndSetListener(Tag tag, OptionToggleButton toggleButton) {
|
||||
if (tag instanceof ByteTag byteTag) {
|
||||
byte value = byteTag.getValue();
|
||||
if (value == 0 || value == 1) {
|
||||
toggleButton.setSelected(value == 1);
|
||||
toggleButton.selectedProperty().addListener((o, oldValue, newValue) -> {
|
||||
try {
|
||||
byteTag.setValue((byte) (newValue ? 1 : 0));
|
||||
saveLevelDat();
|
||||
} catch (Exception e) {
|
||||
toggleButton.setSelected(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toggleButton.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
toggleButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(String.valueOf(intTag.getValue()));
|
||||
|
||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
intTag.setValue(Integer.parseInt(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(jfxTextField, true);
|
||||
jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true));
|
||||
}
|
||||
|
||||
private void setTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue()));
|
||||
|
||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
floatTag.setValue(Float.parseFloat(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(jfxTextField, true);
|
||||
jfxTextField.setValidators(new DoubleValidator(i18n("input.number"), true));
|
||||
}
|
||||
|
||||
private void saveLevelDat() {
|
||||
LOG.info("Saving level.dat of world " + world.getWorldName());
|
||||
try {
|
||||
@@ -513,54 +556,34 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Dimension {
|
||||
private record Dimension(String name) {
|
||||
static final Dimension OVERWORLD = new Dimension(null);
|
||||
static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether"));
|
||||
static final Dimension THE_END = new Dimension(i18n("world.info.dimension.the_end"));
|
||||
|
||||
final String name;
|
||||
|
||||
static Dimension of(Tag tag) {
|
||||
if (tag instanceof IntTag) {
|
||||
switch (((IntTag) tag).getValue()) {
|
||||
case 0:
|
||||
return OVERWORLD;
|
||||
case 1:
|
||||
return THE_NETHER;
|
||||
case 2:
|
||||
return THE_END;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} else if (tag instanceof StringTag) {
|
||||
String id = ((StringTag) tag).getValue();
|
||||
switch (id) {
|
||||
case "overworld":
|
||||
case "minecraft:overworld":
|
||||
return OVERWORLD;
|
||||
case "the_nether":
|
||||
case "minecraft:the_nether":
|
||||
return THE_NETHER;
|
||||
case "the_end":
|
||||
case "minecraft:the_end":
|
||||
return THE_END;
|
||||
default:
|
||||
return new Dimension(id);
|
||||
}
|
||||
if (tag instanceof IntTag intTag) {
|
||||
return switch (intTag.getValue()) {
|
||||
case 0 -> OVERWORLD;
|
||||
case 1 -> THE_NETHER;
|
||||
case 2 -> THE_END;
|
||||
default -> null;
|
||||
};
|
||||
} else if (tag instanceof StringTag stringTag) {
|
||||
String id = stringTag.getValue();
|
||||
return switch (id) {
|
||||
case "overworld", "minecraft:overworld" -> OVERWORLD;
|
||||
case "the_nether", "minecraft:the_nether" -> THE_NETHER;
|
||||
case "the_end", "minecraft:the_end" -> THE_END;
|
||||
default -> new Dimension(id);
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Dimension(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
String formatPosition(Tag tag) {
|
||||
if (tag instanceof ListTag) {
|
||||
ListTag listTag = (ListTag) tag;
|
||||
if (listTag.size() != 3)
|
||||
return null;
|
||||
if (tag instanceof ListTag listTag && listTag.size() == 3) {
|
||||
|
||||
Tag x = listTag.get(0);
|
||||
Tag y = listTag.get(1);
|
||||
@@ -575,8 +598,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tag instanceof IntArrayTag) {
|
||||
IntArrayTag intArrayTag = (IntArrayTag) tag;
|
||||
if (tag instanceof IntArrayTag intArrayTag) {
|
||||
|
||||
int x = intArrayTag.getValue(0);
|
||||
int y = intArrayTag.getValue(1);
|
||||
@@ -609,7 +631,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
static final ObservableList<Difficulty> items = FXCollections.observableList(Arrays.asList(values()));
|
||||
|
||||
static Difficulty of(int d) {
|
||||
return d >= 0 && d <= items.size() ? items.get(d) : null;
|
||||
return (d >= 0 && d < items.size()) ? items.get(d) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -633,4 +655,51 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
return i18n("world.info.player.game_type." + name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
private void changeWorldIcon() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("world.icon.choose.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
|
||||
fileChooser.setInitialFileName("icon.png");
|
||||
|
||||
File file = fileChooser.showOpenDialog(Controllers.getStage());
|
||||
if (file == null) return;
|
||||
|
||||
Image image;
|
||||
try {
|
||||
image = FXUtils.loadImage(file.toPath());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load image", e);
|
||||
Controllers.dialog(i18n("world.icon.change.fail.load.text"), i18n("world.icon.change.fail.load.title"), MessageDialogPane.MessageType.ERROR);
|
||||
return;
|
||||
}
|
||||
if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) {
|
||||
Path output = world.getFile().resolve("icon.png");
|
||||
saveImage(image, output);
|
||||
} else {
|
||||
Controllers.dialog(i18n("world.icon.change.fail.not_64x64.text", (int) image.getWidth(), (int) image.getHeight()), i18n("world.icon.change.fail.not_64x64.title"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveImage(Image image, Path path) {
|
||||
Image oldImage = iconImageView.getImage();
|
||||
try {
|
||||
PNGJavaFXUtils.writeImage(image, path);
|
||||
iconImageView.setImage(image);
|
||||
Controllers.showToast(i18n("world.icon.change.succeed.toast"));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to save world icon " + e.getMessage());
|
||||
iconImageView.setImage(oldImage);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearWorldIcon() {
|
||||
Path output = world.getFile().resolve("icon.png");
|
||||
try {
|
||||
Files.deleteIfExists(output);
|
||||
iconImageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_server.png"));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete world icon " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +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 javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.game.WorldLockedException;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
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() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("world.export.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
|
||||
fileChooser.setInitialFileName(world.getWorldName());
|
||||
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> new WorldExportPage(world, file, controller::onFinish)));
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
Controllers.confirm(
|
||||
i18n("button.remove.confirm"),
|
||||
i18n("world.delete"),
|
||||
() -> Task.runAsync(world::delete)
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
parent.remove(this);
|
||||
} else if (exception instanceof WorldLockedException) {
|
||||
Controllers.dialog(i18n("world.locked.failed"), null, MessageType.WARNING);
|
||||
} else {
|
||||
Controllers.dialog(i18n("world.delete.failed", StringUtils.getStackTrace(exception)), null, MessageType.WARNING);
|
||||
}
|
||||
}).start(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
Loaded 100 of 199 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user