Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5768cd446e | ||
|
|
6f406cede8 | ||
|
|
0597282463 | ||
|
|
f91fbe29b4 | ||
|
|
cccff7effd | ||
|
|
101257cae8 | ||
|
|
a6e74e7c19 | ||
|
|
615ebe0311 | ||
|
|
8b3468e2d9 | ||
|
|
7f1a812373 | ||
|
|
22823151c3 | ||
|
|
c0844834cb | ||
|
|
7921c7abb5 | ||
|
|
c610519f87 | ||
|
|
bb1949f919 | ||
|
|
a1e946bba6 | ||
|
|
c1bf66b972 | ||
|
|
25952aa514 | ||
|
|
54d8d877be |
No files matched your search
@@ -145,6 +145,7 @@ val addOpens = listOf(
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.graphics/com.sun.javafx.tk.quantum",
|
||||
"javafx.controls/javafx.scene.control",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"jdk.attach/sun.tools.attach",
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ComboBoxBase;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
@@ -28,6 +29,7 @@ import javafx.scene.paint.Paint;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -178,6 +180,11 @@ public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
|
||||
|
||||
comboBox.hide();
|
||||
});
|
||||
Platform.runLater(() -> {
|
||||
if (listView.lookup(".virtual-flow") instanceof VirtualFlow<?> virtualFlow) {
|
||||
FXUtils.smoothScrolling(virtualFlow);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,731 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDatePicker;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.svg.SVGGlyph;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.*;
|
||||
import javafx.animation.Animation.Status;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.SnapshotParameters;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.SVGContainer;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
|
||||
import java.time.DateTimeException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Year;
|
||||
import java.time.YearMonth;
|
||||
import java.time.chrono.ChronoLocalDate;
|
||||
import java.time.chrono.Chronology;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DecimalStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.WeekFields;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class JFXDatePickerContent extends VBox {
|
||||
private static final String SPINNER_LABEL = "spinner-label";
|
||||
private static final String ROBOTO = "Roboto";
|
||||
protected JFXDatePicker datePicker;
|
||||
private JFXButton backMonthButton;
|
||||
private JFXButton forwardMonthButton;
|
||||
private Label selectedDateLabel;
|
||||
private Label selectedYearLabel;
|
||||
private Label monthYearLabel;
|
||||
protected GridPane contentGrid;
|
||||
private final StackPane calendarPlaceHolder = new StackPane();
|
||||
private final CachedTransition showTransition;
|
||||
private final CachedTransition hideTransition;
|
||||
private ParallelTransition tempImageTransition;
|
||||
private final int daysPerWeek = 7;
|
||||
private final List<DateCell> weekDaysCells = new ArrayList<>();
|
||||
private final List<DateCell> weekNumberCells = new ArrayList<>();
|
||||
protected List<DateCell> dayCells = new ArrayList<>();
|
||||
private LocalDate[] dayCellDates;
|
||||
private DateCell currentFocusedDayCell = null;
|
||||
private final ListView<String> yearsListView = new JFXListView<>() {
|
||||
{
|
||||
this.getStyleClass().addAll("date-picker-list-view", "no-padding");
|
||||
this.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
this.setCellFactory((listView) -> new ListCell<>() {
|
||||
static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
final Label label = new Label();
|
||||
final StackPane root;
|
||||
final RipplerContainer ripplerContainer;
|
||||
|
||||
final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
{
|
||||
label.getStyleClass().add(SPINNER_LABEL);
|
||||
label.setMaxWidth(Double.MAX_VALUE);
|
||||
label.textProperty().bind(itemProperty());
|
||||
StackPane pane = new StackPane(label);
|
||||
pane.setPadding(new Insets(7));
|
||||
ripplerContainer = new RipplerContainer(pane);
|
||||
root = new StackPane(ripplerContainer);
|
||||
root.getStyleClass().add("data-picker-list-cell");
|
||||
this.setGraphic(root);
|
||||
|
||||
var selectedListenerWeak = holder.weak((InvalidationListener) observable -> {
|
||||
String newVal = JFXDatePickerContent.this.selectedYearLabel.getText();
|
||||
if (newVal != null && getItem() != null && getItem().equals(newVal))
|
||||
updateSelected(true);
|
||||
});
|
||||
JFXDatePickerContent.this.selectedYearLabel.textProperty().addListener(selectedListenerWeak);
|
||||
itemProperty().addListener(selectedListenerWeak);
|
||||
|
||||
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
|
||||
if (getListView() != null) {
|
||||
if (selected) {
|
||||
getListView().getSelectionModel().select(getIndex());
|
||||
} else {
|
||||
getListView().getSelectionModel().clearSelection(getIndex());
|
||||
}
|
||||
}
|
||||
root.pseudoClassStateChanged(SELECTED, selected);
|
||||
if (selected) {
|
||||
int offset = Integer.parseInt(getItem()) - Integer.parseInt(JFXDatePickerContent.this.selectedYearLabel.getText());
|
||||
JFXDatePickerContent.this.forward(offset, ChronoUnit.YEARS, false, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateItem(String item, boolean empty) {
|
||||
String oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(item, empty);
|
||||
setText(null);
|
||||
|
||||
if (Objects.equals(oldItem, item) && oldEmpty == empty) return;
|
||||
ripplerContainer.releaseRippleImmediately();
|
||||
|
||||
if (!empty && item != null) {
|
||||
setGraphic(root);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
final DateTimeFormatter fullFormatter = DateTimeFormatter.ofPattern("EEE, MMM yyyy");
|
||||
final DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("MMMM");
|
||||
final DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("y");
|
||||
final DateTimeFormatter weekNumberFormatter = DateTimeFormatter.ofPattern("w");
|
||||
final DateTimeFormatter weekDayNameFormatter = DateTimeFormatter.ofPattern("ccc");
|
||||
final DateTimeFormatter dayCellFormatter = DateTimeFormatter.ofPattern("d");
|
||||
private final ObjectProperty<YearMonth> selectedYearMonth = new SimpleObjectProperty<>(this, "selectedYearMonth");
|
||||
|
||||
final ObjectProperty<LocalDate> valueProperty = new SimpleObjectProperty<>(this, "value");
|
||||
|
||||
JFXDatePickerContent(final DatePicker datePicker) {
|
||||
this.datePicker = (JFXDatePicker) datePicker;
|
||||
this.getStyleClass().add("date-picker-popup");
|
||||
LocalDate date = datePicker.getValue();
|
||||
this.valueProperty.set(date);
|
||||
this.valueProperty.addListener((o, old, n) -> {
|
||||
this.displayedYearMonthProperty().set(n != null ? YearMonth.from(n) : YearMonth.now());
|
||||
this.updateValues();
|
||||
});
|
||||
this.selectedYearMonth.set(date != null ? YearMonth.from(date) : YearMonth.now());
|
||||
this.selectedYearMonth.addListener((observable, oldValue, newValue) -> this.updateValues());
|
||||
this.getChildren().add(this.createHeaderPane());
|
||||
this.getChildren().add(new Separator());
|
||||
this.contentGrid = new GridPane() {
|
||||
protected double computePrefWidth(double height) {
|
||||
int nCols = JFXDatePickerContent.this.daysPerWeek + (datePicker.isShowWeekNumbers() ? 1 : 0);
|
||||
double leftSpace = this.snapSpaceX(this.getInsets().getLeft());
|
||||
double rightSpace = this.snapSpaceX(this.getInsets().getRight());
|
||||
double hgaps = this.snapSpaceX(this.getHgap()) * (double) (nCols - 1);
|
||||
double contentWidth = super.computePrefWidth(height) - leftSpace - rightSpace - hgaps;
|
||||
return this.snapSizeX(contentWidth / (double) nCols) * (double) nCols + leftSpace + rightSpace + hgaps;
|
||||
}
|
||||
|
||||
protected void layoutChildren() {
|
||||
if (this.getWidth() > (double) 0.0F && this.getHeight() > (double) 0.0F) {
|
||||
super.layoutChildren();
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
this.contentGrid.setFocusTraversable(true);
|
||||
this.contentGrid.getStyleClass().add("calendar-grid");
|
||||
this.contentGrid.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
this.contentGrid.setPadding(new Insets(0.0F, 12.0F, 12.0F, 12.0F));
|
||||
this.contentGrid.setVgap(4);
|
||||
this.contentGrid.setHgap(4);
|
||||
this.createWeekDaysCells();
|
||||
this.createDayCells();
|
||||
|
||||
this.yearsListView.getItems().addAll(IntStream.rangeClosed(1900, Year.now().getValue() + 100).mapToObj(Integer::toString).toList());
|
||||
|
||||
StackPane main = new StackPane();
|
||||
this.yearsListView.setVisible(false);
|
||||
this.yearsListView.setOpacity(0.0F);
|
||||
this.yearsListView.maxWidthProperty().bind(main.widthProperty());
|
||||
this.yearsListView.maxHeightProperty().bind(main.heightProperty());
|
||||
main.getChildren().setAll(this.contentGrid, this.yearsListView);
|
||||
|
||||
VBox contentHolder = new VBox();
|
||||
contentHolder.getChildren().setAll(this.createCalendarMonthLabelPane(), main);
|
||||
this.calendarPlaceHolder.getStyleClass().add("content");
|
||||
this.calendarPlaceHolder.getChildren().setAll(contentHolder, this.createCalendarArrowsPane());
|
||||
FXUtils.setOverflowHidden(this.calendarPlaceHolder);
|
||||
|
||||
this.getChildren().add(this.calendarPlaceHolder);
|
||||
|
||||
this.getChildren().add(this.createActions());
|
||||
|
||||
this.refresh();
|
||||
this.addEventHandler(KeyEvent.ANY, (event) -> {
|
||||
Node node = this.getScene().getFocusOwner();
|
||||
if (node instanceof DateCell) {
|
||||
this.currentFocusedDayCell = (DateCell) node;
|
||||
}
|
||||
|
||||
switch (event.getCode()) {
|
||||
case HOME -> {
|
||||
this.init();
|
||||
this.goToDate(LocalDate.now(), true);
|
||||
event.consume();
|
||||
}
|
||||
case PAGE_UP -> {
|
||||
if (!this.backMonthButton.isDisabled()) {
|
||||
this.forward(-1, ChronoUnit.MONTHS, true, true);
|
||||
}
|
||||
|
||||
event.consume();
|
||||
}
|
||||
case PAGE_DOWN -> {
|
||||
if (!this.forwardMonthButton.isDisabled()) {
|
||||
this.forward(1, ChronoUnit.MONTHS, true, true);
|
||||
}
|
||||
|
||||
event.consume();
|
||||
}
|
||||
case ESCAPE -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
event.consume();
|
||||
}
|
||||
default -> event.consume();
|
||||
}
|
||||
|
||||
});
|
||||
this.showTransition = new CachedTransition(this.yearsListView, new Timeline(new KeyFrame(Duration.millis(0.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(500.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1000.0F), new KeyValue(this.yearsListView.opacityProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setCycleDuration(Duration.millis(320.0F));
|
||||
this.setDelay(Duration.seconds(0.0F));
|
||||
}
|
||||
|
||||
protected void starting() {
|
||||
super.starting();
|
||||
JFXDatePickerContent.this.yearsListView.setVisible(true);
|
||||
}
|
||||
};
|
||||
this.hideTransition = new CachedTransition(this.yearsListView, new Timeline(new KeyFrame(Duration.millis(0.0F), new KeyValue(this.yearsListView.opacityProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(500.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1000.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 1, Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setCycleDuration(Duration.millis(320.0F));
|
||||
this.setDelay(Duration.seconds(0.0F));
|
||||
}
|
||||
|
||||
protected void stopping() {
|
||||
super.stopping();
|
||||
JFXDatePickerContent.this.yearsListView.setVisible(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ObjectProperty<YearMonth> displayedYearMonthProperty() {
|
||||
return this.selectedYearMonth;
|
||||
}
|
||||
|
||||
private void createWeekDaysCells() {
|
||||
for (int i = 0; i < this.daysPerWeek; ++i) {
|
||||
DateCell cell = new DateCell();
|
||||
cell.getStyleClass().add("day-name-cell");
|
||||
cell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
|
||||
cell.setAlignment(Pos.BASELINE_CENTER);
|
||||
this.weekDaysCells.add(cell);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
DateCell cell = new DateCell();
|
||||
cell.getStyleClass().add("week-number-cell");
|
||||
cell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
|
||||
this.weekNumberCells.add(cell);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected VBox createHeaderPane() {
|
||||
Label title = new Label(I18n.i18n("button.select_date"));
|
||||
title.getStyleClass().add("title");
|
||||
title.setFont(Font.font(ROBOTO, FontWeight.NORMAL, 12.0F));
|
||||
HBox titleContainer = new HBox(title);
|
||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
titleContainer.setPadding(new Insets(0, 0, 5, 0));
|
||||
this.selectedYearLabel = new Label();
|
||||
this.selectedYearLabel.getStyleClass().add(SPINNER_LABEL);
|
||||
this.selectedYearLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 14.0F));
|
||||
HBox yearLabelContainer = new HBox();
|
||||
yearLabelContainer.getStyleClass().add("spinner");
|
||||
yearLabelContainer.getChildren().addAll(this.selectedYearLabel);
|
||||
yearLabelContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
yearLabelContainer.setFillHeight(false);
|
||||
yearLabelContainer.setOnMouseClicked((click) -> {
|
||||
if (!this.yearsListView.isVisible()) {
|
||||
int yearIndex = Integer.parseInt(this.selectedYearLabel.getText()) - 1900 - 2;
|
||||
this.yearsListView.scrollTo(yearIndex >= 0 ? yearIndex : yearIndex + 2);
|
||||
this.hideTransition.stop();
|
||||
this.showTransition.play();
|
||||
}
|
||||
|
||||
});
|
||||
this.selectedDateLabel = new Label();
|
||||
this.selectedDateLabel.getStyleClass().add(SPINNER_LABEL);
|
||||
this.selectedDateLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 32.0F));
|
||||
HBox selectedDateContainer = new HBox();
|
||||
selectedDateContainer.getStyleClass().add("spinner");
|
||||
selectedDateContainer.getChildren().addAll(this.selectedDateLabel);
|
||||
selectedDateContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
selectedDateContainer.setOnMouseClicked((click) -> {
|
||||
if (this.yearsListView.isVisible()) {
|
||||
this.showTransition.stop();
|
||||
this.hideTransition.play();
|
||||
}
|
||||
|
||||
});
|
||||
VBox headerPanel = new VBox();
|
||||
headerPanel.getStyleClass().add("month-year-pane");
|
||||
headerPanel.setPadding(new Insets(12.0F, 24.0F, 12.0F, 24.0F));
|
||||
headerPanel.getChildren().addAll(titleContainer, yearLabelContainer, selectedDateContainer);
|
||||
return headerPanel;
|
||||
}
|
||||
|
||||
protected BorderPane createCalendarArrowsPane() {
|
||||
SVGGlyph leftChevron = new SVGGlyph(0, "CHEVRON_LEFT", "M 742,-37 90,614 Q 53,651 53,704.5 53,758 90,795 l 652,651 q 37,37 90.5,37 53.5,0 90.5,-37 l 75,-75 q 37,-37 37,-90.5 0,-53.5 -37,-90.5 L 512,704 998,219 q 37,-38 37,-91 0,-53 -37,-90 L 923,-37 Q 886,-74 832.5,-74 779,-74 742,-37 z", Color.GRAY);
|
||||
SVGGlyph rightChevron = new SVGGlyph(0, "CHEVRON_RIGHT", "m 1099,704 q 0,-52 -37,-91 L 410,-38 q -37,-37 -90,-37 -53,0 -90,37 l -76,75 q -37,39 -37,91 0,53 37,90 l 486,486 -486,485 q -37,39 -37,91 0,53 37,90 l 76,75 q 36,38 90,38 54,0 90,-38 l 652,-651 q 37,-37 37,-90 z", Color.GRAY);
|
||||
leftChevron.setSize(6.0F, 11.0F);
|
||||
rightChevron.setSize(6.0F, 11.0F);
|
||||
this.backMonthButton = new JFXButton();
|
||||
this.backMonthButton.setMinSize(40.0F, 40.0F);
|
||||
this.backMonthButton.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(40.0F), Insets.EMPTY)));
|
||||
this.backMonthButton.getStyleClass().add("left-button");
|
||||
this.backMonthButton.setGraphic(leftChevron);
|
||||
this.backMonthButton.setRipplerFill(this.datePicker.getDefaultColor());
|
||||
this.backMonthButton.setOnAction((t) -> this.forward(-1, ChronoUnit.MONTHS, false, true));
|
||||
this.forwardMonthButton = new JFXButton();
|
||||
this.forwardMonthButton.setMinSize(40.0F, 40.0F);
|
||||
this.forwardMonthButton.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(40.0F), Insets.EMPTY)));
|
||||
this.forwardMonthButton.getStyleClass().add("right-button");
|
||||
this.forwardMonthButton.setGraphic(rightChevron);
|
||||
this.forwardMonthButton.setRipplerFill(this.datePicker.getDefaultColor());
|
||||
this.forwardMonthButton.setOnAction((t) -> this.forward(1, ChronoUnit.MONTHS, false, true));
|
||||
BorderPane arrowsContainer = new BorderPane();
|
||||
arrowsContainer.setLeft(this.backMonthButton);
|
||||
arrowsContainer.setRight(this.forwardMonthButton);
|
||||
arrowsContainer.setPadding(new Insets(4.0F, 12.0F, 2.0F, 12.0F));
|
||||
arrowsContainer.setPickOnBounds(false);
|
||||
return arrowsContainer;
|
||||
}
|
||||
|
||||
protected BorderPane createCalendarMonthLabelPane() {
|
||||
this.monthYearLabel = new Label();
|
||||
this.monthYearLabel.getStyleClass().add(SPINNER_LABEL);
|
||||
this.monthYearLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 13.0F));
|
||||
SVGContainer svgContainer = new SVGContainer(13);
|
||||
HBox box = new HBox(monthYearLabel, svgContainer);
|
||||
box.setSpacing(3);
|
||||
box.setAlignment(Pos.CENTER);
|
||||
FXUtils.onChangeAndOperate(this.yearsListView.visibleProperty(), b -> svgContainer.setIcon(b ? SVG.ARROW_DROP_UP : SVG.ARROW_DROP_DOWN));
|
||||
FXUtils.onClicked(box, () -> {
|
||||
if (!this.yearsListView.isVisible()) {
|
||||
int yearIndex = Integer.parseInt(this.selectedYearLabel.getText()) - 1900 - 2;
|
||||
this.yearsListView.scrollTo(yearIndex >= 0 ? yearIndex : yearIndex + 2);
|
||||
this.hideTransition.stop();
|
||||
this.showTransition.play();
|
||||
} else {
|
||||
this.showTransition.stop();
|
||||
this.hideTransition.play();
|
||||
}
|
||||
});
|
||||
BorderPane monthContainer = new BorderPane();
|
||||
monthContainer.setMinHeight(50.0F);
|
||||
monthContainer.setCenter(box);
|
||||
monthContainer.setPadding(new Insets(2.0F, 12.0F, 2.0F, 12.0F));
|
||||
return monthContainer;
|
||||
}
|
||||
|
||||
protected HBox createActions() {
|
||||
JFXButton cancelButton = new JFXButton(I18n.i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
FXUtils.onClicked(cancelButton, () -> fireEvent(new DialogCloseEvent()));
|
||||
|
||||
JFXButton acceptButton = new JFXButton(I18n.i18n("button.ok"));
|
||||
acceptButton.getStyleClass().add("dialog-accept");
|
||||
FXUtils.onClicked(acceptButton, () -> {
|
||||
datePicker.setValue(valueProperty.get());
|
||||
fireEvent(new DialogCloseEvent());
|
||||
});
|
||||
|
||||
HBox hBox = new HBox(cancelButton, acceptButton);
|
||||
hBox.setPadding(new Insets(0, 5, 3, 0));
|
||||
hBox.setSpacing(5);
|
||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
return hBox;
|
||||
}
|
||||
|
||||
void updateContentGrid() {
|
||||
this.contentGrid.getColumnConstraints().clear();
|
||||
this.contentGrid.getChildren().clear();
|
||||
int colsNumber = this.daysPerWeek + (this.datePicker.isShowWeekNumbers() ? 1 : 0);
|
||||
ColumnConstraints columnConstraints = new ColumnConstraints();
|
||||
columnConstraints.setPercentWidth(100.0F);
|
||||
|
||||
for (int i = 0; i < colsNumber; ++i) {
|
||||
this.contentGrid.getColumnConstraints().add(columnConstraints);
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.daysPerWeek; ++i) {
|
||||
this.contentGrid.add(this.weekDaysCells.get(i), i + colsNumber - this.daysPerWeek, 1);
|
||||
}
|
||||
|
||||
if (this.datePicker.isShowWeekNumbers()) {
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
this.contentGrid.add(this.weekNumberCells.get(i), 0, i + 2);
|
||||
}
|
||||
}
|
||||
|
||||
for (int row = 0; row < 6; ++row) {
|
||||
for (int col = 0; col < this.daysPerWeek; ++col) {
|
||||
this.contentGrid.add(this.dayCells.get(row * this.daysPerWeek + col), col + colsNumber - this.daysPerWeek, row + 2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
this.updateDayNameCells();
|
||||
this.updateValues();
|
||||
}
|
||||
|
||||
private void updateDayNameCells() {
|
||||
int weekFirstDay = WeekFields.of(this.getLocale()).getFirstDayOfWeek().getValue();
|
||||
LocalDate date = LocalDate.of(2009, 7, 12 + weekFirstDay);
|
||||
|
||||
for (int i = 0; i < this.daysPerWeek; ++i) {
|
||||
String name = this.weekDayNameFormatter.withLocale(this.getLocale()).format(date.plusDays(i));
|
||||
if (!LocaleUtils.isChinese(this.weekNumberFormatter.getLocale())) {
|
||||
name = name.substring(0, 1).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
this.weekDaysCells.get(i).setText(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void updateValues() {
|
||||
this.updateWeekNumberDateCells();
|
||||
this.updateDayCells();
|
||||
this.updateMonthYearPane();
|
||||
}
|
||||
|
||||
void updateWeekNumberDateCells() {
|
||||
if (this.datePicker.isShowWeekNumbers()) {
|
||||
Locale locale = this.getLocale();
|
||||
LocalDate firstDayOfMonth = this.selectedYearMonth.get().atDay(1);
|
||||
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
LocalDate date = firstDayOfMonth.plusWeeks(i);
|
||||
String weekNumber = this.weekNumberFormatter.withLocale(locale).withDecimalStyle(DecimalStyle.of(locale)).format(date);
|
||||
this.weekNumberCells.get(i).setText(weekNumber);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void updateDayCells() {
|
||||
Locale locale = this.getLocale();
|
||||
Chronology chrono = this.getPrimaryChronology();
|
||||
int firstDayOfWeek = WeekFields.of(this.getLocale()).getFirstDayOfWeek().getValue();
|
||||
int firstOfMonthIndex = this.selectedYearMonth.get().atDay(1).getDayOfWeek().getValue() - firstDayOfWeek;
|
||||
firstOfMonthIndex += firstOfMonthIndex < 0 ? this.daysPerWeek : 0;
|
||||
YearMonth currentYearMonth = this.selectedYearMonth.get();
|
||||
int daysInCurMonth = -1;
|
||||
|
||||
for (int i = 0; i < 6 * this.daysPerWeek; ++i) {
|
||||
DateCell dayCell = this.dayCells.get(i);
|
||||
try {
|
||||
if (daysInCurMonth == -1) {
|
||||
daysInCurMonth = currentYearMonth.lengthOfMonth();
|
||||
}
|
||||
|
||||
int dayIndex = i - firstOfMonthIndex + 1;
|
||||
LocalDate date;
|
||||
if (dayIndex <= 0) {
|
||||
YearMonth previous = currentYearMonth.minusMonths(1);
|
||||
date = previous.atDay(previous.lengthOfMonth() + dayIndex);
|
||||
} else if (dayIndex > daysInCurMonth) {
|
||||
YearMonth next = currentYearMonth.plusMonths(1);
|
||||
date = next.atDay(dayIndex - daysInCurMonth);
|
||||
} else {
|
||||
date = currentYearMonth.atDay(dayIndex);
|
||||
}
|
||||
this.dayCellDates[i] = date;
|
||||
if (date.equals(LocalDate.now())) {
|
||||
dayCell.getStyleClass().add("today");
|
||||
} else {
|
||||
dayCell.getStyleClass().removeAll("today");
|
||||
}
|
||||
|
||||
if (date.equals(valueProperty.get())) {
|
||||
dayCell.getStyleClass().add("selected");
|
||||
this.currentFocusedDayCell = dayCell;
|
||||
} else {
|
||||
dayCell.getStyleClass().removeAll("selected");
|
||||
}
|
||||
|
||||
ChronoLocalDate cDate = chrono.date(date);
|
||||
String cellText = this.dayCellFormatter.withLocale(locale).withChronology(chrono).withDecimalStyle(DecimalStyle.of(locale)).format(cDate);
|
||||
dayCell.setText(cellText);
|
||||
if (i < firstOfMonthIndex) {
|
||||
dayCell.getStyleClass().add("previous-month");
|
||||
dayCell.getStyleClass().removeAll("next-month");
|
||||
dayCell.setDisable(true);
|
||||
} else if (i >= firstOfMonthIndex + daysInCurMonth) {
|
||||
dayCell.getStyleClass().add("next-month");
|
||||
dayCell.getStyleClass().removeAll("previous-month");
|
||||
dayCell.setDisable(true);
|
||||
} else {
|
||||
dayCell.getStyleClass().removeAll("previous-month", "next-month");
|
||||
dayCell.setDisable(false);
|
||||
}
|
||||
|
||||
dayCell.updateItem(date, false);
|
||||
} catch (DateTimeException var13) {
|
||||
dayCell.setText("");
|
||||
dayCell.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void updateMonthYearPane() {
|
||||
YearMonth yearMonth = this.selectedYearMonth.get();
|
||||
this.selectedDateLabel.setText(fullFormatter.withLocale(this.getLocale()).format(Objects.requireNonNullElseGet(valueProperty.get(), LocalDate::now)));
|
||||
|
||||
this.selectedYearLabel.setText(this.formatYear(yearMonth));
|
||||
this.monthYearLabel.setText(this.formatMonth(yearMonth) + " " + this.formatYear(yearMonth));
|
||||
Chronology chrono = this.datePicker.getChronology();
|
||||
LocalDate firstDayOfMonth = yearMonth.atDay(1);
|
||||
this.backMonthButton.setDisable(!this.isValidDate(chrono, firstDayOfMonth, -1, ChronoUnit.DAYS));
|
||||
this.forwardMonthButton.setDisable(!this.isValidDate(chrono, firstDayOfMonth, 1, ChronoUnit.MONTHS));
|
||||
}
|
||||
|
||||
private String formatMonth(YearMonth yearMonth) {
|
||||
try {
|
||||
Chronology chrono = this.getPrimaryChronology();
|
||||
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
|
||||
return this.monthFormatter.withLocale(this.getLocale()).withChronology(chrono).format(cDate);
|
||||
} catch (DateTimeException var4) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String formatYear(YearMonth yearMonth) {
|
||||
try {
|
||||
Chronology chrono = this.getPrimaryChronology();
|
||||
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
|
||||
return this.yearFormatter.withLocale(this.getLocale()).withChronology(chrono).withDecimalStyle(DecimalStyle.of(this.getLocale())).format(cDate);
|
||||
} catch (DateTimeException var4) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
protected LocalDate dayCellDate(DateCell dateCell) {
|
||||
assert this.dayCellDates != null;
|
||||
|
||||
return this.dayCellDates[this.dayCells.indexOf(dateCell)];
|
||||
}
|
||||
|
||||
protected void forward(int offset, ChronoUnit unit, boolean focusDayCell, boolean withAnimation) {
|
||||
if (withAnimation && (this.tempImageTransition == null || this.tempImageTransition.getStatus() == Status.STOPPED)) {
|
||||
Pane monthContent = (Pane) this.calendarPlaceHolder.getChildren().get(0);
|
||||
this.getParent().setManaged(false);
|
||||
SnapshotParameters snapShotparams = new SnapshotParameters();
|
||||
snapShotparams.setFill(Color.TRANSPARENT);
|
||||
WritableImage temp = monthContent.snapshot(snapShotparams, new WritableImage((int) monthContent.getWidth(), (int) monthContent.getHeight()));
|
||||
ImageView tempImage = new ImageView(temp);
|
||||
this.calendarPlaceHolder.getChildren().add(this.calendarPlaceHolder.getChildren().size() - 2, tempImage);
|
||||
TranslateTransition imageTransition = new TranslateTransition(Duration.millis(160.0F), tempImage);
|
||||
imageTransition.setToX((double) (-offset) * this.calendarPlaceHolder.getWidth());
|
||||
imageTransition.setOnFinished((finish) -> this.calendarPlaceHolder.getChildren().remove(tempImage));
|
||||
monthContent.setTranslateX((double) offset * this.calendarPlaceHolder.getWidth());
|
||||
TranslateTransition contentTransition = new TranslateTransition(Duration.millis(160.0F), monthContent);
|
||||
contentTransition.setToX(0.0F);
|
||||
this.tempImageTransition = new ParallelTransition(imageTransition, contentTransition);
|
||||
this.tempImageTransition.setOnFinished((finish) -> {
|
||||
this.calendarPlaceHolder.getChildren().remove(tempImage);
|
||||
this.getParent().setManaged(true);
|
||||
});
|
||||
this.tempImageTransition.play();
|
||||
}
|
||||
|
||||
YearMonth yearMonth = this.selectedYearMonth.get();
|
||||
DateCell dateCell = this.currentFocusedDayCell;
|
||||
if (dateCell == null || this.dayCellDate(dateCell).getMonth() != yearMonth.getMonth()) {
|
||||
dateCell = this.findDayCellOfDate(yearMonth.atDay(1));
|
||||
}
|
||||
|
||||
this.goToDayCell(dateCell, offset, unit, focusDayCell);
|
||||
}
|
||||
|
||||
private void goToDayCell(DateCell dateCell, int offset, ChronoUnit unit, boolean focusDayCell) {
|
||||
this.goToDate(this.dayCellDate(dateCell).plus(offset, unit), focusDayCell);
|
||||
}
|
||||
|
||||
private void goToDate(LocalDate date, boolean focusDayCell) {
|
||||
if (this.isValidDate(this.datePicker.getChronology(), date)) {
|
||||
this.valueProperty.set(date);
|
||||
this.selectedYearMonth.set(YearMonth.from(date));
|
||||
if (focusDayCell) {
|
||||
this.findDayCellOfDate(date).requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void selectDayCell(DateCell dateCell) {
|
||||
this.valueProperty.set(this.dayCellDate(dateCell));
|
||||
}
|
||||
|
||||
private DateCell findDayCellOfDate(LocalDate date) {
|
||||
for (int i = 0; i < this.dayCellDates.length; ++i) {
|
||||
if (date.equals(this.dayCellDates[i])) {
|
||||
return this.dayCells.get(i);
|
||||
}
|
||||
}
|
||||
|
||||
return this.dayCells.get(this.dayCells.size() / 2 + 1);
|
||||
}
|
||||
|
||||
void init() {
|
||||
this.contentGrid.setOpacity(1.0F);
|
||||
this.yearsListView.setOpacity(0.0F);
|
||||
this.yearsListView.setVisible(false);
|
||||
}
|
||||
|
||||
void clearFocus() {
|
||||
LocalDate focusDate = this.valueProperty.get();
|
||||
if (focusDate == null) {
|
||||
focusDate = LocalDate.now();
|
||||
}
|
||||
|
||||
if (YearMonth.from(focusDate).equals(this.selectedYearMonth.get())) {
|
||||
this.goToDate(focusDate, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void createDayCells() {
|
||||
for (int row = 0; row < 6; ++row) {
|
||||
for (int col = 0; col < this.daysPerWeek; ++col) {
|
||||
DateCell dayCell = this.createDayCell();
|
||||
dayCell.addEventHandler(MouseEvent.MOUSE_CLICKED, (click) -> {
|
||||
if (click.getButton() == MouseButton.PRIMARY) {
|
||||
DateCell selectedDayCell = (DateCell) click.getSource();
|
||||
this.selectDayCell(selectedDayCell);
|
||||
}
|
||||
});
|
||||
dayCell.setOnMouseEntered((event) -> dayCell.getStyleClass().add("hovered"));
|
||||
dayCell.setOnMouseExited((event) -> dayCell.getStyleClass().removeAll("hovered"));
|
||||
dayCell.setAlignment(Pos.BASELINE_CENTER);
|
||||
dayCell.setBorder(new Border(new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(5.0F))));
|
||||
dayCell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
|
||||
this.dayCells.add(dayCell);
|
||||
}
|
||||
}
|
||||
|
||||
this.dayCellDates = new LocalDate[6 * this.daysPerWeek];
|
||||
this.updateContentGrid();
|
||||
}
|
||||
|
||||
private DateCell createDayCell() {
|
||||
DateCell dayCell = null;
|
||||
if (this.datePicker.getDayCellFactory() != null) {
|
||||
dayCell = this.datePicker.getDayCellFactory().call(this.datePicker);
|
||||
}
|
||||
|
||||
if (dayCell == null) {
|
||||
dayCell = new DateCell();
|
||||
}
|
||||
dayCell.getStyleClass().add("day-cell");
|
||||
dayCell.setPrefSize(36.0F, 36.0F);
|
||||
dayCell.setDisable(false);
|
||||
dayCell.setStyle(null);
|
||||
dayCell.setGraphic(null);
|
||||
dayCell.setTooltip(null);
|
||||
|
||||
return dayCell;
|
||||
}
|
||||
|
||||
protected Chronology getPrimaryChronology() {
|
||||
return this.datePicker.getChronology();
|
||||
}
|
||||
|
||||
protected Locale getLocale() {
|
||||
return SettingsManager.settings().languageProperty().get().getLocale();
|
||||
}
|
||||
|
||||
protected boolean isValidDate(Chronology chrono, LocalDate date, int offset, ChronoUnit unit) {
|
||||
return date != null && this.isValidDate(chrono, date.plus(offset, unit));
|
||||
}
|
||||
|
||||
protected boolean isValidDate(Chronology chrono, LocalDate date) {
|
||||
try {
|
||||
if (date != null) {
|
||||
chrono.date(date);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (DateTimeException var4) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.adapters.ReflectionHelper;
|
||||
import com.jfoenix.adapters.skins.DatePickerSkin;
|
||||
import com.jfoenix.controls.JFXDatePicker;
|
||||
import com.jfoenix.controls.JFXDialog;
|
||||
import com.jfoenix.controls.JFXDialog.DialogTransition;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.util.Stack;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class JFXDatePickerSkin extends DatePickerSkin {
|
||||
public static final String PROPERTY_DATE_PICKER_CONTENTS = JFXDatePicker.class.getName() + ".dialog.contents";
|
||||
|
||||
private final JFXDatePicker jfxDatePicker;
|
||||
private TextField displayNode;
|
||||
private JFXDatePickerContent content;
|
||||
private JFXDialog dialog;
|
||||
|
||||
public JFXDatePickerSkin(JFXDatePicker datePicker) {
|
||||
super(datePicker);
|
||||
this.jfxDatePicker = datePicker;
|
||||
|
||||
try {
|
||||
Object expressionHelper = ReflectionHelper.getFieldContent(datePicker.focusedProperty().getClass().getSuperclass(), datePicker.focusedProperty(), "helper");
|
||||
ChangeListener<Boolean>[] changeListeners = ReflectionHelper.getFieldContent(expressionHelper, "changeListeners");
|
||||
|
||||
int i = changeListeners.length - 1;
|
||||
if (i >= 0) {
|
||||
while (i >= 0 && changeListeners[i] == null) {
|
||||
--i;
|
||||
}
|
||||
if (i >= 0) datePicker.focusedProperty().removeListener(changeListeners[i]);
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
LOG.warning("Cannot remove focusedProperty listener", e);
|
||||
}
|
||||
|
||||
datePicker.focusedProperty().addListener((obj, oldVal, newVal) -> {
|
||||
if (this.getEditor() != null && !newVal) {
|
||||
this.setTextFromTextFieldIntoComboBoxValue2();
|
||||
}
|
||||
|
||||
});
|
||||
var arrowBox = new HBox(new RipplerContainer(SVG.EDIT_CALENDAR.createIcon(30)));
|
||||
arrowBox.setAlignment(Pos.CENTER);
|
||||
this.setArrow(arrowBox);
|
||||
this.getArrowButton().getChildren().setAll(arrowBox);
|
||||
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
this.getArrowButton().setPadding(new Insets(1.0F, 8.0F, 1.0F, 8.0F));
|
||||
this.registerChangeListener2(datePicker.converterProperty(), "CONVERTER", this::updateDisplayNode2);
|
||||
this.registerChangeListener2(datePicker.dayCellFactoryProperty(), "DAY_CELL_FACTORY", () -> {
|
||||
this.updateDisplayNode2();
|
||||
this.content = null;
|
||||
this.setPopup2(null);
|
||||
});
|
||||
this.registerChangeListener2(datePicker.editorProperty(), "EDITOR", this::getEditableInputNode2);
|
||||
this.registerChangeListener2(datePicker.showingProperty(), "SHOWING", () -> {
|
||||
if (this.jfxDatePicker.isShowing()) {
|
||||
if (this.content != null) {
|
||||
LocalDate date = this.jfxDatePicker.getValue();
|
||||
this.content.displayedYearMonthProperty().set(date != null ? YearMonth.from(date) : YearMonth.now());
|
||||
this.content.updateValues();
|
||||
}
|
||||
|
||||
this.show();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
});
|
||||
this.registerChangeListener2(datePicker.showWeekNumbersProperty(), "SHOW_WEEK_NUMBERS", () -> {
|
||||
if (this.content != null) {
|
||||
this.content.updateContentGrid();
|
||||
this.content.updateWeekNumberDateCells();
|
||||
}
|
||||
|
||||
});
|
||||
this.registerChangeListener2(datePicker.valueProperty(), "VALUE", () -> {
|
||||
this.updateDisplayNode2();
|
||||
this.jfxDatePicker.fireEvent(new ActionEvent());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public JFXDatePickerContent getPopupContent() {
|
||||
if (this.content == null) {
|
||||
this.content = new JFXDatePickerContent(this.jfxDatePicker);
|
||||
}
|
||||
|
||||
return this.content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
if (!((JFXDatePicker) this.getSkinnable()).isOverLay()) {
|
||||
super.show();
|
||||
}
|
||||
|
||||
if (this.content != null) {
|
||||
this.content.init();
|
||||
this.content.clearFocus();
|
||||
}
|
||||
|
||||
if (((JFXDatePicker) this.getSkinnable()).isOverLay() && this.dialog == null) {
|
||||
StackPane dialogParent = this.jfxDatePicker.getDialogParent();
|
||||
if (dialogParent == null) {
|
||||
dialogParent = (StackPane) this.getSkinnable().getScene().getRoot();
|
||||
}
|
||||
|
||||
this.dialog = new JFXDialog(dialogParent, this.getPopupContent(), DialogTransition.CENTER, true) {
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
Object o = getDialogContainer().getProperties().get(PROPERTY_DATE_PICKER_CONTENTS);
|
||||
if (o instanceof Stack<?> stack) {
|
||||
stack.pop();
|
||||
}
|
||||
jfxDatePicker.hide();
|
||||
}
|
||||
};
|
||||
this.getPopupContent().addEventHandler(DialogCloseEvent.CLOSE, e -> this.dialog.close());
|
||||
getArrowButton().setOnMouseClicked((click) -> {
|
||||
if (((JFXDatePicker) this.getSkinnable()).isOverLay()) {
|
||||
StackPane parent = this.jfxDatePicker.getDialogParent();
|
||||
if (parent == null) {
|
||||
parent = (StackPane) this.getSkinnable().getScene().getRoot();
|
||||
}
|
||||
Stack<JFXDatePickerContent> contentStack = (Stack<JFXDatePickerContent>) parent.getProperties().computeIfAbsent(PROPERTY_DATE_PICKER_CONTENTS, k -> new Stack<>());
|
||||
contentStack.push(this.getPopupContent());
|
||||
|
||||
this.getPopupContent().valueProperty.set(this.getSkinnable().getValue());
|
||||
this.dialog.show(parent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
JFXDialog getDialog() {
|
||||
return this.dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getDisplayNode() {
|
||||
if (this.displayNode == null) {
|
||||
this.displayNode = this.getEditableInputNode2();
|
||||
this.displayNode.getStyleClass().add("date-picker-display-node");
|
||||
this.updateDisplayNode2();
|
||||
}
|
||||
|
||||
this.displayNode.setEditable(this.jfxDatePicker.isEditable());
|
||||
return this.displayNode;
|
||||
}
|
||||
|
||||
public void syncWithAutoUpdate() {
|
||||
if (!this.getPopup2().isShowing() && this.jfxDatePicker.isShowing()) {
|
||||
this.jfxDatePicker.hide();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -47,9 +47,11 @@ import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.macos.LibSystem;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Gdi32;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Shell32;
|
||||
import org.jackhuang.hmcl.util.platform.windows.User32;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
@@ -326,6 +328,9 @@ public final class Launcher extends Application {
|
||||
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
|
||||
}
|
||||
|
||||
@Nullable Boolean isRoot = isRootUser();
|
||||
LOG.info("Administrator User: " + (isRoot == null ? "Unknown" : isRoot.toString()));
|
||||
|
||||
LOG.info("Zlib Compatible: " + ZlibUtils.IS_ZLIB_COMPATIBLE);
|
||||
|
||||
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
|
||||
@@ -382,6 +387,9 @@ public final class Launcher extends Application {
|
||||
return;
|
||||
if (!NativeUtils.USE_JNA || Shell32.INSTANCE == null)
|
||||
return;
|
||||
@Nullable Path thisJar = JarUtils.thisJarPath();
|
||||
if (thisJar == null || !Files.isRegularFile(thisJar) || !"exe".equalsIgnoreCase(FileUtils.getExtension(thisJar)))
|
||||
return;
|
||||
|
||||
try {
|
||||
int hr = Shell32.INSTANCE.SetCurrentProcessExplicitAppUserModelID(new WString(Metadata.WINDOWS_APP_USER_MODEL_ID));
|
||||
@@ -588,5 +596,68 @@ public final class Launcher extends Application {
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
private static @Nullable Boolean isRootUser() {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS -> {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
|
||||
if (reg == null)
|
||||
return null;
|
||||
try {
|
||||
return reg.exists(WinReg.HKEY.HKEY_USERS, "S-1-5-19");
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to check Windows administrator privileges", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
case LINUX -> {
|
||||
Path selfStatus = Paths.get("/proc/self/status");
|
||||
if (Files.exists(selfStatus)) {
|
||||
try (var reader = Files.newBufferedReader(selfStatus)) {
|
||||
@Nullable String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("Uid:")) {
|
||||
String[] parts = line.split("\t");
|
||||
if (parts.length >= 3) {
|
||||
String uid = parts[2].trim();
|
||||
return "0".equals(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to read " + selfStatus, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
case MACOS -> {
|
||||
if (NativeUtils.USE_JNA) {
|
||||
LibSystem libSystem = LibSystem.INSTANCE;
|
||||
if (libSystem != null) {
|
||||
try {
|
||||
return libSystem.geteuid() == 0;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to check macOS root privileges", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS.isUnixLike()) {
|
||||
@Nullable Path id = SystemUtils.which("id");
|
||||
if (id != null) {
|
||||
try {
|
||||
return "0".equals(SystemUtils.run(id.toString(), "-u").trim());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to run id -u", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
|
||||
}
|
||||
+32
-20
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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
|
||||
@@ -15,47 +15,55 @@
|
||||
* 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.instances;
|
||||
package org.jackhuang.hmcl.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class AddonCheckUpdatesTask<T extends LocalAddonFile> extends Task<List<LocalAddonFile.AddonUpdate>> {
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final List<Task<LocalAddonFile.AddonUpdate>> dependents;
|
||||
public class AddonCheckUpdatesTask extends Task<AddonUpdates> {
|
||||
private final List<Task<Pair<AddonUpdate, @Nullable AddonUpdate>>> dependents;
|
||||
|
||||
public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<T> addons) {
|
||||
this.downloadProvider = downloadProvider;
|
||||
public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<? extends LocalAddonFile> addons) {
|
||||
dependents = addons.stream().map(addon ->
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
LocalAddonFile.AddonUpdate candidate = null;
|
||||
AddonUpdate candidate = null;
|
||||
AddonUpdate candidateRelease = null;
|
||||
for (RemoteAddon.Source source : RemoteAddon.Source.values()) {
|
||||
LocalAddonFile.AddonUpdate update = null;
|
||||
AddonUpdate update = null;
|
||||
AddonUpdate updateRelease = null;
|
||||
try {
|
||||
update = addon.checkUpdates(downloadProvider, gameVersion, source);
|
||||
var u = addon.checkUpdates(downloadProvider, gameVersion, source);
|
||||
if (u != null) {
|
||||
update = u.key();
|
||||
updateRelease = u.value();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Cannot check update for addon %s.", addon.getFileName()), e);
|
||||
}
|
||||
if (update == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidate == null || candidate.targetVersion().datePublished().isBefore(update.targetVersion().datePublished())) {
|
||||
if (update == null) continue;
|
||||
if (candidate == null || candidate.targetVersion().datePublished().isBefore(update.targetVersion().datePublished()))
|
||||
candidate = update;
|
||||
}
|
||||
|
||||
if (updateRelease == null) continue;
|
||||
if (candidateRelease == null || candidateRelease.targetVersion().datePublished().isBefore(updateRelease.targetVersion().datePublished()))
|
||||
candidateRelease = updateRelease;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
if (candidate == null) return null; // If there's no candidate for all channels, then no candidate for release channel
|
||||
return Pair.pair(candidate, candidateRelease);
|
||||
}).setName(addon.getFileName()).setSignificance(TaskSignificance.MAJOR).withCounter("update.checking")
|
||||
).toList();
|
||||
|
||||
@@ -85,8 +93,12 @@ public class AddonCheckUpdatesTask<T extends LocalAddonFile> extends Task<List<L
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
setResult(dependents.stream()
|
||||
.map(Task::getResult)
|
||||
.filter(Objects::nonNull).toList());
|
||||
List<AddonUpdate> commonUpdates = new ArrayList<>(), releaseUpdates = new ArrayList<>();
|
||||
dependents.stream().map(Task::getResult).filter(Objects::nonNull)
|
||||
.forEachOrdered(pair -> {
|
||||
commonUpdates.add(pair.key());
|
||||
if (pair.value() != null) releaseUpdates.add(pair.value());
|
||||
});
|
||||
setResult(new AddonUpdates(commonUpdates, releaseUpdates));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonManager;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class AddonUpdateTask extends Task<Void> {
|
||||
private final Collection<Task<?>> dependents = new ArrayList<>();
|
||||
private final List<LocalAddonFile> failedAddons = new ArrayList<>();
|
||||
|
||||
public AddonUpdateTask(DownloadProvider downloadProvider, Path addonDirectory, List<AddonUpdate> addons) {
|
||||
setStage("addon.check_update.confirm");
|
||||
getProperties().put("total", addons.size());
|
||||
|
||||
for (AddonUpdate addon : addons) {
|
||||
LocalAddonFile local = addon.localAddonFile();
|
||||
RemoteAddon.Version remote = addon.targetVersion();
|
||||
boolean isDisabled = local.isDisabled();
|
||||
String fileName = remote.file().filename();
|
||||
if (isDisabled)
|
||||
fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION);
|
||||
String newFileName = fileName;
|
||||
Path targetFile = addonDirectory.resolve(newFileName);
|
||||
|
||||
dependents.add(Task
|
||||
.supplyAsync(Schedulers.io(), () -> Files.createTempFile("hmcl-addon-update-", ".tmp"))
|
||||
.thenComposeAsync(tempFile ->
|
||||
new FileDownloadTask(downloadProvider.getDownloadCandidates(remote.file().url()), tempFile)
|
||||
.setName(remote.name())
|
||||
.thenRunAsync(Schedulers.javafx(), () -> local.setOld(true))
|
||||
.thenRunAsync(Schedulers.io(), () -> Files.move(tempFile, targetFile))
|
||||
.whenComplete(Schedulers.javafx(), exception -> {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to delete temp file " + tempFile, e);
|
||||
}
|
||||
if (exception != null) {
|
||||
// restore state if failed
|
||||
local.setOld(false);
|
||||
if (isDisabled)
|
||||
local.markDisabled();
|
||||
} else {
|
||||
local.onUpdated(newFileName);
|
||||
if (!local.keepOldFiles()) {
|
||||
try {
|
||||
local.delete();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete outdated addon: " + local.getFile(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
).whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to update addon", exception);
|
||||
failedAddons.add(local);
|
||||
}
|
||||
}).withCounter("addon.check_update.confirm"));
|
||||
}
|
||||
}
|
||||
|
||||
public List<LocalAddonFile> getFailedAddons() {
|
||||
return failedAddons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() {
|
||||
notifyPropertiesChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelyingOnDependents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!isDependentsSucceeded())
|
||||
throw getException();
|
||||
}
|
||||
}
|
||||
@@ -129,8 +129,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
}
|
||||
});
|
||||
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), List.of("/minecraft"), it -> !"pack.json".equals(it), config));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), List.of("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack"));
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, List.of("/minecraft"), it -> !"pack.json".equals(it), config));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, List.of("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -66,9 +65,9 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modpack readManifest(ZipArchiveReader file, Path path, Charset encoding) throws IOException, JsonParseException {
|
||||
public Modpack readManifest(ZipArchiveReader file, Path path) throws IOException, JsonParseException {
|
||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class);
|
||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
|
||||
GameInstanceManifest game = JsonUtils.fromNonNullJson(gameJson, GameInstanceManifest.class);
|
||||
if (game.jar() == null)
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.game;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.Unzipper;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@@ -29,12 +28,10 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||
|
||||
private final Path zipFile;
|
||||
private final Charset charset;
|
||||
private final String name;
|
||||
|
||||
public ManuallyCreatedModpackInstallTask(Path zipFile, Charset charset, String name) {
|
||||
public ManuallyCreatedModpackInstallTask(Path zipFile, String name) {
|
||||
this.zipFile = zipFile;
|
||||
this.charset = charset;
|
||||
this.name = name;
|
||||
|
||||
setName(i18n("modpack.installing"));
|
||||
@@ -51,7 +48,6 @@ public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||
new Unzipper(zipFile, dest)
|
||||
.setSubDirectory(subdirectory)
|
||||
.setTerminateIfSubDirectoryNotExists()
|
||||
.setEncoding(charset)
|
||||
.unzip();
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -90,8 +89,8 @@ public final class ModpackHelper {
|
||||
return "zip".equals(ext) || "mrpack".equals(ext);
|
||||
}
|
||||
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
||||
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file, charset)) {
|
||||
public static Modpack readModpackManifest(Path file) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
||||
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file)) {
|
||||
// Order for trying detecting manifest is necessary here.
|
||||
// Do not change to iterating providers.
|
||||
for (ModpackProvider provider : new ModpackProvider[]{
|
||||
@@ -102,7 +101,7 @@ public final class ModpackHelper {
|
||||
MultiMCModpackProvider.INSTANCE,
|
||||
ServerModpackProvider.INSTANCE}) {
|
||||
try {
|
||||
return provider.readManifest(zipFile, file, charset);
|
||||
return provider.readManifest(zipFile, file);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
@@ -173,6 +172,7 @@ public final class ModpackHelper {
|
||||
|
||||
return new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instanceId)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStage("hmcl.modpack")
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
|
||||
@@ -180,12 +180,13 @@ public final class ModpackHelper {
|
||||
return Files.exists(Paths.get("externalgames").resolve(name));
|
||||
}
|
||||
|
||||
public static Task<?> getInstallManuallyCreatedModpackTask(Path zipFile, String name, Charset charset) {
|
||||
/// Extracts a manually created modpack and selects its new external game directory.
|
||||
public static Task<?> getInstallManuallyCreatedModpackTask(Path zipFile, String name) {
|
||||
if (isExternalGameNameConflicts(name)) {
|
||||
throw new IllegalArgumentException("name existing");
|
||||
}
|
||||
|
||||
return new ManuallyCreatedModpackInstallTask(zipFile, charset, name)
|
||||
return new ManuallyCreatedModpackInstallTask(zipFile, name)
|
||||
.thenAcceptAsync(Schedulers.javafx(), location -> {
|
||||
GameDirectory newGameDirectory = new GameDirectory(
|
||||
GameDirectoryManager.newGameDirectoryId(),
|
||||
@@ -193,7 +194,9 @@ public final class ModpackHelper {
|
||||
PortablePath.fromPath(location));
|
||||
GameDirectoryManager.addLocalGameDirectory(newGameDirectory);
|
||||
GameDirectoryManager.setSelectedGameDirectory(newGameDirectory);
|
||||
});
|
||||
})
|
||||
.withStage("hmcl.modpack")
|
||||
.withStagesHints("hmcl.modpack");
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, @Nullable String iconUrl) {
|
||||
@@ -233,19 +236,23 @@ public final class ModpackHelper {
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
||||
.withStage("hmcl.modpack")
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), instanceId))
|
||||
.withStage("hmcl.modpack")
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStage("hmcl.modpack")
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
/// Updates a server modpack with backup, repository refresh, and modpack stage hints.
|
||||
public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
switch (configuration.getType()) {
|
||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE: {
|
||||
HMCLGameInstance instance = repository.getInstance(instanceId);
|
||||
@@ -253,6 +260,7 @@ public final class ModpackHelper {
|
||||
instance,
|
||||
new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instance))
|
||||
.thenComposeAsync(repository.refreshAsync())
|
||||
.withStage("hmcl.modpack")
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
default:
|
||||
@@ -260,15 +268,14 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
return getUpdateTask(repository, zipFile, charset, instanceId, configuration, null);
|
||||
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
return getUpdateTask(repository, zipFile, instanceId, configuration, null);
|
||||
}
|
||||
|
||||
/// Creates an update task that respects optional-file selection.
|
||||
///
|
||||
/// @param repository the target repository
|
||||
/// @param zipFile the modpack archive
|
||||
/// @param charset the archive encoding
|
||||
/// @param instanceId the instance to update
|
||||
/// @param configuration the existing modpack configuration
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
|
||||
@@ -277,23 +284,26 @@ public final class ModpackHelper {
|
||||
public static Task<?> getUpdateTask(
|
||||
HMCLGameRepository repository,
|
||||
Path zipFile,
|
||||
Charset charset,
|
||||
GameInstanceID instanceId,
|
||||
ModpackConfiguration<?> configuration,
|
||||
@Nullable Set<String> excludedFiles)
|
||||
throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
|
||||
ModpackProvider provider = getProviderByType(configuration.getType());
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile);
|
||||
@Nullable ModpackProvider provider = getProviderByType(configuration.getType());
|
||||
if (provider == null) {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
.thenComposeAsync(repository.refreshAsync())
|
||||
.withStage("hmcl.modpack")
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
.thenComposeAsync(repository.refreshAsync())
|
||||
.withStage("hmcl.modpack")
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
|
||||
public static void toGameSettings(MultiMCInstanceConfiguration c, GameSettings.Instance setting) {
|
||||
|
||||
@@ -61,7 +61,6 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -70,7 +69,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
@@ -406,8 +404,7 @@ public final class Controllers {
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(Schedulers.io(), () -> {
|
||||
Charset encoding = CompressingUtils.findSuitableEncoding(modpackFile);
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile, encoding);
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
|
||||
return ModpackHelper.getInstallTask(
|
||||
repository, modpackFile, new GameInstanceID(modpack.getName()), modpack, null);
|
||||
})
|
||||
|
||||
@@ -50,9 +50,7 @@ import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
@@ -69,6 +67,7 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
@@ -76,12 +75,15 @@ import org.jackhuang.hmcl.ui.image.ImageLoader;
|
||||
import org.jackhuang.hmcl.ui.image.ImageUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.javafx.Subscription;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.w3c.dom.Document;
|
||||
@@ -106,6 +108,7 @@ import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.*;
|
||||
@@ -823,7 +826,7 @@ public final class FXUtils {
|
||||
property.addListener(binding);
|
||||
}
|
||||
|
||||
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
public static List<Subscription> bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
int itemCount = children.length;
|
||||
int childSelectedCount = 0;
|
||||
for (BooleanProperty child : children) {
|
||||
@@ -833,49 +836,46 @@ public final class FXUtils {
|
||||
|
||||
allEnabled.set(childSelectedCount == itemCount);
|
||||
|
||||
class Listener implements InvalidationListener {
|
||||
class AllListener {
|
||||
private int childSelectedCount;
|
||||
private boolean updating = false;
|
||||
private final List<Subscription> subscriptions = new ArrayList<>(children.length + 1);
|
||||
|
||||
public Listener(int childSelectedCount) {
|
||||
public AllListener(int childSelectedCount) {
|
||||
this.childSelectedCount = childSelectedCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
if (updating)
|
||||
return;
|
||||
|
||||
updating = true;
|
||||
try {
|
||||
boolean value = ((BooleanProperty) observable).get();
|
||||
|
||||
if (observable == allEnabled) {
|
||||
{
|
||||
subscriptions.add(Subscription.subscribe(allEnabled, () -> {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
try {
|
||||
boolean value = allEnabled.get();
|
||||
for (BooleanProperty child : children) {
|
||||
child.setValue(value);
|
||||
child.set(value);
|
||||
}
|
||||
childSelectedCount = value ? itemCount : 0;
|
||||
} else {
|
||||
if (value)
|
||||
childSelectedCount++;
|
||||
else
|
||||
childSelectedCount--;
|
||||
|
||||
allEnabled.set(childSelectedCount == itemCount);
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}));
|
||||
for (var child : children) {
|
||||
subscriptions.add(Subscription.subscribe(child, () -> {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
try {
|
||||
if (child.get()) childSelectedCount++;
|
||||
else childSelectedCount--;
|
||||
allEnabled.set(childSelectedCount == itemCount);
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InvalidationListener listener = new Listener(childSelectedCount);
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(listener);
|
||||
allEnabled.addListener(listener);
|
||||
for (BooleanProperty child : children) {
|
||||
child.addListener(weakListener);
|
||||
}
|
||||
return new AllListener(childSelectedCount).subscriptions;
|
||||
}
|
||||
|
||||
public static void setIcon(Stage stage) {
|
||||
@@ -1197,6 +1197,15 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static void onDoubleClicked(Node node, Runnable action) {
|
||||
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
|
||||
action.run();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void addMacOSCloseWindowHandler(Window window, @Nullable Supplier<Boolean> restriction) {
|
||||
window.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
@@ -1274,6 +1283,34 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyFiles(List<Path> paths) {
|
||||
copyFiles(paths.stream().map(Path::toFile).toList(), i18n("message.copied"));
|
||||
}
|
||||
|
||||
public static void copyFiles(List<File> files, @Nullable String toastMessage) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putFiles(files);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
|
||||
if (toastMessage != null && !Controllers.isStopped()) {
|
||||
Controllers.showToast(toastMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyImage(Image image) {
|
||||
copyImage(image, i18n("message.copied"));
|
||||
}
|
||||
|
||||
public static void copyImage(Image image, @Nullable String toastMessage) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putImage(image);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
|
||||
if (toastMessage != null && !Controllers.isStopped()) {
|
||||
Controllers.showToast(toastMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
if (segment.indexOf('<') < 0)
|
||||
return Collections.singletonList(new Text(segment));
|
||||
@@ -1524,4 +1561,52 @@ public final class FXUtils {
|
||||
|
||||
}
|
||||
|
||||
public static void chooseDateRange(Consumer<@NotNull Pair<@Nullable LocalDate, @Nullable LocalDate>> onConfirm) {
|
||||
Controllers.dialog(new DateRangeDialog(onConfirm));
|
||||
}
|
||||
|
||||
private static class DateRangeDialog extends JFXDialogLayout {
|
||||
|
||||
private final JFXDatePicker fromPicker, toPicker;
|
||||
|
||||
public DateRangeDialog(Consumer<Pair<LocalDate, LocalDate>> onConfirm) {
|
||||
super();
|
||||
|
||||
setHeading(new Label(i18n("button.select_date")));
|
||||
|
||||
GridPane body = new GridPane(4, 8);
|
||||
{
|
||||
Label fromLabel = new Label(i18n("button.select_date.from"));
|
||||
this.fromPicker = new JFXDatePicker(LocalDate.now());
|
||||
fromPicker.setOverLay(true);
|
||||
fromPicker.setDialogParent(Controllers.getDecorator().getDialogContainer());
|
||||
body.add(fromLabel, 0, 0);
|
||||
body.add(fromPicker, 1, 0);
|
||||
}
|
||||
{
|
||||
Label toLabel = new Label(i18n("button.select_date.to"));
|
||||
this.toPicker = new JFXDatePicker(LocalDate.now());
|
||||
toPicker.setOverLay(true);
|
||||
toPicker.setDialogParent(Controllers.getDecorator().getDialogContainer());
|
||||
body.add(toLabel, 0, 1);
|
||||
body.add(toPicker, 1, 1);
|
||||
}
|
||||
setBody(body);
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
|
||||
JFXButton okButton = new JFXButton(i18n("button.ok"));
|
||||
okButton.getStyleClass().add("dialog-accept");
|
||||
okButton.setOnAction(e -> {
|
||||
if (onConfirm != null) onConfirm.accept(Pair.pair(fromPicker.getValue(), toPicker.getValue()));
|
||||
fireEvent(new DialogCloseEvent());
|
||||
});
|
||||
|
||||
setActions(cancelButton, okButton);
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import javafx.scene.shape.SVGPath;
|
||||
/// All vector icons used in the launcher.
|
||||
///
|
||||
/// Unless otherwise stated,
|
||||
/// these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
|
||||
/// these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.set=Material+Symbols">Material Symbols</a>,
|
||||
/// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
|
||||
/// The view boxes of all icons are normalized to `0 0 24 24`.
|
||||
public enum SVG {
|
||||
@@ -49,6 +49,7 @@ public enum SVG {
|
||||
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
|
||||
CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v3H7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"),
|
||||
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
|
||||
DATE_RANGE("M7.2875 13.7125Q7 13.425 7 13t0.2875 -0.7125Q7.575 12 8 12t0.7125 0.2875Q9 12.575 9 13t-0.2875 0.7125Q8.425 14 8 14t-0.7125 -0.2875zm4 0Q11 13.425 11 13t0.2875 -0.7125Q11.575 12 12 12t0.7125 0.2875Q13 12.575 13 13t-0.2875 0.7125Q12.425 14 12 14t-0.7125 -0.2875zm4 0Q15 13.425 15 13t0.2875 -0.7125Q15.575 12 16 12t0.7125 0.2875Q17 12.575 17 13t-0.2875 0.7125Q16.425 14 16 14t-0.7125 -0.2875zM5 22q-0.825 0 -1.4125 -0.5875T3 20l0 -14q0 -0.825 0.5875 -1.4125T5 4l1 0l0 -2l2 0l0 2l8 0l0 -2l2 0l0 2l1 0q0.825 0 1.4125 0.5875T21 6l0 14q0 0.825 -0.5875 1.4125T19 22L5 22zm0 -2l14 0l0 -10L5 10l0 10zm0 -12l14 0l0 -2L5 6l0 2zm0 0l0 -2 0 2z"),
|
||||
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
|
||||
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
|
||||
DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"),
|
||||
@@ -56,6 +57,7 @@ public enum SVG {
|
||||
DOWNLOAD("M12 16 7 11 8.4 9.55 11 12.15V4H13V12.15L15.6 9.55 17 11 12 16ZM6 20Q5.175 20 4.5875 19.4125T4 18V15H6V18H18V15H20V18Q20 18.825 19.4125 19.4125T18 20H6Z"),
|
||||
DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"),
|
||||
EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"),
|
||||
EDIT_CALENDAR("M5 22q-0.825 0 -1.4125 -0.5875T3 20l0 -14q0 -0.825 0.5875 -1.4125T5 4l1 0l0 -2l2 0l0 2l8 0l0 -2l2 0l0 2l1 0q0.825 0 1.4125 0.5875T21 6l0 5l-2 0l0 -1L5 10l0 10l7 0l0 2L5 22zm0 -14l14 0l0 -2L5 6l0 2zm0 0l0 -2 0 2zM14 22l0 -3.075l5.525 -5.5q0.225 -0.225 0.5 -0.325t0.55 -0.1q0.3 0 0.575 0.1125t0.5 0.3375l0.925 0.925q0.2 0.225 0.3125 0.5t0.1125 0.55q0 0.275 -0.1 0.5625T22.575 16.5L17.075 22L14 22zm7.5 -6.575 -0.925 -0.925 0.925 0.925zM15.5 20.5l0.95 0l3.025 -3.05 -0.45 -0.475 -0.475 -0.45 -3.05 3.025l0 0.95zm3.525 -3.525 -0.475 -0.45 0.925 0.925 -0.45 -0.475z"),
|
||||
ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
EXPLORE("M12 12Zm0 8q-3.325 0-5.6625-2.3375T4 12Q4 8.675 6.3375 6.3375T12 4q3.325-0 5.6625 2.3375T20 12q0 3.325-2.3375 5.6625T12 20Zm0 2q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22Zm0-8.5q.625 0 1.0625-.4375T13.5 12t-.4375-1.0625T12 10.5t-1.0625.4375T10.5 12t.4375 1.0625T12 13.5Zm-4.5 3 2-7 7-2-2 7-7 2Z"),
|
||||
EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"),
|
||||
@@ -109,6 +111,7 @@ public enum SVG {
|
||||
SCHEMA("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Zm2-2H9V19H6v2Zm0-8H9V11H6v2Zm10 0h3V11H16v2ZM6 5H9V3H6V5ZM7.5 4Zm0 8Zm10 0Zm-10 8Z"),
|
||||
SCHEMA_FILL("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Z"),
|
||||
SCREENSHOT_MONITOR("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8ZM4 17H20V5H4V17ZM4 17V5 17Z"),
|
||||
SCREENSHOT_MONITOR_FILL("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8Z"),
|
||||
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material
|
||||
SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"),
|
||||
SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"),
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
|
||||
@@ -34,6 +35,7 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
|
||||
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||
@@ -47,7 +49,9 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
maxHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.7));
|
||||
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
Label heading = new Label(i18n("update.changelog"));
|
||||
heading.setPadding(new Insets(0, 0, 8, 0));
|
||||
setHeading(heading);
|
||||
setBody(new JFXSpinner());
|
||||
|
||||
String url = CHANGELOG_URL + remoteVersion.channel().channelName + ".html";
|
||||
@@ -62,16 +66,18 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
Document document = Jsoup.parse(WebURL.toURL(url), 30 * 1000);
|
||||
Node node = document.selectFirst("h1[data-version=\"%s\"]".formatted(targetVersion));
|
||||
|
||||
if (node == null || !"h1".equals(node.nodeName())) {
|
||||
LOG.warning("Changelog not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
HTMLRenderer renderer = new HTMLRenderer(uri -> {
|
||||
LOG.info("Open link: " + uri);
|
||||
FXUtils.openLink(uri.toString());
|
||||
});
|
||||
|
||||
if (node == null || !"h1".equals(node.nodeName())) {
|
||||
LOG.warning("Changelog not found");
|
||||
Element list = new Element("ul");
|
||||
list.appendElement("li").text(i18n("update.changelog.empty"));
|
||||
return renderer.appendNode(list).mergeLineBreaks().render();
|
||||
}
|
||||
|
||||
do {
|
||||
if ("h1".equals(node.nodeName())) {
|
||||
String changelogVersion = node.attr("data-version");
|
||||
|
||||
@@ -64,6 +64,33 @@ public class ImageContainer extends StackPane {
|
||||
this.getChildren().setAll(imageView);
|
||||
}
|
||||
|
||||
public ImageContainer(Image image, double maxWidth, double maxHeight) {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
double width = maxWidth;
|
||||
double height = width / image.getWidth() * image.getHeight();
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight;
|
||||
width = height / image.getHeight() * image.getWidth();
|
||||
}
|
||||
|
||||
FXUtils.setLimitWidth(this, width);
|
||||
FXUtils.setLimitHeight(this, height);
|
||||
|
||||
imageView.setPreserveRatio(true);
|
||||
FXUtils.limitSize(imageView, width, height);
|
||||
StackPane.setAlignment(imageView, Pos.CENTER);
|
||||
|
||||
clip.setWidth(width);
|
||||
clip.setHeight(height);
|
||||
updateCornerRadius(getCornerRadius());
|
||||
this.setClip(clip);
|
||||
|
||||
setImage(image);
|
||||
|
||||
this.getChildren().setAll(imageView);
|
||||
}
|
||||
|
||||
private void updateCornerRadius(double radius) {
|
||||
clip.setArcWidth(radius);
|
||||
clip.setArcHeight(radius);
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXSnackbar;
|
||||
import com.jfoenix.controls.JFXSnackbarLayout;
|
||||
import com.jfoenix.skins.JFXDatePickerContent;
|
||||
import com.jfoenix.skins.JFXDatePickerSkin;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
@@ -81,6 +83,7 @@ import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Stack;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
@@ -1130,13 +1133,23 @@ public final class Decorator {
|
||||
}
|
||||
|
||||
Node newTarget;
|
||||
@Nullable JFXDialogPane currentDialogPane =
|
||||
(JFXDialogPane) getDialogContainer().getProperties()
|
||||
.get(DialogUtils.PROPERTY_DIALOG_PANE_INSTANCE);
|
||||
if (currentDialogPane != null && currentDialogPane.peek().isPresent()) {
|
||||
newTarget = currentDialogPane.peek().orElseThrow();
|
||||
} else {
|
||||
newTarget = navigator.getCurrentPage();
|
||||
{
|
||||
//noinspection unchecked
|
||||
@Nullable Stack<JFXDatePickerContent> stack =
|
||||
(Stack<JFXDatePickerContent>) getDialogContainer().getProperties()
|
||||
.get(JFXDatePickerSkin.PROPERTY_DATE_PICKER_CONTENTS);
|
||||
if (stack != null && !stack.isEmpty()) {
|
||||
newTarget = stack.peek();
|
||||
} else {
|
||||
@Nullable JFXDialogPane currentDialogPane =
|
||||
(JFXDialogPane) getDialogContainer().getProperties()
|
||||
.get(DialogUtils.PROPERTY_DIALOG_PANE_INSTANCE);
|
||||
if (currentDialogPane != null && currentDialogPane.peek().isPresent()) {
|
||||
newTarget = currentDialogPane.peek().orElseThrow();
|
||||
} else {
|
||||
newTarget = navigator.getCurrentPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (@Nullable Node node = target; node != null; node = node.getParent()) {
|
||||
|
||||
@@ -44,26 +44,20 @@ import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
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 LocalModpackPage extends ModpackPage {
|
||||
|
||||
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
|
||||
private Modpack manifest = null;
|
||||
private Charset charset;
|
||||
private final ObservableList<ModpackFile> allFiles = FXCollections.observableList(new ArrayList<>());
|
||||
private final ObservableSet<String> excludedFiles = FXCollections.observableSet(new HashSet<>());
|
||||
private final BooleanProperty loadingOptionalFiles = new SimpleBooleanProperty(false);
|
||||
@@ -98,8 +92,8 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !ModpackHelper.isExternalGameNameConflicts(str)
|
||||
&& GameDirectoryManager.getGameDirectories().stream()
|
||||
.noneMatch(existingProfile ->
|
||||
str.equals(GameDirectoryManager.getGameDirectoryCustomName(existingProfile)))),
|
||||
.noneMatch(existingProfile ->
|
||||
str.equals(GameDirectoryManager.getGameDirectoryCustomName(existingProfile)))),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId));
|
||||
}
|
||||
});
|
||||
@@ -125,11 +119,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
}
|
||||
|
||||
showSpinner();
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile))
|
||||
.thenApplyAsync(encoding -> {
|
||||
charset = encoding;
|
||||
return ModpackHelper.readModpackManifest(selectedFile, encoding);
|
||||
})
|
||||
Task.supplyAsync(() -> ModpackHelper.readModpackManifest(selectedFile))
|
||||
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
|
||||
if (exception instanceof ManuallyCreatedModpackException) {
|
||||
hideSpinner();
|
||||
@@ -142,7 +132,8 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
}
|
||||
|
||||
Controllers.confirm(i18n("modpack.type.manual.warning"), i18n("install.modpack"), MessageDialogPane.MessageType.WARNING,
|
||||
() -> {},
|
||||
() -> {
|
||||
},
|
||||
controller::onEnd);
|
||||
|
||||
controller.getSettings().put(MODPACK_MANUALLY_CREATED, true);
|
||||
@@ -163,7 +154,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
|
||||
if (name == null) {
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
txtModpackName.setText(manifest.getName().trim());
|
||||
txtModpackName.setText(processModpackName(manifest.getName()));
|
||||
}
|
||||
|
||||
btnDescription.setVisible(StringUtils.isNotBlank(manifest.getDescription()));
|
||||
@@ -223,7 +214,6 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
|
||||
private void finishInstall(String name) {
|
||||
controller.getSettings().put(MODPACK_NAME, name);
|
||||
controller.getSettings().put(MODPACK_CHARSET, charset);
|
||||
controller.getSettings().put(MODPACK_EXCLUDED_FILES, getExcludedFiles());
|
||||
controller.onFinish();
|
||||
}
|
||||
@@ -239,10 +229,20 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
|
||||
}
|
||||
|
||||
@Contract("null->null;!null->!null")
|
||||
public static @Nullable String processModpackName(@Nullable String name) {
|
||||
if (name == null)
|
||||
return null;
|
||||
|
||||
return name.trim()
|
||||
.replace(':', '_')
|
||||
.replace('/', '_')
|
||||
.replace('\\', '_');
|
||||
}
|
||||
|
||||
public static final SettingsMap.Key<Path> MODPACK_FILE = new SettingsMap.Key<>("MODPACK_FILE");
|
||||
public static final SettingsMap.Key<String> MODPACK_NAME = new SettingsMap.Key<>("MODPACK_NAME");
|
||||
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");
|
||||
public static final SettingsMap.Key<Set<String>> MODPACK_EXCLUDED_FILES = new SettingsMap.Key<>("MODPACK_EXCLUDED_FILES");
|
||||
|
||||
@@ -35,7 +35,6 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -88,12 +87,11 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
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);
|
||||
var excludedFiles = settings.get(LocalModpackPage.MODPACK_EXCLUDED_FILES);
|
||||
|
||||
if (isManuallyCreated) {
|
||||
return ModpackHelper.getInstallManuallyCreatedModpackTask(selected, name, charset);
|
||||
return ModpackHelper.getInstallManuallyCreatedModpackTask(selected, name);
|
||||
}
|
||||
|
||||
if ((selected == null && serverModpackManifest == null) || modpack == null || name == null) return null;
|
||||
@@ -107,9 +105,9 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
}
|
||||
try {
|
||||
if (serverModpackManifest != null) {
|
||||
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
|
||||
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
|
||||
} else {
|
||||
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)), excludedFiles);
|
||||
return ModpackHelper.getUpdateTask(repository, selected, instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)), excludedFiles);
|
||||
}
|
||||
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
|
||||
@@ -45,7 +45,7 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
throw new IllegalStateException("MODPACK_SERVER_MANIFEST should exist");
|
||||
|
||||
try {
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest.toModpack(null));
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest.toModpack());
|
||||
} catch (IOException e) {
|
||||
Controllers.dialog(i18n("modpack.type.server.malformed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
Platform.runLater(controller::onEnd);
|
||||
@@ -62,8 +62,7 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
txtModpackName.setText(name);
|
||||
txtModpackName.setDisable(true);
|
||||
} else {
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
txtModpackName.setText(manifest.getName().trim());
|
||||
txtModpackName.setText(LocalModpackPage.processModpackName(manifest.getName()));
|
||||
txtModpackName.getValidators().addAll(
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !repository.instanceIdConflicts(str)),
|
||||
|
||||
@@ -26,8 +26,8 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
@@ -54,8 +54,8 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
this.getChildren().setAll(
|
||||
title,
|
||||
createButton(MODPACK_TYPE_MCBBS, McbbsModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_SERVER, ServerModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_MODRINTH, ModrinthModpackExportTask.OPTION)
|
||||
createButton(MODPACK_TYPE_MODRINTH, ModrinthModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_SERVER, ServerModpackExportTask.OPTION)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,30 +31,31 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdate;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonManager;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdateTask;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdates;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
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.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.CSVTable;
|
||||
import org.jackhuang.hmcl.util.javafx.Subscription;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -62,15 +63,17 @@ import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane implements DecoratorPage {
|
||||
public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("addon.check_update")));
|
||||
|
||||
private final LocalAddonManager<F> localAddonManager;
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final Path localAddonDirectory;
|
||||
private final ObservableList<AddonUpdateObject> objects;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public AddonUpdatesPage(LocalAddonManager<F> localAddonManager, List<LocalAddonFile.AddonUpdate> updates) {
|
||||
this.localAddonManager = localAddonManager;
|
||||
public AddonUpdatesPage(DownloadProvider downloadProvider, Path localAddonDirectory, AddonUpdates updates) {
|
||||
this.downloadProvider = downloadProvider;
|
||||
this.localAddonDirectory = localAddonDirectory;
|
||||
|
||||
getStyleClass().add("gray-background");
|
||||
|
||||
@@ -96,6 +99,9 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
targetVersionColumn.setPrefWidth(180);
|
||||
setupCellValueFactory(targetVersionColumn, AddonUpdateObject::targetVersionProperty);
|
||||
|
||||
TableColumn<AddonUpdateObject, String> targetVersionTypeColumn = new TableColumn<>(i18n("addon.version_type"));
|
||||
setupCellValueFactory(targetVersionTypeColumn, AddonUpdateObject::targetVersionTypeProperty);
|
||||
|
||||
TableColumn<AddonUpdateObject, String> sourceColumn = new TableColumn<>(i18n("addon.check_update.source"));
|
||||
setupCellValueFactory(sourceColumn, AddonUpdateObject::sourceProperty);
|
||||
|
||||
@@ -115,12 +121,26 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
changelogColumn.setCellValueFactory(__ -> new SimpleStringProperty(i18n("button.view")));
|
||||
}
|
||||
|
||||
objects = FXCollections.observableList(updates.stream().map(AddonUpdateObject::new).collect(Collectors.toList()));
|
||||
FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new));
|
||||
var common = new Lazy<>(() -> updates.commonUpdates().stream().map(AddonUpdateObject::new).toList());
|
||||
var release = new Lazy<>(() -> updates.releaseUpdates().stream().map(AddonUpdateObject::new).toList());
|
||||
|
||||
objects = FXCollections.observableArrayList();
|
||||
|
||||
BooleanProperty showPreview = new SimpleBooleanProperty();
|
||||
{
|
||||
List<Subscription> subscriptions = new ArrayList<>();
|
||||
FXUtils.onChangeAndOperate(showPreview, preview -> {
|
||||
if (preview) objects.setAll(common.get());
|
||||
else objects.setAll(release.get());
|
||||
subscriptions.forEach(Subscription::unsubscribe);
|
||||
subscriptions.clear();
|
||||
subscriptions.addAll(FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(AddonUpdateObject::enabledProperty).toArray(BooleanProperty[]::new)));
|
||||
});
|
||||
}
|
||||
|
||||
TableView<AddonUpdateObject> table = new TableView<>(objects);
|
||||
table.setEditable(true);
|
||||
table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn, changelogColumn);
|
||||
table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, targetVersionTypeColumn, sourceColumn, changelogColumn);
|
||||
setMargin(table, new Insets(10, 10, 5, 10));
|
||||
|
||||
setCenter(table);
|
||||
@@ -129,6 +149,9 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
actions.setPadding(new Insets(8));
|
||||
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXCheckBox showPreviewCheck = new JFXCheckBox(i18n("addon.check_update.preview"));
|
||||
showPreviewCheck.selectedProperty().bindBidirectional(showPreview);
|
||||
|
||||
JFXButton exportListButton = FXUtils.newRaisedButton(i18n("button.export"));
|
||||
exportListButton.setOnAction(e -> exportList());
|
||||
|
||||
@@ -140,7 +163,7 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
onEscPressed(table, cancelButton::fire);
|
||||
|
||||
actions.getChildren().setAll(exportListButton, nextButton, cancelButton);
|
||||
actions.getChildren().setAll(showPreviewCheck, exportListButton, nextButton, cancelButton);
|
||||
setBottom(actions);
|
||||
}
|
||||
|
||||
@@ -150,7 +173,8 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
|
||||
private void updateFiles() {
|
||||
AddonUpdateTask task = new AddonUpdateTask(
|
||||
localAddonManager.getDirectory(),
|
||||
downloadProvider,
|
||||
localAddonDirectory,
|
||||
objects.stream()
|
||||
.filter(AddonUpdateObject::isEnabled)
|
||||
.map(AddonUpdateObject::getData)
|
||||
@@ -210,31 +234,30 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
}
|
||||
|
||||
private static final class AddonUpdateObject {
|
||||
final LocalAddonFile.AddonUpdate data;
|
||||
final AddonUpdate data;
|
||||
final BooleanProperty enabled = new SimpleBooleanProperty();
|
||||
final StringProperty fileName = new SimpleStringProperty();
|
||||
final StringProperty currentVersion = new SimpleStringProperty();
|
||||
final StringProperty targetVersion = new SimpleStringProperty();
|
||||
final StringProperty targetVersionType = new SimpleStringProperty();
|
||||
final StringProperty source = new SimpleStringProperty();
|
||||
String changelog = null;
|
||||
|
||||
public AddonUpdateObject(LocalAddonFile.AddonUpdate data) {
|
||||
public AddonUpdateObject(AddonUpdate data) {
|
||||
this.data = data;
|
||||
|
||||
enabled.set(!data.localAddonFile().isDisabled());
|
||||
fileName.set(data.localAddonFile().getFileName());
|
||||
currentVersion.set(data.currentVersion().version());
|
||||
targetVersion.set(data.targetVersion().version());
|
||||
switch (data.currentVersion().self().getSource()) {
|
||||
case CURSEFORGE:
|
||||
source.set(i18n("addon.curseforge"));
|
||||
break;
|
||||
case MODRINTH:
|
||||
source.set(i18n("addon.modrinth"));
|
||||
}
|
||||
targetVersionType.set(data.targetVersion().versionType().name());
|
||||
source.set(switch (data.currentVersion().source()) {
|
||||
case CURSEFORGE -> i18n("addon.curseforge");
|
||||
case MODRINTH -> i18n("addon.modrinth");
|
||||
});
|
||||
}
|
||||
|
||||
public LocalAddonFile.AddonUpdate getData() {
|
||||
public AddonUpdate getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -246,57 +269,26 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled.set(enabled);
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName.get();
|
||||
}
|
||||
|
||||
public StringProperty fileNameProperty() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName.set(fileName);
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return currentVersion.get();
|
||||
}
|
||||
|
||||
public StringProperty currentVersionProperty() {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
public void setCurrentVersion(String currentVersion) {
|
||||
this.currentVersion.set(currentVersion);
|
||||
}
|
||||
|
||||
public String getTargetVersion() {
|
||||
return targetVersion.get();
|
||||
}
|
||||
|
||||
public StringProperty targetVersionProperty() {
|
||||
return targetVersion;
|
||||
}
|
||||
|
||||
public void setTargetVersion(String targetVersion) {
|
||||
this.targetVersion.set(targetVersion);
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source.get();
|
||||
public StringProperty targetVersionTypeProperty() {
|
||||
return targetVersionType;
|
||||
}
|
||||
|
||||
public StringProperty sourceProperty() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source.set(source);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AddonChangelog extends JFXDialogLayout {
|
||||
@@ -342,13 +334,10 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
|
||||
private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) {
|
||||
spinnerPane.setLoading(true);
|
||||
RemoteAddonRepository repo = object.data.source().getRepository();
|
||||
RemoteAddonRepository repo = object.data.targetVersion().source().getRepository();
|
||||
Task.supplyAsync(() -> {
|
||||
if (object.changelog != null) {
|
||||
return object.changelog;
|
||||
}
|
||||
if (object.changelog != null) return object.changelog;
|
||||
RemoteAddon.Version version = object.data.targetVersion();
|
||||
if (repo == null) return null;
|
||||
return StringUtils.convertToHtml(
|
||||
repo.getAddonChangelog(DownloadProviders.getDownloadProvider(), version.projectId(), version.versionId()),
|
||||
"238222".equals(object.data.targetVersion().projectId())
|
||||
@@ -356,7 +345,7 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
object.changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty");
|
||||
scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, repo == null ? "" : repo.getBaseUrl()));
|
||||
scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, repo.getBaseUrl()));
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
spinnerPane.setFailedReason(null);
|
||||
} else {
|
||||
@@ -367,10 +356,9 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
}
|
||||
|
||||
private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) {
|
||||
Task.supplyAsync(() -> {
|
||||
RemoteAddonRepository repo = object.data.source().getRepository();
|
||||
return repo == null ? null : repo.getVersionPageUrl(object.data.targetVersion());
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
Task.supplyAsync(() ->
|
||||
object.data.targetVersion().source().getRepository().getVersionPageUrl(object.data.targetVersion())
|
||||
).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null && StringUtils.isNotBlank(result)) {
|
||||
button.setExternalLink(result);
|
||||
button.setDisable(false);
|
||||
@@ -380,84 +368,4 @@ public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane imple
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddonUpdateTask extends Task<Void> {
|
||||
private final Collection<Task<?>> dependents;
|
||||
private final List<LocalAddonFile> failedAddons = new ArrayList<>();
|
||||
|
||||
AddonUpdateTask(Path addonDirectory, List<LocalAddonFile.AddonUpdate> addons) {
|
||||
setStage("addon.check_update.confirm");
|
||||
getProperties().put("total", addons.size());
|
||||
|
||||
this.dependents = new ArrayList<>();
|
||||
for (LocalAddonFile.AddonUpdate addon : addons) {
|
||||
LocalAddonFile local = addon.localAddonFile();
|
||||
RemoteAddon.Version remote = addon.targetVersion();
|
||||
boolean isDisabled = local.isDisabled();
|
||||
String originalFileName = local.getFile().getFileName().toString();
|
||||
|
||||
dependents.add(Task
|
||||
.runAsync(Schedulers.javafx(), () -> local.setOld(true))
|
||||
.thenComposeAsync(() -> {
|
||||
String fileName = addon.useRemoteFileName() ? remote.file().filename() : originalFileName;
|
||||
if (isDisabled)
|
||||
fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION);
|
||||
|
||||
var task = new FileDownloadTask(
|
||||
remote.file().url(),
|
||||
addonDirectory.resolve(fileName)
|
||||
);
|
||||
|
||||
task.setName(remote.name());
|
||||
return task;
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
// restore state if failed
|
||||
local.setOld(false);
|
||||
if (isDisabled)
|
||||
local.markDisabled();
|
||||
failedAddons.add(local);
|
||||
} else if (!local.keepOldFiles()) {
|
||||
try {
|
||||
local.delete();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete outdated addon: " + local.getFile(), e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.withCounter("addon.check_update.confirm"));
|
||||
}
|
||||
}
|
||||
|
||||
public List<LocalAddonFile> getFailedAddons() {
|
||||
return failedAddons;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() {
|
||||
notifyPropertiesChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRelyingOnDependents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!isDependentsSucceeded())
|
||||
throw getException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,6 +301,22 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (getSkinnable().type == RemoteAddon.Type.MODPACK) {
|
||||
control.versions
|
||||
.keys()
|
||||
.stream()
|
||||
.max(Comparator.comparing(GameVersionNumber::asGameVersion))
|
||||
.flatMap(gameVersion ->
|
||||
control.versions.get(gameVersion)
|
||||
.stream()
|
||||
.max(Comparator.comparing(RemoteAddon.Version::datePublished))
|
||||
.map(v -> Pair.pair(gameVersion, v))
|
||||
).ifPresent(pair ->
|
||||
list.getContent().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("addon.download.recommend", pair.key())),
|
||||
new AddonItem(control.addon, pair.value(), control)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
final class Versions {
|
||||
@@ -361,15 +377,14 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
|
||||
private static final class DependencyAddonItem extends LineButton {
|
||||
public static final EnumMap<RemoteAddon.DependencyType, String> I18N_KEY = new EnumMap<>(Lang.mapOf(
|
||||
Pair.pair(RemoteAddon.DependencyType.EMBEDDED, "addon.dependency.embedded"),
|
||||
Pair.pair(RemoteAddon.DependencyType.OPTIONAL, "addon.dependency.optional"),
|
||||
Pair.pair(RemoteAddon.DependencyType.REQUIRED, "addon.dependency.required"),
|
||||
Pair.pair(RemoteAddon.DependencyType.TOOL, "addon.dependency.tool"),
|
||||
Pair.pair(RemoteAddon.DependencyType.INCLUDE, "addon.dependency.include"),
|
||||
Pair.pair(RemoteAddon.DependencyType.INCOMPATIBLE, "addon.dependency.incompatible"),
|
||||
Pair.pair(RemoteAddon.DependencyType.BROKEN, "addon.dependency.broken")
|
||||
));
|
||||
public static String translateDependencyType(RemoteAddon.DependencyType dependencyType) {
|
||||
return switch (dependencyType) {
|
||||
case EMBEDDED -> i18n("addon.dependency.embedded");
|
||||
case OPTIONAL -> i18n("addon.dependency.optional");
|
||||
case REQUIRED -> i18n("addon.dependency.required");
|
||||
default -> dependencyType.name().toLowerCase(Locale.ROOT);
|
||||
};
|
||||
}
|
||||
|
||||
public final RemoteAddon addon;
|
||||
|
||||
@@ -582,7 +597,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
|
||||
if (!dependencies.containsKey(dependency.getType())) {
|
||||
Label title = new Label(i18n(DependencyAddonItem.I18N_KEY.get(dependency.getType())));
|
||||
Label title = new Label(DependencyAddonItem.translateDependencyType(dependency.getType()));
|
||||
title.setPadding(new Insets(0, 8, 0, 8));
|
||||
List<DependencyAddonItem> list = new ArrayList<>();
|
||||
dependencies.put(dependency.getType(), Pair.pair(title, list));
|
||||
|
||||
@@ -60,6 +60,7 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
|
||||
private final TabHeader.Tab<SchematicsPage> schematicsTab = new TabHeader.Tab<>("schematicsTab");
|
||||
private final TabHeader.Tab<ResourcePackListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
|
||||
private final TabHeader.Tab<ScreenshotsPage> screenshotsTab = new TabHeader.Tab<>("screenshotsTab");
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
private final BooleanProperty currentInstanceUpgradable = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<HMCLGameInstance.@Nullable Optional> instance =
|
||||
@@ -92,8 +93,9 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
resourcePackTab.setNodeSupplier(() -> new ResourcePackListPage(instance));
|
||||
worldListTab.setNodeSupplier(() -> new WorldListPage(instance));
|
||||
schematicsTab.setNodeSupplier(() -> new SchematicsPage(instance));
|
||||
screenshotsTab.setNodeSupplier(() -> new ScreenshotsPage(instance));
|
||||
|
||||
tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab);
|
||||
tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab, screenshotsTab);
|
||||
tab.select(gameSettingsTab);
|
||||
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
@@ -363,13 +365,15 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
JFXPopup browsePopup = new JFXPopup(browseList);
|
||||
browseList.getContent().setAll(
|
||||
new IconedMenuItem(SVG.STADIA_CONTROLLER, i18n("folder.game"), () -> control.onBrowse(""), browsePopup),
|
||||
new IconedMenuItem(SVG.SETTINGS, i18n("folder.config"), () -> control.onBrowse("config"), browsePopup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.EXTENSION, i18n("folder.mod"), () -> control.onBrowse("mods"), browsePopup),
|
||||
new IconedMenuItem(SVG.TEXTURE, i18n("folder.resourcepacks"), () -> control.onBrowse("resourcepacks"), browsePopup),
|
||||
new IconedMenuItem(SVG.PUBLIC, i18n("folder.saves"), () -> control.onBrowse("saves"), browsePopup),
|
||||
new IconedMenuItem(SVG.SCHEMA, i18n("folder.schematics"), control::onBrowseSchematics, 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 MenuSeparator(),
|
||||
new IconedMenuItem(SVG.SCRIPT, i18n("folder.logs"), () -> control.onBrowse("logs"), browsePopup),
|
||||
new IconedMenuItem(SVG.FRAME_BUG, i18n("folder.crash-reports"), () -> control.onBrowse("crash-reports"), browsePopup)
|
||||
);
|
||||
@@ -385,6 +389,10 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
new IconedMenuItem(SVG.DELETE, i18n("instance.manage.remove"), control::remove, managementPopup),
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), control::export, managementPopup),
|
||||
new MenuSeparator(),
|
||||
// Due to the sidebar height limit, we temporarily place the entries for these tabs
|
||||
// in the management menu; they will be moved back to the sidebar after a future UI refactor.
|
||||
new IconedMenuItem(SVG.SCREENSHOT_MONITOR, i18n("screenshots.manage"), () -> control.tab.select(control.screenshotsTab), managementPopup),
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(null, i18n("instance.manage.redownload_assets_index"), control::redownloadAssetIndex, managementPopup),
|
||||
new IconedMenuItem(null, i18n("instance.manage.remove_assets"), control::clearAssets, managementPopup),
|
||||
new IconedMenuItem(null, i18n("instance.manage.remove_libraries"), control::clearLibraries, managementPopup),
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.update.AddonCheckUpdatesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
@@ -251,19 +252,22 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
GameVersionNumber version = gameInstance.getVersion();
|
||||
return version != GameVersionNumber.unknown()
|
||||
? new AddonCheckUpdatesTask<>(
|
||||
DownloadProviders.getDownloadProvider(), version.toString(), mods)
|
||||
: null;
|
||||
if (version != GameVersionNumber.unknown()) {
|
||||
return new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), version.toString(), mods);
|
||||
} else {
|
||||
LOG.warning("Failed to check for updates, due to unable to get instance game version");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception instanceof CancellationException) return;
|
||||
if (exception != null || result == null) {
|
||||
Controllers.dialog(i18n("addon.check_update.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
} else if (result.isEmpty()) {
|
||||
if (exception != null) LOG.warning("Failed to check for updates", exception);
|
||||
} else if (result.commonUpdates().isEmpty()) {
|
||||
Controllers.dialog(i18n("addon.check_update.empty"));
|
||||
} else {
|
||||
Controllers.navigateForward(new AddonUpdatesPage<>(modManager, result));
|
||||
Controllers.navigateForward(new AddonUpdatesPage(DownloadProviders.getDownloadProvider(), modManager.getDirectory(), result));
|
||||
}
|
||||
})
|
||||
.withStagesHints("update.checking"),
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile;
|
||||
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager;
|
||||
import org.jackhuang.hmcl.addon.update.AddonCheckUpdatesTask;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
@@ -261,18 +262,20 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
GameVersionNumber version = gameInstance.getVersion();
|
||||
return version != GameVersionNumber.unknown()
|
||||
? new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(),
|
||||
version.toString(), resourcePacks)
|
||||
: null;
|
||||
if (version != GameVersionNumber.unknown()) {
|
||||
return new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), version.toString(), resourcePacks);
|
||||
} else {
|
||||
LOG.warning("Failed to check for updates, due to unable to get instance game version");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null || result == null) {
|
||||
Controllers.dialog(i18n("addon.check_update.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
} else if (result.isEmpty()) {
|
||||
} else if (result.commonUpdates().isEmpty()) {
|
||||
Controllers.dialog(i18n("addon.check_update.empty"));
|
||||
} else {
|
||||
Controllers.navigateForward(new AddonUpdatesPage<>(resourcePackManager, result));
|
||||
Controllers.navigateForward(new AddonUpdatesPage(DownloadProviders.getDownloadProvider(), resourcePackManager.getDirectory(), result));
|
||||
}
|
||||
})
|
||||
.withStagesHints("update.checking"),
|
||||
@@ -483,7 +486,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
LocalAddonFile.Description description = resourcePack.getDescription();
|
||||
Stream<String> descriptionParts = description == null
|
||||
? Stream.empty()
|
||||
: description.getParts().stream().map(LocalAddonFile.Description.Part::getText);
|
||||
: description.parts().stream().map(LocalAddonFile.Description.Part::text);
|
||||
if (predicate.test(resourcePack.getFileNameWithExtension())
|
||||
|| predicate.test(resourcePack.getFileName())
|
||||
|| descriptionParts.anyMatch(predicate)) {
|
||||
|
||||
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* 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.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.BackgroundType;
|
||||
import org.jackhuang.hmcl.setting.LauncherSettings;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// Lists screenshots of the current game instance and provides preview and file actions.
|
||||
public final class ScreenshotsPage extends ListPageBase<ScreenshotsPage.Screenshot> {
|
||||
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
|
||||
private @Nullable Path screenshotsDir;
|
||||
|
||||
public ScreenshotsPage(ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
|
||||
Objects.requireNonNull(instanceContext, "instanceContext");
|
||||
|
||||
listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> {
|
||||
if (current != null) {
|
||||
loadInstance(current);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new ScreenshotsPageSkin(this);
|
||||
}
|
||||
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
HMCLGameInstance gameInstance = instance.instance();
|
||||
if (gameInstance == null) return;
|
||||
screenshotsDir = gameInstance.getRunDirectory().resolve("screenshots");
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (screenshotsDir == null) return;
|
||||
setLoading(true);
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
if (screenshotsDir == null) return List.<Screenshot>of();
|
||||
try (Stream<Path> stream = Files.list(screenshotsDir)) {
|
||||
return stream.map(Screenshot::fromFile).filter(Objects::nonNull).sorted(Comparator.reverseOrder()).toList();
|
||||
}
|
||||
}).whenComplete(Schedulers.javafx(), (list, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to load screenshots in: " + screenshotsDir, exception);
|
||||
getItems().clear();
|
||||
} else {
|
||||
getItems().setAll(list);
|
||||
}
|
||||
setLoading(false);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void revealFolder() {
|
||||
if (screenshotsDir != null) FXUtils.openFolder(screenshotsDir);
|
||||
}
|
||||
|
||||
private void deleteAt(Path path) {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete screenshot: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void delete(Screenshot screenshot) {
|
||||
deleteAt(screenshot.getPath());
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void delete(Collection<Screenshot> screenshots) {
|
||||
screenshots.stream().map(Screenshot::getPath).forEach(this::deleteAt);
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
if (screenshotsDir == null) return;
|
||||
try (var stream = Files.list(screenshotsDir)) {
|
||||
stream.filter(Screenshot::isFileScreenshot).forEach(this::deleteAt);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to clear screenshots at: " + screenshotsDir, e);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void selectDate(JFXListView<Screenshot> listView) {
|
||||
FXUtils.chooseDateRange(pair -> {
|
||||
Instant from = pair.key() != null ? pair.key().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant() : Instant.MIN;
|
||||
Instant to = pair.value() != null ? pair.value().plusDays(1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant() : Instant.MAX;
|
||||
if (from.compareTo(to) >= 0) return;
|
||||
List<Screenshot> items = listView.getItems();
|
||||
int start = -1, end = -1;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
// Reversed order
|
||||
var creationTime = items.get(i).getCreationTime();
|
||||
if ((creationTime == null || creationTime.compareTo(from) < 0) && end < 0) end = i;
|
||||
if ((creationTime == null || creationTime.compareTo(to) < 0) && start < 0) start = i;
|
||||
}
|
||||
if (start < 0) return;
|
||||
if (end < 0) end = items.size();
|
||||
listView.getSelectionModel().selectRange(start, end);
|
||||
listView.scrollTo(start);
|
||||
});
|
||||
}
|
||||
|
||||
/// Describes a screenshot file and loads its preview images on demand.
|
||||
public static final class Screenshot implements Comparable<Screenshot> {
|
||||
private final Path path;
|
||||
private final String fileName;
|
||||
private final BasicFileAttributes attributes;
|
||||
|
||||
/// Caches asynchronous thumbnail loads with a soft reference.
|
||||
private final ItemPropertyAsyncCache<@Nullable Image, Screenshot> thumbnailCache =
|
||||
new ItemPropertyAsyncCache.Soft<>(this, this::loadThumbnail, null);
|
||||
|
||||
/// Retains the full image after a successful load, or is null before one succeeds.
|
||||
private @Nullable Image fullImage;
|
||||
|
||||
public static boolean isFileScreenshot(Path path) {
|
||||
return Files.isRegularFile(path) && "png".equalsIgnoreCase(FileUtils.getExtension(path));
|
||||
}
|
||||
|
||||
public static Screenshot fromFile(Path path) {
|
||||
if (!isFileScreenshot(path)) return null;
|
||||
BasicFileAttributes attr = null;
|
||||
try {
|
||||
attr = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read screenshot attributes at: " + path, e);
|
||||
}
|
||||
return new Screenshot(path, FileUtils.getName(path), attr);
|
||||
}
|
||||
|
||||
public Screenshot(Path path, String fileName, BasicFileAttributes attributes) {
|
||||
this.path = path;
|
||||
this.fileName = fileName;
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public BasicFileAttributes getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public Instant getCreationTime() {
|
||||
if (attributes == null) return null;
|
||||
return attributes.creationTime().toInstant();
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
if (attributes == null) return -1;
|
||||
return attributes.size();
|
||||
}
|
||||
|
||||
public boolean isFullImageLoaded() {
|
||||
return fullImage != null;
|
||||
}
|
||||
|
||||
/// Loads an aspect-ratio-preserving thumbnail bounded by 72 by 72 pixels.
|
||||
///
|
||||
/// @return the thumbnail, or null if loading throws an exception
|
||||
@Nullable
|
||||
private Image loadThumbnail() {
|
||||
try {
|
||||
return FXUtils.loadImage(path, 72, 72, true, false);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load screenshot thumbnail at: " + path, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Image getFullImage() {
|
||||
if (fullImage == null) {
|
||||
try {
|
||||
fullImage = FXUtils.loadImage(path);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load screenshot content at: " + path, e);
|
||||
}
|
||||
}
|
||||
return fullImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Screenshot that && Objects.equals(this.path, that.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull ScreenshotsPage.Screenshot o) {
|
||||
return this.path.compareTo(o.path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Displays a screenshot thumbnail, metadata, and file actions in a reusable cell.
|
||||
private static final class ScreenshotCell extends MDListCell<Screenshot> {
|
||||
|
||||
/// Holds the thumbnail or the placeholder icon when no thumbnail is available.
|
||||
private final StackPane imagePane = new StackPane();
|
||||
/// Receives the cached thumbnail for the current item.
|
||||
private final ObjectProperty<@Nullable Image> thumbnail = new SimpleObjectProperty<>();
|
||||
/// Displays the screenshot file name and creation time.
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
|
||||
/// Creates a cell whose file actions operate on the supplied page.
|
||||
public ScreenshotCell(JFXListView<Screenshot> listView, ScreenshotsPage page) {
|
||||
super(listView);
|
||||
|
||||
setSelectable();
|
||||
|
||||
FXUtils.onChangeAndOperate(thumbnail, image -> imagePane.getChildren().setAll(
|
||||
image == null ? SVG.SCREENSHOT_MONITOR.createIcon(36) : new ImageContainer(image, 36, 36)));
|
||||
|
||||
HBox container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
content.setMouseTransparent(true);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
|
||||
JFXButton copyButton = FXUtils.newToggleButton4(SVG.CONTENT_COPY);
|
||||
copyButton.setOnAction(e -> {
|
||||
@Nullable Screenshot screenshot = getItem();
|
||||
if (screenshot != null) {
|
||||
@Nullable Image image = screenshot.getFullImage();
|
||||
if (image != null) FXUtils.copyImage(image);
|
||||
}
|
||||
});
|
||||
|
||||
JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER);
|
||||
revealButton.setOnAction(e -> {
|
||||
@Nullable Screenshot screenshot = getItem();
|
||||
if (screenshot != null) {
|
||||
FXUtils.showFileInExplorer(screenshot.getPath());
|
||||
}
|
||||
});
|
||||
|
||||
JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
|
||||
infoButton.setOnAction(e -> {
|
||||
@Nullable Screenshot screenshot = getItem();
|
||||
if (screenshot != null) {
|
||||
Controllers.dialog(new ScreenshotDialog(screenshot));
|
||||
}
|
||||
});
|
||||
FXUtils.onDoubleClicked(this, infoButton::fire);
|
||||
|
||||
JFXButton deleteButton = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
|
||||
deleteButton.setOnAction(e -> {
|
||||
@Nullable Screenshot screenshot = getItem();
|
||||
if (screenshot != null) {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
|
||||
() -> page.delete(screenshot), null);
|
||||
}
|
||||
});
|
||||
|
||||
container.getChildren().setAll(imagePane, content, copyButton, revealButton, infoButton, deleteButton);
|
||||
|
||||
StackPane.setMargin(container, new Insets(8));
|
||||
getContainer().getChildren().setAll(container);
|
||||
}
|
||||
|
||||
/// Attaches the current item's cached thumbnail and updates its metadata.
|
||||
/// Clears the displayed thumbnail when the cell becomes empty.
|
||||
@Override
|
||||
protected void updateControl(@Nullable Screenshot item, boolean empty) {
|
||||
if (item == null || empty) {
|
||||
thumbnail.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
item.thumbnailCache.attachValue(thumbnail, new WeakReference<>(itemProperty()));
|
||||
|
||||
content.setTitle(item.getFileName());
|
||||
@Nullable Instant creationTime = item.getCreationTime();
|
||||
if (creationTime != null) content.setSubtitle(I18n.formatDateTime(creationTime));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ScreenshotDialog extends JFXDialogLayout {
|
||||
|
||||
public ScreenshotDialog(Screenshot screenshot) {
|
||||
TwoLineListItem head = new TwoLineListItem();
|
||||
head.setTitle(screenshot.getFileName());
|
||||
setHeading(head);
|
||||
|
||||
var image = screenshot.getFullImage();
|
||||
if (image == null) {
|
||||
setBody(SVG.SCREENSHOT_MONITOR.createIcon(360));
|
||||
if (screenshot.getAttributes() != null)
|
||||
head.setSubtitle(I18n.formatDateTime(screenshot.getCreationTime()) + " " + FileUtils.parseFileSize(screenshot.getFileSize()));
|
||||
} else {
|
||||
setBody(new ImageContainer(image, Math.min(Controllers.getDecorator().contentWidthProperty().get() * 0.8, image.getWidth()), Controllers.getDecorator().contentHeightProperty().get() * 0.57));
|
||||
if (screenshot.getAttributes() != null)
|
||||
head.setSubtitle(
|
||||
I18n.formatDateTime(screenshot.getCreationTime())
|
||||
+ " " + FileUtils.parseFileSize(screenshot.getFileSize())
|
||||
+ " " + (int) image.getWidth() + " * " + (int) image.getHeight()
|
||||
);
|
||||
else head.setSubtitle((int) image.getWidth() + " * " + (int) image.getHeight());
|
||||
}
|
||||
|
||||
JFXButton setAsBgButton = new JFXButton(i18n("screenshots.set_as_bg"));
|
||||
setAsBgButton.getStyleClass().add("dialog-accept");
|
||||
setAsBgButton.setOnAction(e -> {
|
||||
SettingsManager.settings().getThemeAppearanceOverrides().add(LauncherSettings.THEME_APPEARANCE_BACKGROUND);
|
||||
SettingsManager.settings().backgroundTypeProperty().set(BackgroundType.CUSTOM);
|
||||
SettingsManager.settings().customBackgroundImagePathProperty().set(screenshot.getPath().toString());
|
||||
});
|
||||
|
||||
JFXButton copyButton = new JFXButton(i18n("menu.copy"));
|
||||
copyButton.getStyleClass().add("dialog-accept");
|
||||
copyButton.setDisable(image == null);
|
||||
copyButton.setOnAction(e -> {
|
||||
if (image != null) FXUtils.copyImage(image);
|
||||
});
|
||||
|
||||
JFXButton okButton = new JFXButton(i18n("button.ok"));
|
||||
okButton.getStyleClass().add("dialog-accept");
|
||||
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
FXUtils.onEscPressed(this, okButton::fire);
|
||||
setActions(setAsBgButton, copyButton, okButton);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ScreenshotsPageSkin extends SkinBase<ScreenshotsPage> {
|
||||
|
||||
private final TransitionPane toolbarPane;
|
||||
private final HBox toolbarNormal;
|
||||
private final HBox toolbarSelecting;
|
||||
|
||||
private final JFXListView<Screenshot> listView;
|
||||
|
||||
public ScreenshotsPageSkin(ScreenshotsPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
StackPane pane = new StackPane();
|
||||
pane.setPadding(new Insets(10));
|
||||
pane.getStyleClass().addAll("notice-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
|
||||
toolbarNormal = new HBox();
|
||||
toolbarSelecting = new HBox();
|
||||
|
||||
// Toolbar Normal
|
||||
toolbarNormal.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::revealFolder),
|
||||
createToolbarButton2(i18n("button.clear"), SVG.DELETE_FOREVER, () -> {
|
||||
Controllers.confirm(i18n("screenshots.clear.confirm"), i18n("button.clear"), skinnable::clear, null);
|
||||
}),
|
||||
createToolbarButton2(i18n("button.select_date"), SVG.DATE_RANGE, () -> skinnable.selectDate(listView))
|
||||
);
|
||||
|
||||
// Toolbar Selecting
|
||||
toolbarSelecting.getChildren().setAll(
|
||||
createToolbarButton2(i18n("menu.copy"), SVG.FOLDER_COPY, () -> {
|
||||
FXUtils.copyFiles(listView.getSelectionModel().getSelectedItems().stream().map(Screenshot::getPath).toList());
|
||||
}),
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () ->
|
||||
skinnable.delete(listView.getSelectionModel().getSelectedItems()), null);
|
||||
}),
|
||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||
listView.getSelectionModel().selectAll()),
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
listView.getSelectionModel().clearSelection())
|
||||
);
|
||||
|
||||
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
|
||||
selectedItem -> {
|
||||
if (selectedItem == null)
|
||||
changeToolbar(toolbarNormal);
|
||||
else
|
||||
changeToolbar(toolbarSelecting);
|
||||
});
|
||||
root.getContent().add(toolbarPane);
|
||||
|
||||
// Clear selection when pressing ESC
|
||||
root.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.ESCAPE) {
|
||||
if (listView.getSelectionModel().getSelectedItem() != null) {
|
||||
listView.getSelectionModel().clearSelection();
|
||||
e.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ScreenshotCell(listView, skinnable));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
|
||||
listView.setOnContextMenuRequested(event -> {
|
||||
Screenshot selectedItem = listView.getSelectionModel().getSelectedItem();
|
||||
if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) {
|
||||
listView.getSelectionModel().clearSelection();
|
||||
Controllers.dialog(new ScreenshotDialog(selectedItem));
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
}
|
||||
|
||||
pane.getChildren().setAll(root);
|
||||
getChildren().setAll(pane);
|
||||
}
|
||||
|
||||
private void changeToolbar(HBox newToolbar) {
|
||||
if (newToolbar != toolbarPane.getCurrentNode()) toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -65,8 +65,8 @@ public class DownloadSettingsPage extends StackPane {
|
||||
getChildren().setAll(scrollPane);
|
||||
|
||||
{
|
||||
var downloadSource = new ComponentList();
|
||||
downloadSource.getStyleClass().add("card-non-transparent");
|
||||
var gameContent = new ComponentList();
|
||||
gameContent.getStyleClass().add("card-non-transparent");
|
||||
{
|
||||
Function<DownloadSource, String> converter = source -> switch (source) {
|
||||
case DEFAULT -> i18n("settings.launcher.download_source.auto");
|
||||
@@ -102,10 +102,10 @@ public class DownloadSettingsPage extends StackPane {
|
||||
defaultAddonSourcePane.setItems("modrinth", "curseforge");
|
||||
defaultAddonSourcePane.valueProperty().bindBidirectional(settings().defaultAddonSourceProperty());
|
||||
|
||||
downloadSource.getContent().setAll(versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
|
||||
gameContent.getContent().setAll(versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
|
||||
}
|
||||
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.game_content")), gameContent);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -231,9 +231,11 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
|
||||
this.remoteVersionBox = new JFXComboBox<>();
|
||||
this.remoteVersionBox.setConverter(FXUtils.stringConverter(JavaRemoteVersion::getDistributionVersion));
|
||||
this.remoteVersionBox.disableProperty().bind(Bindings.isNull(distributionBox.getSelectionModel().selectedItemProperty()));
|
||||
|
||||
this.packageTypeBox = new JFXComboBox<>(FXCollections.observableArrayList());
|
||||
this.packageTypeBox.setConverter(FXUtils.stringConverter(JavaPackageType::getDisplayName));
|
||||
this.packageTypeBox.disableProperty().bind(Bindings.isNull(distributionBox.getSelectionModel().selectedItemProperty()));
|
||||
|
||||
this.downloadButton = new JFXButton(i18n("download"));
|
||||
downloadButton.setOnAction(e -> onDownload());
|
||||
@@ -313,8 +315,6 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
Consumer<DiscoJavaVersionList> updateListStatus = list -> {
|
||||
remoteVersionBox.setItems(null);
|
||||
packageTypeBox.getItems().clear();
|
||||
remoteVersionBox.setDisable(true);
|
||||
packageTypeBox.setDisable(true);
|
||||
warningLabel.setText(null);
|
||||
|
||||
if (list == null || (list.versions != null && list.versions.isEmpty()))
|
||||
@@ -327,9 +327,6 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
if (list.status == DiscoJavaVersionList.Status.SUCCESS) {
|
||||
packageTypeBox.getItems().setAll(list.versions.keySet());
|
||||
packageTypeBox.getSelectionModel().selectFirst();
|
||||
|
||||
remoteVersionBox.setDisable(false);
|
||||
packageTypeBox.setDisable(false);
|
||||
} else
|
||||
warningLabel.setText(i18n("java.download.load_list.failed"));
|
||||
}
|
||||
|
||||
@@ -182,15 +182,23 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
|
||||
@FXThread
|
||||
private void loadJava(Collection<JavaRuntime> javaRuntimes) {
|
||||
if (javaRuntimes != null) {
|
||||
// JavaRuntime equality is path-based. setAll can therefore retain a stale item when the
|
||||
// Java installation at an existing path has been upgraded.
|
||||
this.getItems().clear();
|
||||
this.getItems().addAll(javaRuntimes);
|
||||
this.setLoading(false);
|
||||
} else {
|
||||
if (javaRuntimes == null) {
|
||||
this.setLoading(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// JavaRuntime equality is path-based. setAll can therefore retain a stale item when the
|
||||
// Java installation at an existing path has been upgraded.
|
||||
this.getItems().clear();
|
||||
this.getItems().addAll(javaRuntimes);
|
||||
|
||||
if (javaRuntimes.isEmpty()) {
|
||||
this.setFailedReason(i18n("java.empty"));
|
||||
} else {
|
||||
this.setFailedReason(null);
|
||||
}
|
||||
|
||||
this.setLoading(false);
|
||||
}
|
||||
|
||||
private static final class JavaPageSkin extends ToolbarListPageSkin<JavaRuntime, JavaManagementPage> {
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
|
||||
@@ -80,6 +81,10 @@ public final class JavaRestorePage extends ListPageBase<JavaRestorePage.Disabled
|
||||
return a.path.compareTo(b.path);
|
||||
});
|
||||
this.setItems(FXCollections.observableList(result));
|
||||
|
||||
if (result.isEmpty()) {
|
||||
fireEvent(new PageCloseEvent());
|
||||
}
|
||||
};
|
||||
disabledJava.addListener(new WeakInvalidationListener(listener));
|
||||
listener.invalidated(disabledJava);
|
||||
|
||||
@@ -2009,3 +2009,82 @@
|
||||
.addon-changelog-table-cell {
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Date Picker *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
.jfx-date-picker {
|
||||
-jfx-default-color: -monet-primary;
|
||||
}
|
||||
|
||||
.date-picker-popup {
|
||||
-fx-padding: 5;
|
||||
-fx-background-color: -monet-surface-container-high;
|
||||
-fx-background-radius: 4;
|
||||
}
|
||||
|
||||
.date-picker-popup .spinner .spinner-label {
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.date-picker-popup .month-year-pane,
|
||||
.date-picker-popup .content {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.date-picker-popup .separator .line {
|
||||
-fx-border-width: 1;
|
||||
-fx-border-color: -monet-outline-variant;
|
||||
}
|
||||
|
||||
.date-picker-popup .content .spinner-label,
|
||||
.date-picker-popup .data-picker-list-cell .spinner-label {
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.date-picker-popup .data-picker-list-cell:selected {
|
||||
-fx-background-color: -monet-secondary-container;
|
||||
}
|
||||
|
||||
.date-picker-popup .date-cell {
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.date-picker-popup .day-cell {
|
||||
-fx-border-radius: 20;
|
||||
-fx-background-radius: 20;
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.date-picker-popup .day-cell.today {
|
||||
-fx-border-color: -monet-primary;
|
||||
-fx-text-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.date-picker-popup .day-cell.previous-month,
|
||||
.date-picker-popup .day-cell.next-month {
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.date-picker-popup .day-cell.hovered {
|
||||
-fx-background-color: -monet-surface-container-highest;
|
||||
}
|
||||
|
||||
.date-picker-popup .day-cell.selected,
|
||||
.date-picker-popup .day-cell.selected.hovered{
|
||||
-fx-background-color: -monet-primary;
|
||||
-fx-text-fill: -monet-on-primary;
|
||||
}
|
||||
|
||||
.date-picker-popup .left-button .jfx-svg-glyph,
|
||||
.date-picker-popup .right-button .jfx-svg-glyph {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.date-picker-popup .title {
|
||||
-fx-text-fill: -monet-on-surface-variant;
|
||||
}
|
||||
@@ -175,16 +175,14 @@ addon.check_update.empty=All files are up-to-date
|
||||
addon.check_update.failed_check=Failed to check for updates.
|
||||
addon.check_update.failed_download=Failed to download some files.
|
||||
addon.check_update.file=File
|
||||
addon.check_update.preview=Show preview versions
|
||||
addon.check_update.source=Source
|
||||
addon.check_update.target_version=Target Version
|
||||
addon.curseforge=CurseForge
|
||||
addon.dependency.embedded=Built-in Dependencies (Already packaged in the addon file by the author. No need to download separately)
|
||||
addon.dependency.optional=Optional Dependencies (If missing, the game will run normally, but the addon features may be missing)
|
||||
addon.dependency.required=Required Dependencies (Must be downloaded separately. Missing may prevent the game from launching)
|
||||
addon.dependency.tool=Required Dependencies (Must be downloaded separately. Missing may prevent the game from launching)
|
||||
addon.dependency.include=Built-in Dependencies (Already packaged in the addon file by the author. No need to download separately)
|
||||
addon.dependency.incompatible=Incompatible Addons (Installing these addons at the same time will prevent the game from launching)
|
||||
addon.dependency.broken=Broken Dependencies (This addon existed before, but it does not exist anymore. Try using another download source.)
|
||||
addon.dependencies.has_broken=This version has broken dependencies. Please try another download source, or installation could be unsuccessful.
|
||||
addon.download.recommend=Recommended Version for Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
@@ -195,6 +193,7 @@ addon.sort.last_updated=Last Updated
|
||||
addon.sort.popularity=Popularity
|
||||
addon.sort.relevancy=Relevancy
|
||||
addon.sort.total_downloads=Total Downloads
|
||||
addon.version_type=Type
|
||||
|
||||
archive.author=Author(s)
|
||||
archive.date=Publish Date
|
||||
@@ -228,6 +227,9 @@ button.remove.confirm=Are you sure you want to permanently remove it? This actio
|
||||
button.save=Save
|
||||
button.save_as=Save As
|
||||
button.select_all=Select All
|
||||
button.select_date=Select by date
|
||||
button.select_date.from=From
|
||||
button.select_date.to=To
|
||||
button.view=View
|
||||
button.yes=Yes
|
||||
|
||||
@@ -857,6 +859,7 @@ java.download.prompt=Please choose the Java version you want to download:
|
||||
java.download.distribution=Distribution
|
||||
java.download.version=Version
|
||||
java.download.packageType=Package Type
|
||||
java.empty=Could not find Java
|
||||
java.management=Java Management
|
||||
java.info.architecture=Architecture
|
||||
java.info.vendor=Vendor
|
||||
@@ -1358,6 +1361,10 @@ schematics.install_mod=Install Litematica Mod
|
||||
schematics.manage=Schematics
|
||||
schematics.sub_items=%d sub-item(s)
|
||||
|
||||
screenshots.clear.confirm=Are you sure you want to permanently remove the screenshots? This action cannot be undone!
|
||||
screenshots.manage=Screenshots
|
||||
screenshots.set_as_bg=Set as Launcher Background
|
||||
|
||||
search=Search
|
||||
search.hint.chinese=Search in English and Chinese
|
||||
search.hint.english=Search in English only
|
||||
@@ -1567,6 +1574,7 @@ settings.launcher.font.anti_aliasing.auto=Auto
|
||||
settings.launcher.font.anti_aliasing.gray=Grayscale
|
||||
settings.launcher.font.anti_aliasing.lcd=Sub-pixel
|
||||
settings.launcher.fonts=Fonts
|
||||
settings.launcher.game_content=Game Content
|
||||
settings.launcher.general=General
|
||||
settings.launcher.language=Language
|
||||
settings.launcher.launcher_log.export=Export Launcher Logs
|
||||
@@ -1775,6 +1783,7 @@ unofficial.hint=You are using an unofficial build of HMCL. We cannot guarantee i
|
||||
update=Update
|
||||
update.accept=Update
|
||||
update.changelog=Changelog
|
||||
update.changelog.empty=Changelog has not been uploaded yet
|
||||
update.channel.dev=Beta
|
||||
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Stable channel builds.\n\
|
||||
\n\
|
||||
|
||||
@@ -176,16 +176,14 @@ addon.check_update.empty=جميع المودات محدّثة
|
||||
addon.check_update.failed_check=فشل التحقق من التحديثات.
|
||||
addon.check_update.failed_download=فشل تنزيل بعض الملفات.
|
||||
addon.check_update.file=الملف
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=المصدر
|
||||
addon.check_update.target_version=الإصدار المستهدف
|
||||
addon.curseforge=CurseForge
|
||||
# addon.dependency.embedded=
|
||||
# addon.dependency.optional=
|
||||
# addon.dependency.required=
|
||||
# addon.dependency.tool=
|
||||
# addon.dependency.include=
|
||||
# addon.dependency.incompatible=
|
||||
# addon.dependency.broken=
|
||||
# addon.dependencies.has_broken=
|
||||
# addon.download.recommend=
|
||||
# addon.download.title.release=
|
||||
@@ -196,6 +194,7 @@ addon.sort.last_updated=آخر تحديث
|
||||
addon.sort.popularity=الشعبية
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=إجمالي التنزيلات
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=المؤلف/المؤلفون
|
||||
archive.date=تاريخ النشر
|
||||
@@ -229,6 +228,9 @@ button.remove.confirm=هل أنت متأكد من الإزالة النهائي
|
||||
button.save=حفظ
|
||||
button.save_as=حفظ باسم
|
||||
button.select_all=تحديد الكل
|
||||
# button.select_date=
|
||||
# button.select_date.from=
|
||||
# button.select_date.to=
|
||||
button.view=عرض
|
||||
button.yes=نعم
|
||||
|
||||
@@ -1358,6 +1360,10 @@ schematics.info.version=إصدار المخطط
|
||||
schematics.manage=المخططات
|
||||
schematics.sub_items=%d عنصر فرعي
|
||||
|
||||
# screenshots.clear.confirm=
|
||||
# screenshots.manage=
|
||||
# screenshots.set_as_bg=
|
||||
|
||||
search=بحث
|
||||
search.hint.chinese=البحث بالإنجليزية والصينية
|
||||
search.hint.english=البحث بالإنجليزية فقط
|
||||
@@ -1565,6 +1571,7 @@ settings.launcher.font.anti_aliasing.auto=تلقائي
|
||||
settings.launcher.font.anti_aliasing.gray=تدرج رمادي
|
||||
settings.launcher.font.anti_aliasing.lcd=بكسل فرعي
|
||||
# settings.launcher.fonts=
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=عام
|
||||
settings.launcher.language=اللغة
|
||||
settings.launcher.launcher_log.export=تصدير سجلات المشغّل
|
||||
@@ -1772,6 +1779,7 @@ unofficial.hint=أنت تستخدم إصداراً غير رسمي من HMCL. ل
|
||||
update=تحديث
|
||||
update.accept=تحديث
|
||||
update.changelog=سجل التغييرات
|
||||
# update.changelog.empty=
|
||||
update.channel.dev=تجريبي
|
||||
update.channel.dev.hint=أنت تستخدم حالياً إصدار القناة التجريبية من المشغّل. على الرغم من أنه قد يتضمن ميزات إضافية، إلا أنه أقل استقراراً أحياناً من إصدارات قناة الإصدار الرسمي.\n\
|
||||
\n\
|
||||
|
||||
@@ -174,16 +174,14 @@ addon.check_update.empty=Alle Dateien sind auf dem neuesten Stand
|
||||
addon.check_update.failed_check=Fehler beim Suchen nach Aktualisierungen.
|
||||
addon.check_update.failed_download=Fehler beim Herunterladen einiger Dateien.
|
||||
addon.check_update.file=Datei
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Quelle
|
||||
addon.check_update.target_version=Zielversion
|
||||
addon.curseforge=CurseForge
|
||||
addon.dependency.embedded=Integrierte Abhängigkeiten (Bereits in der Addon-Datei vom Autor gepackt. Muss nicht separat heruntergeladen werden)
|
||||
addon.dependency.optional=Optionale Abhängigkeiten (Wenn diese fehlen, läuft das Spiel normal, aber die Addon-Funktionen fehlen möglicherweise)
|
||||
addon.dependency.required=Erforderliche Abhängigkeiten (Müssen separat heruntergeladen werden. Ein Fehlen kann den Start des Spiels verhindern)
|
||||
addon.dependency.tool=Erforderliche Abhängigkeiten (Müssen separat heruntergeladen werden. Ein Fehlen kann den Start des Spiels verhindern)
|
||||
addon.dependency.include=Integrierte Abhängigkeiten (Bereits in der Addon-Datei vom Autor gepackt. Muss nicht separat heruntergeladen werden)
|
||||
addon.dependency.incompatible=Inkompatible Addons (Wenn diese Addons gleichzeitig installiert werden, kann das Spiel nicht gestartet werden)
|
||||
addon.dependency.broken=Fehlerhafte Abhängigkeiten (Dieses Addon existierte vorher, ist aber nicht mehr vorhanden. Versuchen Sie, eine andere Download-Quelle zu verwenden.)
|
||||
# addon.dependencies.has_broken=
|
||||
addon.download.recommend=Empfohlene Version für Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
@@ -194,6 +192,7 @@ addon.sort.last_updated=Zuletzt aktualisiert
|
||||
addon.sort.popularity=Beliebtheit
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Downloads insgesamt
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=Autor(en)
|
||||
archive.date=Veröffentlichungsdatum
|
||||
@@ -227,6 +226,9 @@ button.remove.confirm=Sind Sie sicher, dass Sie dies dauerhaft entfernen möchte
|
||||
button.save=Speichern
|
||||
button.save_as=Speichern unter
|
||||
button.select_all=Alles auswählen
|
||||
# button.select_date=
|
||||
# button.select_date.from=
|
||||
# button.select_date.to=
|
||||
button.view=Anzeigen
|
||||
button.yes=Ja
|
||||
|
||||
@@ -1357,6 +1359,10 @@ schematics.info.version=Schematik-Version
|
||||
schematics.manage=Schematiken
|
||||
schematics.sub_items=%d Unterelement(e)
|
||||
|
||||
# screenshots.clear.confirm=
|
||||
# screenshots.manage=
|
||||
# screenshots.set_as_bg=
|
||||
|
||||
search=Suchen
|
||||
search.hint.chinese=Auf Englisch und Chinesisch suchen
|
||||
search.hint.english=Nur auf Englisch suchen
|
||||
@@ -1566,6 +1572,7 @@ settings.launcher.font.anti_aliasing.auto=Auto
|
||||
settings.launcher.font.anti_aliasing.gray=Graustufen
|
||||
settings.launcher.font.anti_aliasing.lcd=Sub-Pixel
|
||||
settings.launcher.fonts=Schriftarten
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=Allgemein
|
||||
settings.launcher.language=Sprache
|
||||
settings.launcher.launcher_log.export=Launcher-Protokolle exportieren
|
||||
@@ -1774,6 +1781,7 @@ unofficial.hint=Sie verwenden einen inoffiziellen Build von HMCL. Wir können di
|
||||
update=Aktualisieren
|
||||
update.accept=Aktualisieren
|
||||
update.changelog=Änderungsprotokoll
|
||||
# update.changelog.empty=
|
||||
update.channel.dev=Beta
|
||||
update.channel.dev.hint=Sie verwenden derzeit einen Beta-Kanal-Build des Launchers. Obwohl er möglicherweise einige zusätzliche Funktionen enthält, ist er manchmal auch weniger stabil als die Builds des stabilen Kanals.\n\
|
||||
\n\
|
||||
|
||||
@@ -174,16 +174,14 @@ addon.check_update.empty=Todos los mods están actualizados
|
||||
addon.check_update.failed_check=No se ha podido comprobar si hay actualizaciones.
|
||||
addon.check_update.failed_download=No se han podido descargar algunos de los archivos.
|
||||
addon.check_update.file=Archivo
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Fuente
|
||||
addon.check_update.target_version=Versión de destino
|
||||
addon.curseforge=CurseForge
|
||||
# addon.dependency.embedded=
|
||||
# addon.dependency.optional=
|
||||
# addon.dependency.required=
|
||||
# addon.dependency.tool=
|
||||
# addon.dependency.include=
|
||||
# addon.dependency.incompatible=
|
||||
# addon.dependency.broken=
|
||||
# addon.dependencies.has_broken=
|
||||
addon.download.recommend=Versión recomendada - Minecraft %1s
|
||||
# addon.download.title.release=
|
||||
@@ -194,6 +192,7 @@ addon.sort.last_updated=Última actualización
|
||||
addon.sort.popularity=Popularidad
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Descargas totales
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=Autor(es)
|
||||
archive.date=Fecha de publicación
|
||||
@@ -227,6 +226,9 @@ button.remove.confirm=¿Estás seguro de que deseas eliminarlo de forma permanen
|
||||
button.save=Guardar
|
||||
button.save_as=Guardar como
|
||||
button.select_all=Seleccionar todo
|
||||
# button.select_date=
|
||||
# button.select_date.from=
|
||||
# button.select_date.to=
|
||||
button.view=Vista
|
||||
button.yes=Sí
|
||||
|
||||
@@ -1350,6 +1352,10 @@ schematics.info.version=Versión de esquema
|
||||
schematics.manage=Esquemas
|
||||
schematics.sub_items=%d elemento(s)
|
||||
|
||||
# screenshots.clear.confirm=
|
||||
# screenshots.manage=
|
||||
# screenshots.set_as_bg=
|
||||
|
||||
search=Búsqueda
|
||||
search.hint.chinese=Buscar en inglés y chino
|
||||
search.hint.english=Buscar sólo en inglés
|
||||
@@ -1557,6 +1563,7 @@ settings.launcher.font.anti_aliasing.auto=Automático
|
||||
settings.launcher.font.anti_aliasing.gray=Escala de grises
|
||||
settings.launcher.font.anti_aliasing.lcd=Subpíxel
|
||||
# settings.launcher.fonts=
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=General
|
||||
settings.launcher.language=Idioma
|
||||
settings.launcher.launcher_log.export=Exportar registros del launcher
|
||||
@@ -1762,6 +1769,7 @@ unofficial.hint=Está utilizando una versión no oficial de HMCL. No podemos gar
|
||||
update=Actualizar
|
||||
update.accept=Actualizar
|
||||
update.changelog=Registro de cambios
|
||||
# update.changelog.empty=
|
||||
update.channel.dev=Beta
|
||||
update.channel.dev.hint=Actualmente está utilizando una versión beta del launcher, que puede incluir algunas características adicionales, pero también es más inestable que las versiones de lanzamiento.\n\
|
||||
\n\
|
||||
|
||||
@@ -163,16 +163,14 @@ addon.check_update.current_version=Current
|
||||
addon.check_update.failed_check=更新のチェックに失敗しました
|
||||
addon.check_update.failed_download=一部のファイルのダウンロードに失敗しました
|
||||
addon.check_update.file=ファイル
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Source
|
||||
addon.check_update.target_version=Target
|
||||
addon.curseforge=CurseForge
|
||||
# addon.dependency.embedded=
|
||||
# addon.dependency.optional=
|
||||
# addon.dependency.required=
|
||||
# addon.dependency.tool=
|
||||
# addon.dependency.include=
|
||||
# addon.dependency.incompatible=
|
||||
# addon.dependency.broken=
|
||||
# addon.dependencies.has_broken=
|
||||
# addon.download.recommend=
|
||||
# addon.download.title.release=
|
||||
@@ -183,6 +181,7 @@ addon.sort.last_updated=最終更新
|
||||
addon.sort.popularity=人気
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=合計ダウンロード数
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=作成者
|
||||
archive.date=公開日
|
||||
@@ -216,6 +215,9 @@ button.remove.confirm=本当に完全に削除しますか?この操作は元
|
||||
button.save=保存
|
||||
button.save_as=名前を付けて保存
|
||||
button.select_all=すべて選択
|
||||
# button.select_date=
|
||||
# button.select_date.from=
|
||||
# button.select_date.to=
|
||||
button.view=読む
|
||||
button.yes=はい
|
||||
|
||||
@@ -1154,6 +1156,10 @@ reveal.in_file_manager=ファイルマネージャーで表示する
|
||||
# schematics.manage=
|
||||
# schematics.sub_items=
|
||||
|
||||
# screenshots.clear.confirm=
|
||||
# screenshots.manage=
|
||||
# screenshots.set_as_bg=
|
||||
|
||||
search=検索
|
||||
search.hint.chinese=中国語と英語での検索をサポート
|
||||
search.hint.english=英語でのみ検索をサポート
|
||||
@@ -1361,6 +1367,7 @@ settings.launcher.font=フォント
|
||||
# settings.launcher.font.anti_aliasing.gray=
|
||||
# settings.launcher.font.anti_aliasing.lcd=
|
||||
# settings.launcher.fonts=
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=全般的
|
||||
settings.launcher.language=言語
|
||||
settings.launcher.launcher_log.export=ランチャーログのエクスポート
|
||||
@@ -1566,6 +1573,7 @@ system.operating_system=OS
|
||||
update=更新
|
||||
update.accept=更新
|
||||
update.changelog=変更
|
||||
# update.changelog.empty=
|
||||
update.channel.dev=ベータ
|
||||
update.channel.dev.hint=ベータ版を使用しています。ベータ版には、リリースバージョンと比較していくつかの追加機能が含まれている可能性があり、テストにのみ使用されます。ベータ版は不安定なはずです!\n\
|
||||
\n\
|
||||
|
||||
@@ -174,16 +174,14 @@ addon.check_update.empty=無改囊可迭更
|
||||
addon.check_update.failed_check=檢囊迭更未成
|
||||
addon.check_update.failed_download=有引案未成
|
||||
addon.check_update.file=案
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=源
|
||||
addon.check_update.target_version=將至之版
|
||||
addon.curseforge=CurseForge
|
||||
# addon.dependency.embedded=
|
||||
# addon.dependency.optional=
|
||||
# addon.dependency.required=
|
||||
# addon.dependency.tool=
|
||||
# addon.dependency.include=
|
||||
# addon.dependency.incompatible=
|
||||
# addon.dependency.broken=
|
||||
# addon.dependencies.has_broken=
|
||||
addon.download.recommend=宜版 - 礦藝 %1s
|
||||
# addon.download.title.release=
|
||||
@@ -194,6 +192,7 @@ addon.sort.last_updated=近日迭更
|
||||
addon.sort.popularity=幾熱
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=引數
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=作者
|
||||
archive.date=廣佈之期
|
||||
@@ -227,6 +226,9 @@ button.remove.confirm=誠刪之?刪則不復!
|
||||
button.save=存
|
||||
button.save_as=另存
|
||||
button.select_all=悉擇之
|
||||
# button.select_date=
|
||||
# button.select_date.from=
|
||||
# button.select_date.to=
|
||||
button.view=覽
|
||||
button.yes=然
|
||||
|
||||
@@ -1166,6 +1168,10 @@ schematics.info.version=蜃圖之版
|
||||
schematics.manage=司蜃圖
|
||||
schematics.sub_items=%d 子项
|
||||
|
||||
# screenshots.clear.confirm=
|
||||
# screenshots.manage=
|
||||
# screenshots.set_as_bg=
|
||||
|
||||
search=尋
|
||||
search.hint.chinese=以中英文尋
|
||||
search.hint.english=惟以英文尋
|
||||
@@ -1373,6 +1379,7 @@ settings.launcher.font.anti_aliasing.auto=自調
|
||||
settings.launcher.font.anti_aliasing.gray=灰階
|
||||
settings.launcher.font.anti_aliasing.lcd=子像素
|
||||
# settings.launcher.fonts=
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=貫用
|
||||
settings.launcher.language=文
|
||||
settings.launcher.launcher_log.export=錄出啟者之誌
|
||||
@@ -1578,6 +1585,7 @@ unofficial.hint=子方用非官築之 HMCL。余無能證其安,請君自辨
|
||||
update=啟者迭更
|
||||
update.accept=迭更
|
||||
update.changelog=迭更誌
|
||||
# update.changelog.empty=
|
||||
update.channel.dev=開發版
|
||||
update.channel.dev.hint=君所用之 HMCL 乃開發版也。開發版納試功之未行於定版者,蓋所以驗新者也。預版曾不以全驗,安能穩也!<a href="https://hmcl.huangyuhui.net/download">引定版</a>\n\
|
||||
\n\
|
||||
|
||||
@@ -170,16 +170,14 @@ addon.check_update.empty=Все файлы актуальны
|
||||
addon.check_update.failed_check=Не удалось проверить обновления.
|
||||
addon.check_update.failed_download=Не удалось скачать некоторые файлы.
|
||||
addon.check_update.file=Файл
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Источник
|
||||
addon.check_update.target_version=Целевая версия
|
||||
addon.curseforge=CurseForge
|
||||
addon.dependency.embedded=Встроенные зависимости (уже включены автором в файл аддона, скачивать отдельно не нужно)
|
||||
addon.dependency.optional=Необязательные зависимости (без них игра запустится, но часть функций аддона может не работать)
|
||||
addon.dependency.required=Обязательные зависимости (нужно скачать отдельно, без них игра может не запуститься)
|
||||
addon.dependency.tool=Обязательные зависимости (нужно скачать отдельно, без них игра может не запуститься)
|
||||
addon.dependency.include=Встроенные зависимости (уже включены автором в файл аддона, скачивать отдельно не нужно)
|
||||
addon.dependency.incompatible=Несовместимые аддоны (если установить их одновременно, игра не запустится)
|
||||
addon.dependency.broken=Сломанные зависимости (этот аддон раньше существовал, но теперь недоступен. Попробуйте другой источник скачивания.)
|
||||
addon.dependencies.has_broken=У этой версии есть сломанные зависимости. Попробуйте другой источник скачивания, иначе установка может не удаться.
|
||||
addon.download.recommend=Рекомендуемая версия для Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
@@ -190,6 +188,7 @@ addon.sort.last_updated=Обновлён
|
||||
addon.sort.popularity=Популярность
|
||||
addon.sort.relevancy=Релевантность
|
||||
addon.sort.total_downloads=Скачиваний
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=Автор(ы)
|
||||
archive.date=Дата публикации
|
||||
@@ -223,6 +222,9 @@ button.remove.confirm=Удалить навсегда? Это действие
|
||||
button.save=Сохранить
|
||||
button.save_as=Сохранить как
|
||||
button.select_all=Выбрать все
|
||||
# button.select_date=
|
||||
# button.select_date.from=
|
||||
# button.select_date.to=
|
||||
button.view=Просмотреть
|
||||
button.yes=Да
|
||||
|
||||
@@ -1290,6 +1292,10 @@ schematics.info.version=Версия схемы
|
||||
schematics.manage=Схемы
|
||||
schematics.sub_items=%d элемент(ов)
|
||||
|
||||
# screenshots.clear.confirm=
|
||||
# screenshots.manage=
|
||||
# screenshots.set_as_bg=
|
||||
|
||||
search=Поиск
|
||||
search.hint.chinese=Поиск на китайском и английском языках
|
||||
search.hint.english=Поиск только на английском языке
|
||||
@@ -1497,6 +1503,7 @@ settings.launcher.font.anti_aliasing.auto=Автоматическое
|
||||
settings.launcher.font.anti_aliasing.gray=Оттенки серого
|
||||
settings.launcher.font.anti_aliasing.lcd=Субпиксельное
|
||||
settings.launcher.fonts=Шрифты
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=Общие
|
||||
settings.launcher.language=Язык
|
||||
settings.launcher.launcher_log.export=Экспорт логов лаунчера
|
||||
@@ -1702,6 +1709,7 @@ unofficial.hint=Вы используете неофициальную верс
|
||||
update=Обновление
|
||||
update.accept=Обновить
|
||||
update.changelog=Изменения
|
||||
# update.changelog.empty=
|
||||
update.channel.dev=Бета
|
||||
update.channel.dev.hint=Вы используете бета-версию лаунчера. В ней могут быть дополнительные функции, но иногда она работает менее стабильно, чем стабильная версия.\n\nЕсли вы столкнулись с ошибками или проблемами, сообщите о них через каналы, указанные на странице <a href="hmcl://settings/feedback">«Обратная связь»</a>.\n\nПодпишитесь на <a href="https://space.bilibili.com/1445341">@huanghongxun</a> в Bilibili, чтобы следить за важными новостями HMCL, или на <a href="https://space.bilibili.com/20314891">@Glavo</a>, чтобы следить за ходом разработки HMCL.
|
||||
update.channel.dev.title=Уведомление о бета-версии
|
||||
|
||||
@@ -176,16 +176,14 @@ addon.check_update.empty=Усі моди оновлені
|
||||
addon.check_update.failed_check=Не вдалося перевірити оновлення.
|
||||
addon.check_update.failed_download=Не вдалося завантажити деякі файли.
|
||||
addon.check_update.file=Файл
|
||||
# addon.check_update.preview=
|
||||
addon.check_update.source=Джерело
|
||||
addon.check_update.target_version=Цільова версія
|
||||
addon.curseforge=CurseForge
|
||||
addon.dependency.embedded=Вбудовані залежності (Вже упаковані у файл доповнення автором. Не потрібно завантажувати окремо)
|
||||
addon.dependency.optional=Необов'язкові залежності (Якщо відсутні, гра буде працювати нормально, але функції доповнення можуть бути відсутні)
|
||||
addon.dependency.required=Обов'язкові залежності (Необхідно завантажити окремо. Їх відсутність може перешкодити запуску гри)
|
||||
addon.dependency.tool=Обов'язкові залежності (Необхідно завантажити окремо. Їх відсутність може перешкодити запуску гри)
|
||||
addon.dependency.include=Вбудовані залежності (Вже упаковані у файл доповнення автором. Не потрібно завантажувати окремо)
|
||||
addon.dependency.incompatible=Несумісні доповнення (Одночасне встановлення цих доповнень перешкодить запуску гри)
|
||||
addon.dependency.broken=Зламані залежності (Це доповнення існувало раніше, але його більше немає. Спробуйте використати інше джерело завантаження.)
|
||||
# addon.dependencies.has_broken=
|
||||
addon.download.recommend=Рекомендована версія для Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
@@ -196,6 +194,7 @@ addon.sort.last_updated=Останнє оновлення
|
||||
addon.sort.popularity=Популярність
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Всього завантажень
|
||||
# addon.version_type=
|
||||
|
||||
archive.author=Автор(и)
|
||||
archive.date=Дата публікації
|
||||
@@ -229,6 +228,9 @@ button.remove.confirm=Ви впевнені, що хочете назавжди
|
||||
button.save=Зберегти
|
||||
button.save_as=Зберегти як
|
||||
button.select_all=Вибрати все
|
||||
# button.select_date=
|
||||
# button.select_date.from=
|
||||
# button.select_date.to=
|
||||
button.view=Переглянути
|
||||
button.yes=Так
|
||||
|
||||
@@ -1313,6 +1315,10 @@ schematics.info.version=Версія схематики
|
||||
schematics.manage=Схематики
|
||||
schematics.sub_items=%d підпунктів
|
||||
|
||||
# screenshots.clear.confirm=
|
||||
# screenshots.manage=
|
||||
# screenshots.set_as_bg=
|
||||
|
||||
search=Пошук
|
||||
search.hint.chinese=Пошук англійською та китайською
|
||||
search.hint.english=Пошук лише англійською
|
||||
@@ -1520,6 +1526,7 @@ settings.launcher.font.anti_aliasing.auto=Авто
|
||||
settings.launcher.font.anti_aliasing.gray=Відтінки сірого
|
||||
settings.launcher.font.anti_aliasing.lcd=Субпіксельне
|
||||
settings.launcher.fonts=Шрифти
|
||||
# settings.launcher.game_content=
|
||||
settings.launcher.general=Загальні
|
||||
settings.launcher.language=Мова
|
||||
settings.launcher.launcher_log.export=Експортувати журнали лаунчера
|
||||
@@ -1725,6 +1732,7 @@ unofficial.hint=Ви використовуєте неофіційну збір
|
||||
update=Оновлення
|
||||
update.accept=Оновити
|
||||
update.changelog=Журнал змін
|
||||
# update.changelog.empty=
|
||||
update.channel.dev=Бета
|
||||
update.channel.dev.hint=Ви зараз використовуєте збірку лаунчера каналу Бета. Хоча вона може включати деякі додаткові функції, іноді вона менш стабільна, ніж збірки каналу Реліз. \n\
|
||||
\n\
|
||||
|
||||
@@ -175,16 +175,14 @@ addon.check_update.empty=没有需要更新的文件
|
||||
addon.check_update.failed_check=检查更新失败
|
||||
addon.check_update.failed_download=部分文件下载失败
|
||||
addon.check_update.file=文件
|
||||
addon.check_update.preview=显示测试版本
|
||||
addon.check_update.source=来源
|
||||
addon.check_update.target_version=目标版本
|
||||
addon.curseforge=CurseForge
|
||||
addon.dependency.embedded=内置的前置内容 (已经由作者打包在文件中,无需另外下载)
|
||||
addon.dependency.optional=可选的前置内容 (若缺失游戏能够正常运行,但功能可能缺失)
|
||||
addon.dependency.required=必需的前置内容 (必须另外下载,缺失可能会导致游戏无法启动)
|
||||
addon.dependency.tool=前置库 (必须另外下载,缺失可能会导致游戏无法启动)
|
||||
addon.dependency.include=内置的前置内容 (已经由作者打包在文件中,无需另外下载)
|
||||
addon.dependency.incompatible=不兼容的附加内容 (同时与正在下载的文件安装会导致游戏无法启动)
|
||||
addon.dependency.broken=损坏的前置内容 (该前置内容曾经在该附加内容下载源上存在过,但现在被删除了,换个下载源试试吧)
|
||||
addon.dependencies.has_broken=此版本存在损坏的前置内容。请尝试切换下载源,否则安装可能失败。
|
||||
addon.download.recommend=推荐版本 - Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
@@ -195,6 +193,7 @@ addon.sort.last_updated=最近更新
|
||||
addon.sort.popularity=热度
|
||||
addon.sort.relevancy=相关度
|
||||
addon.sort.total_downloads=下载量
|
||||
addon.version_type=版本类型
|
||||
|
||||
archive.author=作者
|
||||
archive.date=发布日期
|
||||
@@ -228,6 +227,9 @@ button.remove.confirm=你确定要删除吗?此操作无法撤销!
|
||||
button.save=保存
|
||||
button.save_as=另存为
|
||||
button.select_all=全选
|
||||
button.select_date=按日期选择
|
||||
button.select_date.from=开始
|
||||
button.select_date.to=截止
|
||||
button.view=查看
|
||||
button.yes=是
|
||||
|
||||
@@ -675,6 +677,7 @@ java.download.prompt=请选择你要下载的 Java 版本:
|
||||
java.download.distribution=发行版
|
||||
java.download.version=版本
|
||||
java.download.packageType=包类型
|
||||
java.empty=未能找到 Java
|
||||
java.management=Java 管理
|
||||
java.info.architecture=架构
|
||||
java.info.vendor=供应商
|
||||
@@ -857,7 +860,7 @@ modpack.type.curse.error=未能完成该整合包所需的依赖下载,请再
|
||||
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载。请尝试该整合包的最新版本或者安装其他整合包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
modpack.type.manual.warning=该整合包由发布者手动打包,其中可能已经包含启动器。建议尝试解压后使用其自带的启动器运行游戏。\n如遇到问题,你可以点击右上角帮助按钮进行求助。\nHMCL 可以尝试导入该整合包,但不保证可用性。是否继续?
|
||||
modpack.type.mcbbs=MCBBS 整合包 (推荐)
|
||||
modpack.type.mcbbs.export=可以被 HMCL 导入
|
||||
modpack.type.mcbbs.export=可以被主流第三方启动器导入
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=可以被主流第三方启动器导入
|
||||
modpack.type.multimc=MultiMC
|
||||
@@ -1170,6 +1173,10 @@ schematics.install_mod=安装投影模组
|
||||
schematics.manage=原理图管理
|
||||
schematics.sub_items=%d 个子项
|
||||
|
||||
screenshots.clear.confirm=你确定要清除所有截图吗?此操作无法撤销!
|
||||
screenshots.manage=截图管理
|
||||
screenshots.set_as_bg=设为启动器背景
|
||||
|
||||
search=搜索
|
||||
search.hint.chinese=支持中英文搜索
|
||||
search.hint.english=仅支持英文搜索
|
||||
@@ -1379,6 +1386,7 @@ settings.launcher.font.anti_aliasing.auto=自动
|
||||
settings.launcher.font.anti_aliasing.gray=灰度
|
||||
settings.launcher.font.anti_aliasing.lcd=子像素
|
||||
settings.launcher.fonts=字体
|
||||
settings.launcher.game_content=游戏内容
|
||||
settings.launcher.general=通用
|
||||
settings.launcher.language=语言
|
||||
settings.launcher.launcher_log.export=导出启动器日志
|
||||
@@ -1587,6 +1595,7 @@ unofficial.hint=你正在使用非官方构建的 HMCL。我们无法保证其
|
||||
update=启动器更新
|
||||
update.accept=更新
|
||||
update.changelog=更新日志
|
||||
update.changelog.empty=暂未上传更新日志
|
||||
update.channel.dev=开发版
|
||||
update.channel.dev.hint=你正在使用 HMCL 开发版。开发版包含一些未在稳定版中包含的测试性功能,仅用于体验新功能。开发版功能未受充分验证,使用起来可能不稳定!<a href="https://hmcl.huangyuhui.net/download">下载稳定版</a>\n\
|
||||
\n\
|
||||
|
||||
@@ -173,16 +173,14 @@ addon.check_update.empty=沒有需要更新的檔案
|
||||
addon.check_update.failed_check=檢查更新失敗
|
||||
addon.check_update.failed_download=部分檔案下載失敗
|
||||
addon.check_update.file=檔案
|
||||
addon.check_update.preview=顯示 Alpha/Beta 版本
|
||||
addon.check_update.source=來源
|
||||
addon.check_update.target_version=目標版本
|
||||
addon.curseforge=CurseForge
|
||||
addon.dependency.embedded=內建相依內容 (作者已經打包在檔案中,無需單獨下載)
|
||||
addon.dependency.optional=可選相依內容 (如果不安裝,遊戲可以執行,但功能可能缺失)
|
||||
addon.dependency.required=必需相依內容 (必須單獨下載,缺少可能會導致遊戲無法啟動)
|
||||
addon.dependency.tool=相依庫 (必須單獨下載,缺少可能會導致遊戲無法啟動)
|
||||
addon.dependency.include=內建相依內容 (作者已經打包在檔案中,無需單獨下載)
|
||||
addon.dependency.incompatible=不相容附加內容 (與正在下載的檔案同時安裝會導致遊戲無法啟動)
|
||||
addon.dependency.broken=損壞的相依內容 (該相依內容曾經存在於附加內容下載源中,但現在已被刪除,請嘗試其他下載源)
|
||||
addon.dependencies.has_broken=此版本存在損壞的前置內容。請嘗試切換下載源,否則安裝可能失敗。
|
||||
addon.download.recommend=推薦版本 - Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
@@ -193,6 +191,7 @@ addon.sort.last_updated=最近更新
|
||||
addon.sort.popularity=熱度
|
||||
addon.sort.relevancy=相關度
|
||||
addon.sort.total_downloads=下載量
|
||||
addon.version_type=版本類型
|
||||
|
||||
archive.author=作者
|
||||
archive.date=發布日期
|
||||
@@ -226,6 +225,9 @@ button.remove.confirm=你確認要刪除嗎?該操作無法復原!
|
||||
button.save=儲存
|
||||
button.save_as=另存新檔
|
||||
button.select_all=全選
|
||||
button.select_date=按日期選擇
|
||||
button.select_date.from=開始
|
||||
button.select_date.to=截止
|
||||
button.view=查看
|
||||
button.yes=是
|
||||
|
||||
@@ -669,6 +671,7 @@ java.download.prompt=請選擇你要下載的 Java 版本:
|
||||
java.download.distribution=發行版
|
||||
java.download.version=版本
|
||||
java.download.packageType=包類型
|
||||
java.empty=未能找到 Java
|
||||
java.management=Java 管理
|
||||
java.info.architecture=架構
|
||||
java.info.vendor=供應商
|
||||
@@ -1164,6 +1167,10 @@ schematics.install_mod=安裝投影模組
|
||||
schematics.manage=原理圖管理
|
||||
schematics.sub_items=%d 個子項
|
||||
|
||||
screenshots.clear.confirm=你確認要清除所有擷圖嗎?該操作無法復原!
|
||||
screenshots.manage=擷圖管理
|
||||
screenshots.set_as_bg=設成啟動器背景
|
||||
|
||||
search=搜尋
|
||||
search.hint.chinese=支援中英文搜尋
|
||||
search.hint.english=僅支援英文搜尋
|
||||
@@ -1373,6 +1380,7 @@ settings.launcher.font.anti_aliasing.auto=自動
|
||||
settings.launcher.font.anti_aliasing.gray=灰階
|
||||
settings.launcher.font.anti_aliasing.lcd=子像素
|
||||
settings.launcher.fonts=字體
|
||||
settings.launcher.game_content=遊戲內容
|
||||
settings.launcher.general=一般
|
||||
settings.launcher.language=語言
|
||||
settings.launcher.launcher_log.export=匯出啟動器日誌
|
||||
@@ -1581,6 +1589,7 @@ unofficial.hint=你正在使用非官方構建的 HMCL。我們無法保證其
|
||||
update=啟動器更新
|
||||
update.accept=更新
|
||||
update.changelog=更新日誌
|
||||
update.changelog.empty=暫未上傳更新日誌
|
||||
update.channel.dev=開發版
|
||||
update.channel.dev.hint=你正在使用 HMCL 開發版。開發版包含一些未在穩定版中包含的測試性功能,僅用於體驗新功能。開發版功能未受充分驗證,使用起來可能不穩定!\n\
|
||||
\n\
|
||||
|
||||
@@ -17,17 +17,22 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.addon;
|
||||
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdate;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdateConditions;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/// Sub-classes should implement `Comparable`
|
||||
@@ -53,42 +58,60 @@ public abstract class LocalAddonFile {
|
||||
|
||||
public abstract void delete() throws IOException;
|
||||
|
||||
/// @return the update target version restrictions for this addon file, or null if not upgradable.
|
||||
@Nullable
|
||||
public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException {
|
||||
protected AddonUpdateConditions getTargetVersionRestrictions() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
public record AddonUpdate(
|
||||
RemoteAddon.Source source,
|
||||
RemoteAddon.Type repoType,
|
||||
LocalAddonFile localAddonFile,
|
||||
RemoteAddon.Version currentVersion,
|
||||
RemoteAddon.Version targetVersion,
|
||||
boolean useRemoteFileName
|
||||
) {
|
||||
/// @return A pair of addon update info, the first for all versions and the second for release versions only (might be null).
|
||||
@Nullable
|
||||
public Pair<AddonUpdate, @Nullable AddonUpdate> checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException {
|
||||
var restrictions = getTargetVersionRestrictions();
|
||||
if (restrictions == null) return null;
|
||||
|
||||
RemoteAddonRepository repo = source.getRepository();
|
||||
Optional<RemoteAddon.Version> currentVersion = repo.getRemoteVersionByLocalFile(getFile());
|
||||
if (currentVersion.isEmpty()) return null;
|
||||
var current = currentVersion.orElseThrow();
|
||||
|
||||
var stream = repo.getRemoteVersionsById(downloadProvider, currentVersion.get().projectId())
|
||||
.filter(version -> version.gameVersions().contains(gameVersion));
|
||||
if (current.gameVersions().contains(gameVersion)) // Otherwise it means we are upgrading from another game version
|
||||
stream = stream.filter(version -> version.datePublished().isAfter(current.datePublished()));
|
||||
for (var p : restrictions.restrictions())
|
||||
stream = stream.filter(p);
|
||||
|
||||
List<RemoteAddon.Version> remoteVersions = stream.sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed()).toList();
|
||||
if (remoteVersions.isEmpty()) return null;
|
||||
var release = remoteVersions.stream()
|
||||
.filter(v -> v.versionType() == RemoteAddon.VersionType.Release)
|
||||
.findFirst().orElse(null);
|
||||
|
||||
return Pair.pair(
|
||||
new AddonUpdate(this, current, remoteVersions.get(0)), // All channels
|
||||
release != null ? new AddonUpdate(this, current, release) : null // Release channel
|
||||
);
|
||||
}
|
||||
|
||||
public static class Description {
|
||||
private final List<LocalAddonFile.Description.Part> parts;
|
||||
public void onUpdated(String newFileNameWithExt) {
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
public record Description(@Unmodifiable List<Part> parts) {
|
||||
|
||||
public Description {
|
||||
parts = List.copyOf(parts);
|
||||
}
|
||||
|
||||
public Description(String text) {
|
||||
this.parts = new ArrayList<>();
|
||||
this.parts.add(new LocalAddonFile.Description.Part(text, "black"));
|
||||
}
|
||||
|
||||
public Description(List<LocalAddonFile.Description.Part> parts) {
|
||||
this.parts = parts;
|
||||
}
|
||||
|
||||
public List<LocalAddonFile.Description.Part> getParts() {
|
||||
return parts;
|
||||
this(List.of(new Part(text, "black")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (LocalAddonFile.Description.Part part : parts) {
|
||||
for (Part part : parts) {
|
||||
builder.append(part.text);
|
||||
}
|
||||
return builder.toString();
|
||||
@@ -98,27 +121,16 @@ public abstract class LocalAddonFile {
|
||||
return toString().lines().map(String::trim).filter(StringUtils::isNotBlank).collect(Collectors.joining(" | "));
|
||||
}
|
||||
|
||||
public static class Part {
|
||||
private final String text;
|
||||
private final String color;
|
||||
public record Part(String text, String color) {
|
||||
|
||||
public Part {
|
||||
Objects.requireNonNull(text);
|
||||
Objects.requireNonNull(color);
|
||||
}
|
||||
|
||||
public Part(String text) {
|
||||
this(text, "");
|
||||
}
|
||||
|
||||
public Part(String text, String color) {
|
||||
this.text = Objects.requireNonNull(text);
|
||||
this.color = Objects.requireNonNull(color);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -54,8 +55,6 @@ public record RemoteAddon(String slug, String author, String title, String descr
|
||||
public enum DependencyType {
|
||||
REQUIRED,
|
||||
OPTIONAL,
|
||||
TOOL,
|
||||
INCLUDE,
|
||||
EMBEDDED,
|
||||
INCOMPATIBLE,
|
||||
BROKEN
|
||||
@@ -145,6 +144,7 @@ public record RemoteAddon(String slug, String author, String title, String descr
|
||||
|
||||
private final RemoteAddonRepository repo;
|
||||
|
||||
@NotNull
|
||||
public RemoteAddonRepository getRepository() {
|
||||
return repo;
|
||||
}
|
||||
@@ -169,11 +169,7 @@ public record RemoteAddon(String slug, String author, String title, String descr
|
||||
Stream<Version> loadVersions(RemoteAddonRepository repo, DownloadProvider downloadProvider) throws IOException;
|
||||
}
|
||||
|
||||
public interface IVersion {
|
||||
Source getSource();
|
||||
}
|
||||
|
||||
public record Version(IVersion self, String versionId, String projectId, String name, String version,
|
||||
public record Version(RemoteAddon.Source source, String versionId, String projectId, String name, String version,
|
||||
Instant datePublished, VersionType versionType, File file, List<Dependency> dependencies,
|
||||
List<String> gameVersions, List<AddonLoader> loaders) {
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public class DataPack {
|
||||
public static void installPack(Path sourceDataPackPath, Path targetDataPackDirectory, GameVersionNumber gameVersionNumber) throws IOException {
|
||||
boolean containsMultiplePacks;
|
||||
Set<String> packs = new HashSet<>();
|
||||
try (FileSystem fs = CompressingUtils.readonly(sourceDataPackPath).setAutoDetectEncoding(true).build()) {
|
||||
try (FileSystem fs = CompressingUtils.readonly(sourceDataPackPath).build()) {
|
||||
Path dataPacks = fs.getPath("datapacks");
|
||||
Path mcmeta = fs.getPath("pack.mcmeta");
|
||||
|
||||
|
||||
@@ -19,17 +19,17 @@ package org.jackhuang.hmcl.addon.mod;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.LocalAddonManager;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdateConditions;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -195,19 +195,8 @@ public final class LocalModFile extends LocalAddonFile implements Comparable<Loc
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException {
|
||||
RemoteAddonRepository repository = source.getRepository();
|
||||
if (repository == null) return null;
|
||||
Optional<RemoteAddon.Version> currentVersion = repository.getRemoteVersionByLocalFile(file);
|
||||
if (currentVersion.isEmpty()) return null;
|
||||
List<RemoteAddon.Version> remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().projectId())
|
||||
.filter(version -> version.gameVersions().contains(gameVersion))
|
||||
.filter(version -> version.loaders().stream().anyMatch(it -> it.type() == getModLoaderType()))
|
||||
.filter(version -> version.datePublished().compareTo(currentVersion.get().datePublished()) > 0)
|
||||
.sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed())
|
||||
.toList();
|
||||
if (remoteVersions.isEmpty()) return null;
|
||||
return new AddonUpdate(source, RemoteAddon.Type.MOD, this, currentVersion.get(), remoteVersions.get(0), true);
|
||||
protected AddonUpdateConditions getTargetVersionRestrictions() {
|
||||
return new AddonUpdateConditions(v -> v.loaders().stream().map(AddonLoader::type).collect(Collectors.toSet()).contains(getModLoaderType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-9
@@ -468,9 +468,9 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
pair(1, RemoteAddon.DependencyType.EMBEDDED),
|
||||
pair(2, RemoteAddon.DependencyType.OPTIONAL),
|
||||
pair(3, RemoteAddon.DependencyType.REQUIRED),
|
||||
pair(4, RemoteAddon.DependencyType.TOOL),
|
||||
pair(4, RemoteAddon.DependencyType.OPTIONAL), // CF name: TOOL
|
||||
pair(5, RemoteAddon.DependencyType.INCOMPATIBLE),
|
||||
pair(6, RemoteAddon.DependencyType.INCLUDE)
|
||||
pair(6, RemoteAddon.DependencyType.EMBEDDED) // CF name: INCLUDE, should never appear because CF hides this
|
||||
);
|
||||
|
||||
@Override
|
||||
@@ -554,7 +554,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
int releaseType, int fileStatus, List<LatestFileHash> hashes, Instant fileDate,
|
||||
int fileLength, int downloadCount, String downloadUrl, List<String> gameVersions,
|
||||
List<Dependency> dependencies, int alternateFileId, boolean isServerPack,
|
||||
long fileFingerprint) implements RemoteAddon.IVersion {
|
||||
long fileFingerprint) {
|
||||
|
||||
@Override
|
||||
public String downloadUrl() {
|
||||
@@ -566,11 +566,6 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
return downloadUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteAddon.Source getSource() {
|
||||
return RemoteAddon.Source.CURSEFORGE;
|
||||
}
|
||||
|
||||
public RemoteAddon.Version toVersion() {
|
||||
RemoteAddon.VersionType versionType = switch (releaseType()) {
|
||||
case 1 -> RemoteAddon.VersionType.Release;
|
||||
@@ -596,7 +591,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
}
|
||||
|
||||
return new RemoteAddon.Version(
|
||||
this,
|
||||
RemoteAddon.Source.CURSEFORGE,
|
||||
Integer.toString(id()),
|
||||
Integer.toString(modId()),
|
||||
displayName(),
|
||||
|
||||
+8
-17
@@ -443,7 +443,7 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
|
||||
@SerializedName("author_id") String authorId,
|
||||
@SerializedName("date_published") Instant datePublished, int downloads,
|
||||
@SerializedName("changelog_url") String changelogUrl,
|
||||
List<ProjectVersionFile> files) implements RemoteAddon.IVersion {
|
||||
List<ProjectVersionFile> files) {
|
||||
private static final Map<String, RemoteAddon.@Nullable DependencyType> DEPENDENCY_TYPE = mapOf(
|
||||
pair("required", RemoteAddon.DependencyType.REQUIRED),
|
||||
pair("optional", RemoteAddon.DependencyType.OPTIONAL),
|
||||
@@ -451,29 +451,20 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
|
||||
pair("incompatible", RemoteAddon.DependencyType.INCOMPATIBLE)
|
||||
);
|
||||
|
||||
@Override
|
||||
public RemoteAddon.Source getSource() {
|
||||
return RemoteAddon.Source.MODRINTH;
|
||||
}
|
||||
|
||||
public Optional<RemoteAddon.Version> toVersion() {
|
||||
RemoteAddon.VersionType type;
|
||||
if ("release".equals(versionType)) {
|
||||
type = RemoteAddon.VersionType.Release;
|
||||
} else if ("beta".equals(versionType)) {
|
||||
type = RemoteAddon.VersionType.Beta;
|
||||
} else if ("alpha".equals(versionType)) {
|
||||
type = RemoteAddon.VersionType.Alpha;
|
||||
} else {
|
||||
type = RemoteAddon.VersionType.Release;
|
||||
}
|
||||
RemoteAddon.VersionType type = switch (versionType) {
|
||||
case "release" -> RemoteAddon.VersionType.Release;
|
||||
case "beta" -> RemoteAddon.VersionType.Beta;
|
||||
case "alpha" -> RemoteAddon.VersionType.Alpha;
|
||||
default -> RemoteAddon.VersionType.Release;
|
||||
};
|
||||
|
||||
if (files.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new RemoteAddon.Version(
|
||||
this,
|
||||
RemoteAddon.Source.MODRINTH,
|
||||
id,
|
||||
projectId,
|
||||
name,
|
||||
|
||||
@@ -72,5 +72,4 @@ final class ResourcePackFolder extends ResourcePackFile {
|
||||
public void delete() throws IOException {
|
||||
FileUtils.deleteDirectory(file);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -509,6 +509,39 @@ public final class ResourcePackManager extends LocalAddonManager<ResourcePackFil
|
||||
}
|
||||
}
|
||||
|
||||
public boolean rename(String oldName, String newName) {
|
||||
if (newName.equals(oldName)) return false;
|
||||
|
||||
String oldNameNewFormat = "file/" + oldName;
|
||||
String newNameNewFormat = "file/" + newName;
|
||||
boolean modified = false;
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
Map<String, String> options = loadOptions();
|
||||
List<String> optPacks = new ArrayList<>(deserializePackList(options.get("resourcePacks")));
|
||||
List<String> optIncompatiblePacks = new ArrayList<>(deserializePackList(options.get("incompatibleResourcePacks")));
|
||||
for (var l : List.of(optPacks, optIncompatiblePacks)) {
|
||||
var it = l.listIterator();
|
||||
while (it.hasNext()) {
|
||||
var next = it.next();
|
||||
if (oldName.equals(next) || oldNameNewFormat.equals(next)) {
|
||||
it.set(supportsNewOptionsFormat ? newNameNewFormat : newName);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (modified) {
|
||||
options.put("resourcePacks", serializePackList(optPacks));
|
||||
options.put("incompatibleResourcePacks", serializePackList(optIncompatiblePacks));
|
||||
saveOptions(options);
|
||||
}
|
||||
return modified;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<Pair<ResourcePackFile, Boolean>> arePacksEnabled(Stream<ResourcePackFile> resourcePacks) {
|
||||
lock.lock();
|
||||
try {
|
||||
|
||||
+10
-18
@@ -18,10 +18,8 @@
|
||||
package org.jackhuang.hmcl.addon.resourcepack;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.meta.PackMcMeta;
|
||||
import org.jackhuang.hmcl.addon.update.AddonUpdateConditions;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -29,13 +27,11 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
final class ResourcePackZipFile extends ResourcePackFile {
|
||||
|
||||
private final PackMcMeta.PackInfo info;
|
||||
|
||||
public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOException {
|
||||
@@ -85,18 +81,14 @@ final class ResourcePackZipFile extends ResourcePackFile {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException {
|
||||
RemoteAddonRepository repository = source.getRepository();
|
||||
if (repository == null) return null;
|
||||
Optional<RemoteAddon.Version> currentVersion = repository.getRemoteVersionByLocalFile(file);
|
||||
if (currentVersion.isEmpty()) return null;
|
||||
List<RemoteAddon.Version> remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().projectId())
|
||||
.filter(version -> version.gameVersions().contains(gameVersion))
|
||||
.filter(version -> version.datePublished().compareTo(currentVersion.get().datePublished()) > 0)
|
||||
.sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed())
|
||||
.toList();
|
||||
if (remoteVersions.isEmpty()) return null;
|
||||
return new AddonUpdate(source, RemoteAddon.Type.RESOURCE_PACK, this, currentVersion.get(), remoteVersions.get(0), false);
|
||||
protected AddonUpdateConditions getTargetVersionRestrictions() {
|
||||
return new AddonUpdateConditions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdated(String newFileNameWithExt) {
|
||||
super.onUpdated(newFileNameWithExt);
|
||||
manager.rename(getFileNameWithExtension(), newFileNameWithExt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
public record AddonUpdate(
|
||||
LocalAddonFile localAddonFile,
|
||||
RemoteAddon.Version currentVersion,
|
||||
RemoteAddon.Version targetVersion
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
///
|
||||
/// @param restrictions conditions that the remote version must satisfy to become a valid target
|
||||
@NotNullByDefault
|
||||
public record AddonUpdateConditions(@Unmodifiable List<Predicate<RemoteAddon.Version>> restrictions) {
|
||||
public AddonUpdateConditions {
|
||||
restrictions = List.copyOf(restrictions);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public AddonUpdateConditions(Predicate<RemoteAddon.Version>... restrictions) {
|
||||
this(List.of(restrictions));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.addon.update;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@NotNullByDefault
|
||||
public record AddonUpdates(@Unmodifiable List<AddonUpdate> commonUpdates, @Unmodifiable List<AddonUpdate> releaseUpdates) {
|
||||
|
||||
public AddonUpdates {
|
||||
commonUpdates = List.copyOf(commonUpdates);
|
||||
releaseUpdates = List.copyOf(releaseUpdates);
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,7 @@ public class DefaultGameBuilder extends GameBuilder {
|
||||
Task<?> buildTask = libraryTask.thenComposeAsync(manifest -> {
|
||||
GameInstanceManifest resolved = draft.getBaseSnapshot().resolve(manifest);
|
||||
return new GameDownloadTask(dependencyManager, resolved)
|
||||
.withStage("hmcl.install.game:" + gameVersion)
|
||||
.thenApplyAsync(minecraftJar -> {
|
||||
draft.put(resolved.withPatches(manifest.patches()));
|
||||
draft.putPrimaryJar(instanceId, minecraftJar);
|
||||
|
||||
@@ -1,62 +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.download.liteloader;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class LiteLoaderBranch {
|
||||
|
||||
@SerializedName("libraries")
|
||||
private final Collection<Library> libraries;
|
||||
|
||||
@SerializedName("com.mumfrey:liteloader")
|
||||
private final Map<String, LiteLoaderVersion> liteLoader;
|
||||
|
||||
/**
|
||||
* No-arg constructor for Gson.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public LiteLoaderBranch() {
|
||||
this(Collections.emptySet(), Collections.emptyMap());
|
||||
}
|
||||
|
||||
public LiteLoaderBranch(Collection<Library> libraries, Map<String, LiteLoaderVersion> liteLoader) {
|
||||
this.libraries = libraries;
|
||||
this.liteLoader = liteLoader;
|
||||
}
|
||||
|
||||
public Collection<Library> getLibraries() {
|
||||
return Collections.unmodifiableCollection(libraries);
|
||||
}
|
||||
|
||||
public Map<String, LiteLoaderVersion> getLiteLoader() {
|
||||
return Collections.unmodifiableMap(liteLoader);
|
||||
}
|
||||
|
||||
}
|
||||
-65
@@ -1,65 +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.download.liteloader;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class LiteLoaderGameVersions {
|
||||
|
||||
@SerializedName("repo")
|
||||
private final LiteLoaderRepository repoitory;
|
||||
|
||||
@SerializedName("artefacts")
|
||||
private final LiteLoaderBranch artifacts;
|
||||
|
||||
@SerializedName("snapshots")
|
||||
private final LiteLoaderBranch snapshots;
|
||||
|
||||
/**
|
||||
* No-arg constructor for Gson.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public LiteLoaderGameVersions() {
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
public LiteLoaderGameVersions(LiteLoaderRepository repoitory, LiteLoaderBranch artifacts, LiteLoaderBranch snapshots) {
|
||||
this.repoitory = repoitory;
|
||||
this.artifacts = artifacts;
|
||||
this.snapshots = snapshots;
|
||||
}
|
||||
|
||||
public LiteLoaderRepository getRepoitory() {
|
||||
return repoitory;
|
||||
}
|
||||
|
||||
public LiteLoaderBranch getArtifacts() {
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
public LiteLoaderBranch getSnapshots() {
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
}
|
||||
-73
@@ -1,73 +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.download.liteloader;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class LiteLoaderRepository {
|
||||
|
||||
@SerializedName("stream")
|
||||
private final String stream;
|
||||
|
||||
@SerializedName("type")
|
||||
private final String type;
|
||||
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
@SerializedName("classifier")
|
||||
private final String classifier;
|
||||
|
||||
/**
|
||||
* No-arg constructor for Gson.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public LiteLoaderRepository() {
|
||||
this("", "", "", "");
|
||||
}
|
||||
|
||||
public LiteLoaderRepository(String stream, String type, String url, String classifier) {
|
||||
this.stream = stream;
|
||||
this.type = type;
|
||||
this.url = url;
|
||||
this.classifier = classifier;
|
||||
}
|
||||
|
||||
public String getStream() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getClassifier() {
|
||||
return classifier;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +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.download.liteloader;
|
||||
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class LiteLoaderVersion {
|
||||
private final String tweakClass;
|
||||
private final String file;
|
||||
private final String version;
|
||||
private final String md5;
|
||||
private final String timestamp;
|
||||
private final int lastSuccessfulBuild;
|
||||
private final Collection<Library> libraries;
|
||||
|
||||
/**
|
||||
* No-arg constructor for Gson.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public LiteLoaderVersion() {
|
||||
this("", "", "", "", "", 0, Collections.emptySet());
|
||||
}
|
||||
|
||||
public LiteLoaderVersion(String tweakClass, String file, String version, String md5, String timestamp, int lastSuccessfulBuild, Collection<Library> libraries) {
|
||||
this.tweakClass = tweakClass;
|
||||
this.file = file;
|
||||
this.version = version;
|
||||
this.md5 = md5;
|
||||
this.timestamp = timestamp;
|
||||
this.lastSuccessfulBuild = lastSuccessfulBuild;
|
||||
this.libraries = libraries;
|
||||
}
|
||||
|
||||
public String getTweakClass() {
|
||||
return tweakClass;
|
||||
}
|
||||
|
||||
public String getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getMd5() {
|
||||
return md5;
|
||||
}
|
||||
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public int getLastSuccessfulBuild() {
|
||||
return lastSuccessfulBuild;
|
||||
}
|
||||
|
||||
public Collection<Library> getLibraries() {
|
||||
return Collections.unmodifiableCollection(libraries);
|
||||
}
|
||||
|
||||
}
|
||||
-61
@@ -1,61 +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.download.liteloader;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class LiteLoaderVersionsMeta {
|
||||
|
||||
@SerializedName("description")
|
||||
private final String description;
|
||||
|
||||
@SerializedName("authors")
|
||||
private final String authors;
|
||||
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
public LiteLoaderVersionsMeta() {
|
||||
this("", "", "");
|
||||
}
|
||||
|
||||
public LiteLoaderVersionsMeta(String description, String authors, String url) {
|
||||
this.description = description;
|
||||
this.authors = authors;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getAuthors() {
|
||||
return authors;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
}
|
||||
-56
@@ -1,56 +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.download.liteloader;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class LiteLoaderVersionsRoot {
|
||||
|
||||
@SerializedName("versions")
|
||||
private final Map<String, LiteLoaderGameVersions> versions;
|
||||
|
||||
@SerializedName("meta")
|
||||
private final LiteLoaderVersionsMeta meta;
|
||||
|
||||
public LiteLoaderVersionsRoot() {
|
||||
this(Collections.emptyMap(), null);
|
||||
}
|
||||
|
||||
public LiteLoaderVersionsRoot(Map<String, LiteLoaderGameVersions> versions, LiteLoaderVersionsMeta meta) {
|
||||
this.versions = versions;
|
||||
this.meta = meta;
|
||||
}
|
||||
|
||||
public Map<String, LiteLoaderGameVersions> getVersions() {
|
||||
return Collections.unmodifiableMap(versions);
|
||||
}
|
||||
|
||||
public LiteLoaderVersionsMeta getMeta() {
|
||||
return meta;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -56,6 +56,7 @@ public final class NeoForgeInstallTask extends Task<GameInstancePatch> {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.manifest = manifest;
|
||||
this.remoteVersion = remoteVersion;
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -82,7 +82,7 @@ public final class World {
|
||||
}
|
||||
}
|
||||
} else if (Files.isRegularFile(file))
|
||||
try (FileSystem fs = CompressingUtils.readonly(this.file).setAutoDetectEncoding(true).build()) {
|
||||
try (FileSystem fs = CompressingUtils.readonly(this.file).build()) {
|
||||
Path root;
|
||||
if (Files.isRegularFile(fs.getPath("/level.dat"))) {
|
||||
root = fs.getPath("/");
|
||||
@@ -328,7 +328,7 @@ public final class World {
|
||||
}
|
||||
|
||||
if (Files.isRegularFile(file)) {
|
||||
try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) {
|
||||
try (FileSystem fs = CompressingUtils.readonly(file).build()) {
|
||||
Path levelDatPath = fs.getPath("/level.dat");
|
||||
if (Files.isRegularFile(levelDatPath)) {
|
||||
fileName = FileUtils.getName(file);
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.util.tree.ZipFileTree;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -37,7 +36,6 @@ import java.util.Map;
|
||||
public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>> {
|
||||
|
||||
private final Path zipFile;
|
||||
private final Charset encoding;
|
||||
private final List<String> subDirectories;
|
||||
private final Path jsonFile;
|
||||
private final T manifest;
|
||||
@@ -45,9 +43,8 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
|
||||
private final String name;
|
||||
private final String version;
|
||||
|
||||
public MinecraftInstanceTask(Path zipFile, Charset encoding, List<String> subDirectories, T manifest, ModpackProvider modpackProvider, String name, String version, Path jsonFile) {
|
||||
public MinecraftInstanceTask(Path zipFile, List<String> subDirectories, T manifest, ModpackProvider modpackProvider, String name, String version, Path jsonFile) {
|
||||
this.zipFile = zipFile;
|
||||
this.encoding = encoding;
|
||||
this.subDirectories = subDirectories.stream().map(FileUtils::normalizePath).toList();
|
||||
this.manifest = manifest;
|
||||
this.jsonFile = jsonFile;
|
||||
@@ -83,7 +80,7 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
|
||||
public void execute() throws Exception {
|
||||
List<ModpackConfiguration.FileInformation> overrides = new ArrayList<>();
|
||||
|
||||
try (var tree = new ZipFileTree(CompressingUtils.openZipFileWithPossibleEncoding(zipFile, encoding))) {
|
||||
try (var tree = new ZipFileTree(CompressingUtils.openZipFile(zipFile))) {
|
||||
for (String subDirectory : subDirectories) {
|
||||
ArchiveFileTree.Dir<ZipArchiveEntry> root = tree.getDirectory(subDirectory);
|
||||
if (root == null)
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -41,20 +40,18 @@ public abstract class Modpack {
|
||||
private String version;
|
||||
private String gameVersion;
|
||||
private String description;
|
||||
private transient Charset encoding;
|
||||
private ModpackManifest manifest;
|
||||
|
||||
public Modpack() {
|
||||
this("", null, null, null, null, null, null);
|
||||
this("", null, null, null, null, null);
|
||||
}
|
||||
|
||||
public Modpack(String name, String author, String version, String gameVersion, String description, Charset encoding, ModpackManifest manifest) {
|
||||
public Modpack(String name, String author, String version, String gameVersion, String description, ModpackManifest manifest) {
|
||||
this.name = name;
|
||||
this.author = author;
|
||||
this.version = version;
|
||||
this.gameVersion = gameVersion;
|
||||
this.description = description;
|
||||
this.encoding = encoding;
|
||||
this.manifest = manifest;
|
||||
}
|
||||
|
||||
@@ -103,15 +100,6 @@ public abstract class Modpack {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Charset getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
public Modpack setEncoding(Charset encoding) {
|
||||
this.encoding = encoding;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModpackManifest getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.io.Unzipper;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -31,7 +30,6 @@ public class ModpackInstallTask<T> extends Task<Void> {
|
||||
|
||||
private final Path modpackFile;
|
||||
private final Path dest;
|
||||
private final Charset charset;
|
||||
private final List<String> subDirectories;
|
||||
private final List<ModpackConfiguration.FileInformation> overrides;
|
||||
private final Predicate<String> callback;
|
||||
@@ -40,14 +38,12 @@ public class ModpackInstallTask<T> extends Task<Void> {
|
||||
///
|
||||
/// @param modpackFile a zip file
|
||||
/// @param dest destination to store unpacked files
|
||||
/// @param charset charset of the zip file
|
||||
/// @param subDirectories the subdirectory of zip file to unpack
|
||||
/// @param callback test whether the file (given full path) in zip file should be unpacked or not
|
||||
/// @param oldConfiguration old modpack information if upgrade
|
||||
public ModpackInstallTask(Path modpackFile, Path dest, Charset charset, List<String> subDirectories, Predicate<String> callback, ModpackConfiguration<T> oldConfiguration) {
|
||||
public ModpackInstallTask(Path modpackFile, Path dest, List<String> subDirectories, Predicate<String> callback, ModpackConfiguration<T> oldConfiguration) {
|
||||
this.modpackFile = modpackFile;
|
||||
this.dest = dest;
|
||||
this.charset = charset;
|
||||
this.subDirectories = subDirectories;
|
||||
this.callback = callback;
|
||||
|
||||
@@ -72,7 +68,6 @@ public class ModpackInstallTask<T> extends Task<Void> {
|
||||
.setSubDirectory(subDirectory)
|
||||
.setTerminateIfSubDirectoryNotExists()
|
||||
.setReplaceExistentFile(true)
|
||||
.setEncoding(charset)
|
||||
.setFilter((zipEntry, destFile, relativePath) -> {
|
||||
if (zipEntry.isDirectory()) return true;
|
||||
if (!callback.test(relativePath)) return false;
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -69,11 +68,10 @@ public interface ModpackProvider {
|
||||
///
|
||||
/// @param zipFile the opened modpack archive
|
||||
/// @param file the modpack archive path
|
||||
/// @param encoding the archive entry-name encoding
|
||||
/// @return the parsed modpack
|
||||
/// @throws IOException if the archive cannot be read as this format
|
||||
/// @throws JsonParseException if the required manifest is missing or malformed
|
||||
Modpack readManifest(ZipArchiveReader zipFile, Path file, Charset encoding) throws IOException, JsonParseException;
|
||||
Modpack readManifest(ZipArchiveReader zipFile, Path file) throws IOException, JsonParseException;
|
||||
|
||||
/// Injects provider-specific launch options from a serialized modpack configuration.
|
||||
///
|
||||
|
||||
@@ -122,6 +122,7 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
}
|
||||
|
||||
setStage("hmcl.modpack.download");
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -181,8 +181,8 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
}
|
||||
});
|
||||
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), any -> true, config).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), manifest, CurseModpackProvider.INSTANCE, manifest.name(), manifest.version(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack"));
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, Collections.singletonList(manifest.overrides()), any -> true, config).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, Collections.singletonList(manifest.overrides()), manifest, CurseModpackProvider.INSTANCE, manifest.name(), manifest.version(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack"));
|
||||
|
||||
@Nullable WebURL iconUri = NetworkUtils.toWebURLOrNull(iconUrl);
|
||||
if (iconUri != null) {
|
||||
@@ -230,7 +230,7 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
}
|
||||
|
||||
String pathPrefix = StringUtils.addSuffix(FileUtils.normalizePath(overridesDir), "/");
|
||||
try (var reader = CompressingUtils.openZipFileWithPossibleEncoding(zipFile, modpack.getEncoding())) {
|
||||
try (var reader = CompressingUtils.openZipFile(zipFile)) {
|
||||
for (var entry : reader.getEntries()) {
|
||||
String normalizedPath = FileUtils.normalizePath(entry.getName());
|
||||
if (!normalizedPath.startsWith(pathPrefix)) {
|
||||
|
||||
@@ -36,7 +36,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -69,7 +68,7 @@ public final class CurseModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modpack readManifest(ZipArchiveReader zip, Path file, Charset encoding) throws IOException, JsonParseException {
|
||||
public Modpack readManifest(ZipArchiveReader zip, Path file) throws IOException, JsonParseException {
|
||||
CurseManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "manifest.json"), CurseManifest.class);
|
||||
String description = "No description";
|
||||
try {
|
||||
@@ -79,7 +78,7 @@ public final class CurseModpackProvider implements ModpackProvider {
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
return new Modpack(manifest.name(), manifest.author(), manifest.version(), manifest.minecraft().gameVersion(), description, encoding, manifest) {
|
||||
return new Modpack(manifest.name(), manifest.author(), manifest.version(), manifest.minecraft().gameVersion(), description, manifest) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
|
||||
+1
@@ -97,6 +97,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
|
||||
this.configuration = configuration;
|
||||
|
||||
setStage("hmcl.modpack.download");
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -137,8 +137,8 @@ public final class McbbsModpackLocalInstallTask extends Task<Void> {
|
||||
repository.removeInstanceFromDisk(instanceId);
|
||||
});
|
||||
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack"));
|
||||
instanceTask = new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, McbbsModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(instanceId));
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack"));
|
||||
instanceTask = new MinecraftInstanceTask<>(zipFile, Collections.singletonList("/overrides"), manifest, McbbsModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(instanceId));
|
||||
dependents.add(instanceTask.withStage("hmcl.modpack"));
|
||||
|
||||
try (GameBuilder builder = this.updateTarget == null
|
||||
|
||||
@@ -32,7 +32,6 @@ import org.jackhuang.hmcl.util.gson.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -420,10 +419,10 @@ public class McbbsModpackManifest implements ModpackManifest, Validation {
|
||||
}
|
||||
}
|
||||
|
||||
public Modpack toModpack(Charset encoding) throws IOException {
|
||||
public Modpack toModpack() throws IOException {
|
||||
String gameVersion = addons.stream().filter(x -> GameComponentType.GAME.getPatchId().equals(x.id)).findAny()
|
||||
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
|
||||
return new Modpack(name, author, version, gameVersion, description, encoding, this) {
|
||||
return new Modpack(name, author, version, gameVersion, description, this) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -72,20 +71,20 @@ public final class McbbsModpackProvider implements ModpackProvider {
|
||||
config.getManifest().injectLaunchOptions(builder);
|
||||
}
|
||||
|
||||
private static Modpack fromManifestFile(InputStream json, Charset encoding) throws IOException, JsonParseException {
|
||||
private static Modpack fromManifestFile(InputStream json) throws IOException, JsonParseException {
|
||||
McbbsModpackManifest manifest = JsonUtils.fromNonNullJsonFully(json, McbbsModpackManifest.class);
|
||||
return manifest.toModpack(encoding);
|
||||
return manifest.toModpack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modpack readManifest(ZipArchiveReader zip, Path file, Charset encoding) throws IOException, JsonParseException {
|
||||
public Modpack readManifest(ZipArchiveReader zip, Path file) throws IOException, JsonParseException {
|
||||
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
|
||||
if (mcbbsPackMeta != null) {
|
||||
return fromManifestFile(zip.getInputStream(mcbbsPackMeta), encoding);
|
||||
return fromManifestFile(zip.getInputStream(mcbbsPackMeta));
|
||||
}
|
||||
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
|
||||
if (manifestJson != null) {
|
||||
return fromManifestFile(zip.getInputStream(manifestJson), encoding);
|
||||
return fromManifestFile(zip.getInputStream(manifestJson));
|
||||
}
|
||||
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ public class ModrinthCompletionTask extends Task<Void> {
|
||||
}
|
||||
|
||||
setStage("hmcl.modpack.download");
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -179,8 +179,8 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
});
|
||||
|
||||
List<String> subDirectories = Arrays.asList("/client-overrides", "/overrides");
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), subDirectories, any -> true, config).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), subDirectories, manifest, ModrinthModpackProvider.INSTANCE, manifest.getName(), manifest.getVersionId(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack"));
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, subDirectories, any -> true, config).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, subDirectories, manifest, ModrinthModpackProvider.INSTANCE, manifest.getName(), manifest.getVersionId(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack"));
|
||||
|
||||
@Nullable WebURL iconUri = NetworkUtils.toWebURLOrNull(iconUrl);
|
||||
if (iconUri != null) {
|
||||
|
||||
+2
-3
@@ -34,7 +34,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -67,11 +66,11 @@ public final class ModrinthModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modpack readManifest(ZipArchiveReader zip, Path file, Charset encoding) throws IOException, JsonParseException {
|
||||
public Modpack readManifest(ZipArchiveReader zip, Path file) throws IOException, JsonParseException {
|
||||
try {
|
||||
ModrinthManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), ModrinthManifest.class);
|
||||
|
||||
return new Modpack(manifest.getName(), "", manifest.getVersionId(), manifest.getGameVersion(), manifest.getSummary(), encoding, manifest) {
|
||||
return new Modpack(manifest.getName(), "", manifest.getVersionId(), manifest.getGameVersion(), manifest.getSummary(), manifest) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
|
||||
+7
-8
@@ -208,8 +208,8 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
|
||||
}
|
||||
|
||||
// TODO: Optimize unbearably slow ModpackInstallTask
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(mcDirectory), any -> true, config).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(mcDirectory), manifest, MultiMCModpackProvider.INSTANCE, manifest.getName(), null, repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack"));
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, Collections.singletonList(mcDirectory), any -> true, config).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, Collections.singletonList(mcDirectory), manifest, MultiMCModpackProvider.INSTANCE, manifest.getName(), null, repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack"));
|
||||
}
|
||||
|
||||
// Stage #1: Load all related Json-Patch from meta maven or local mod pack.
|
||||
@@ -378,7 +378,8 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
|
||||
|
||||
Path instanceJar = getPrimaryJarFile();
|
||||
dependencies.add(new GameDownloadTask(dependencyManager, instanceManifest)
|
||||
.thenAcceptAsync(cachedJar -> FileUtils.copyFile(cachedJar, instanceJar)));
|
||||
.thenAcceptAsync(cachedJar -> FileUtils.copyFile(cachedJar, instanceJar))
|
||||
.withStage("hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
setResult(artifact);
|
||||
@@ -410,11 +411,9 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
|
||||
try (FileSystem fs = openModpack()) {
|
||||
Path root = getRootPath(fs).resolve("jarmods");
|
||||
|
||||
try (FileSystem mc = CompressingUtils.writable(
|
||||
getPrimaryJarFile()
|
||||
).setAutoDetectEncoding(true).build()) {
|
||||
try (FileSystem mc = CompressingUtils.writable(getPrimaryJarFile()).build()) {
|
||||
for (String fileName : files) {
|
||||
try (FileSystem jm = CompressingUtils.readonly(root.resolve(fileName)).setAutoDetectEncoding(true).build()) {
|
||||
try (FileSystem jm = CompressingUtils.readonly(root.resolve(fileName)).build()) {
|
||||
FileUtils.copyDirectory(jm.getPath("/"), mc.getPath("/"));
|
||||
}
|
||||
}
|
||||
@@ -466,7 +465,7 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
|
||||
}
|
||||
|
||||
private FileSystem openModpack() throws IOException {
|
||||
return CompressingUtils.readonly(zipFile).setAutoDetectEncoding(true).setEncoding(modpack.getEncoding()).build();
|
||||
return CompressingUtils.readonly(zipFile).build();
|
||||
}
|
||||
|
||||
private static boolean testPath(Path root) {
|
||||
|
||||
+2
-3
@@ -32,7 +32,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -81,7 +80,7 @@ public final class MultiMCModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modpack readManifest(ZipArchiveReader modpackFile, Path modpackPath, Charset encoding) throws IOException {
|
||||
public Modpack readManifest(ZipArchiveReader modpackFile, Path modpackPath) throws IOException {
|
||||
String rootEntryName = getRootEntryName(modpackFile);
|
||||
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(modpackFile, rootEntryName);
|
||||
|
||||
@@ -92,7 +91,7 @@ public final class MultiMCModpackProvider implements ModpackProvider {
|
||||
throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack.");
|
||||
try (InputStream instanceStream = modpackFile.getInputStream(instanceEntry)) {
|
||||
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
|
||||
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
|
||||
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), cfg) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
|
||||
+1
@@ -103,6 +103,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
}
|
||||
|
||||
setStage("hmcl.modpack.download");
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -135,8 +135,8 @@ public class ServerModpackLocalInstallTask extends Task<Void> {
|
||||
repository.removeInstanceFromDisk(instanceId);
|
||||
});
|
||||
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, ServerModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack"));
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, Collections.singletonList("/overrides"), manifest, ServerModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack"));
|
||||
|
||||
try (GameBuilder builder = this.updateTarget == null
|
||||
? dependencyManager.newGameBuilder(instanceId)
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -123,10 +122,10 @@ public class ServerModpackManifest implements ModpackManifest, Validation {
|
||||
}
|
||||
}
|
||||
|
||||
public Modpack toModpack(Charset encoding) throws IOException {
|
||||
public Modpack toModpack() throws IOException {
|
||||
String gameVersion = addons.stream().filter(x -> GameComponentType.GAME.getPatchId().equals(x.id)).findAny()
|
||||
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
|
||||
return new Modpack(name, author, version, gameVersion, description, encoding, this) {
|
||||
return new Modpack(name, author, version, gameVersion, description, this) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -62,9 +61,9 @@ public final class ServerModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modpack readManifest(ZipArchiveReader zip, Path file, Charset encoding) throws IOException, JsonParseException {
|
||||
public Modpack readManifest(ZipArchiveReader zip, Path file) throws IOException, JsonParseException {
|
||||
String json = CompressingUtils.readTextZipEntry(zip, "server-manifest.json");
|
||||
ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class);
|
||||
return manifest.toModpack(encoding);
|
||||
return manifest.toModpack();
|
||||
}
|
||||
}
|
||||
@@ -895,6 +895,7 @@ public abstract class Task<T> {
|
||||
|
||||
public StagesHintTask(List<StagesHint> hints) {
|
||||
this.hints = hints;
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1210,6 +1211,7 @@ public abstract class Task<T> {
|
||||
private final class StageTask extends Task<T> {
|
||||
private StageTask(String stage) {
|
||||
this.setStage(stage);
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -637,6 +637,13 @@ public final class StringUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isASCII(byte[] array) {
|
||||
for (byte b : array)
|
||||
if (b < 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isAlphabetic(char ch) {
|
||||
return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z';
|
||||
}
|
||||
|
||||
@@ -83,22 +83,14 @@ public final class CSVTable {
|
||||
out.append(txt);
|
||||
else {
|
||||
out.append('"');
|
||||
// Escape quotes by doubling them as required by RFC 4180;
|
||||
// line breaks are kept literally inside quoted fields.
|
||||
for (int i = 0; i < txt.length(); i++) {
|
||||
char c = txt.charAt(i);
|
||||
switch (c) {
|
||||
case '"':
|
||||
out.append("\\\"");
|
||||
break;
|
||||
case '\r':
|
||||
out.append("\\r");
|
||||
break;
|
||||
case '\n':
|
||||
out.append("\\n");
|
||||
break;
|
||||
default:
|
||||
out.append(c);
|
||||
break;
|
||||
}
|
||||
if (c == '"')
|
||||
out.append("\"\"");
|
||||
else
|
||||
out.append(c);
|
||||
}
|
||||
out.append('"');
|
||||
}
|
||||
|
||||
@@ -20,8 +20,11 @@ package org.jackhuang.hmcl.util.io;
|
||||
import kala.compress.archivers.zip.ZipArchiveEntry;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.tree.ZipFileTree;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -32,68 +35,69 @@ import java.nio.file.spi.FileSystemProvider;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
/**
|
||||
* Utilities of compressing
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
/// Provides ZIP archive access and filename encoding detection.
|
||||
///
|
||||
/// @author huangyuhui
|
||||
@NotNullByDefault
|
||||
public final class CompressingUtils {
|
||||
|
||||
private static final FileSystemProvider ZIPFS_PROVIDER = FileSystemProvider.installedProviders().stream()
|
||||
/// ZIP filesystem provider, or `null` when the runtime does not include one.
|
||||
private static final @Nullable FileSystemProvider ZIPFS_PROVIDER = FileSystemProvider.installedProviders().stream()
|
||||
.filter(it -> "jar".equalsIgnoreCase(it.getScheme()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
/// Prevents instantiation of this utility class.
|
||||
private CompressingUtils() {
|
||||
}
|
||||
|
||||
private static CharsetDecoder newCharsetDecoder(Charset charset) {
|
||||
return charset.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
}
|
||||
|
||||
public static boolean testEncoding(Path zipFile, Charset encoding) throws IOException {
|
||||
try (ZipArchiveReader zf = openZipFile(zipFile, encoding)) {
|
||||
return testEncoding(zf, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean testEncoding(ZipArchiveReader zipFile, Charset encoding) {
|
||||
CharsetDecoder cd = newCharsetDecoder(encoding);
|
||||
CharBuffer cb = CharBuffer.allocate(32);
|
||||
|
||||
for (ZipArchiveEntry entry : zipFile.getEntries()) {
|
||||
if (entry.getGeneralPurposeBit().usesUTF8ForNames()) continue;
|
||||
|
||||
cd.reset();
|
||||
byte[] ba = entry.getRawName();
|
||||
int clen = (int) (ba.length * cd.maxCharsPerByte());
|
||||
if (clen == 0) continue;
|
||||
if (clen <= cb.capacity())
|
||||
cb.clear();
|
||||
else
|
||||
cb = CharBuffer.allocate(clen);
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(ba, 0, ba.length);
|
||||
CoderResult cr = cd.decode(bb, cb, true);
|
||||
if (!cr.isUnderflow()) return false;
|
||||
cr = cd.flush(cb);
|
||||
if (!cr.isUnderflow()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Charset findSuitableEncoding(Path zipFile) throws IOException {
|
||||
/// Guesses a charset for the archive's unmarked entry names, defaulting to UTF-8.
|
||||
///
|
||||
/// Names marked as UTF-8 or supplied by a Unicode extra field are excluded.
|
||||
/// UTF-8 is preferred when it can decode every remaining name, including when
|
||||
/// there are no remaining names. Otherwise, statistical candidates are tried
|
||||
/// in descending score order, including low-scoring candidates when necessary.
|
||||
/// Candidates must strictly decode every unmarked name. If none qualifies,
|
||||
/// UTF-8 is returned even if some names cannot be decoded with it.
|
||||
/// Successful decoding does not guarantee that the original charset was identified.
|
||||
///
|
||||
/// @param zipFile archive to inspect
|
||||
/// @return guessed filename charset, or UTF-8 when no candidate qualifies
|
||||
/// @throws IOException if the archive cannot be read or its reader cannot be closed
|
||||
static Charset findSuitableEncoding(Path zipFile) throws IOException {
|
||||
try (ZipArchiveReader zf = openZipFile(zipFile, StandardCharsets.UTF_8)) {
|
||||
return findSuitableEncoding(zf);
|
||||
}
|
||||
}
|
||||
|
||||
public static Charset findSuitableEncoding(ZipArchiveReader zipFile) throws IOException {
|
||||
if (testEncoding(zipFile, StandardCharsets.UTF_8)) return StandardCharsets.UTF_8;
|
||||
if (OperatingSystem.NATIVE_CHARSET != StandardCharsets.UTF_8 && testEncoding(zipFile, OperatingSystem.NATIVE_CHARSET))
|
||||
return OperatingSystem.NATIVE_CHARSET;
|
||||
/// Guesses a filename charset without closing the supplied reader,
|
||||
/// defaulting to UTF-8 when no candidate qualifies.
|
||||
///
|
||||
/// Statistical sampling is bounded, but charset validation covers every
|
||||
/// unmarked entry and resets decoder state between filenames.
|
||||
static Charset findSuitableEncoding(ZipArchiveReader zipFile) {
|
||||
@Nullable List<byte[]> rawNames = null;
|
||||
|
||||
for (ZipArchiveEntry entry : zipFile.getEntries()) {
|
||||
if (entry.getNameSource() == ZipArchiveEntry.NameSource.NAME_WITH_EFS_FLAG)
|
||||
continue;
|
||||
|
||||
byte @Nullable [] rawName = entry.getRawName();
|
||||
if (rawName == null || StringUtils.isASCII(rawName))
|
||||
continue;
|
||||
|
||||
if (rawNames == null)
|
||||
rawNames = new ArrayList<>();
|
||||
|
||||
rawNames.add(rawName);
|
||||
}
|
||||
|
||||
if (rawNames == null)
|
||||
return StandardCharsets.UTF_8;
|
||||
|
||||
String[] candidates = {
|
||||
"UTF-8",
|
||||
OperatingSystem.NATIVE_CHARSET.name(),
|
||||
"GB18030",
|
||||
"Big5",
|
||||
"Shift_JIS",
|
||||
@@ -110,122 +114,158 @@ public final class CompressingUtils {
|
||||
"ISO-8859-1",
|
||||
"ISO-8859-5",
|
||||
"ISO-8859-7",
|
||||
"ISO-8859-8",
|
||||
"UTF-16LE", "UTF-16BE",
|
||||
"UTF-32LE", "UTF-32BE"
|
||||
"ISO-8859-8"
|
||||
};
|
||||
|
||||
CharBuffer cb = CharBuffer.allocate(128);
|
||||
|
||||
loop:
|
||||
for (String candidate : candidates) {
|
||||
Charset charset;
|
||||
|
||||
try {
|
||||
Charset charset = Charset.forName(candidate);
|
||||
if (!charset.equals(OperatingSystem.NATIVE_CHARSET) && testEncoding(zipFile, charset)) {
|
||||
return charset;
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
charset = Charset.forName(candidate);
|
||||
} catch (Exception ignored) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CharsetDecoder decoder = charset.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
|
||||
for (byte[] ba : rawNames) {
|
||||
decoder.reset();
|
||||
int clen = (int) (ba.length * decoder.maxCharsPerByte());
|
||||
if (clen == 0) continue;
|
||||
if (clen <= cb.capacity())
|
||||
cb.clear();
|
||||
else
|
||||
cb = CharBuffer.allocate(clen);
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(ba, 0, ba.length);
|
||||
CoderResult cr = decoder.decode(bb, cb, true);
|
||||
if (!cr.isUnderflow()) continue loop;
|
||||
cr = decoder.flush(cb);
|
||||
if (!cr.isUnderflow()) continue loop;
|
||||
}
|
||||
return charset;
|
||||
}
|
||||
|
||||
throw new IOException("Cannot find suitable encoding for the zip.");
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
|
||||
/// Opens an owning ZIP tree using detected filename encoding.
|
||||
///
|
||||
/// @throws IOException if opening or reading the archive fails
|
||||
public static ZipFileTree openZipTree(Path zipFile) throws IOException {
|
||||
return new ZipFileTree(openZipFile(zipFile));
|
||||
}
|
||||
|
||||
/// Opens a ZIP reader using detected filename encoding.
|
||||
///
|
||||
/// The caller must close the returned reader. Readers opened during detection
|
||||
/// are closed if detection fails.
|
||||
///
|
||||
/// @throws IOException if opening, reading, or closing an intermediate reader fails
|
||||
public static ZipArchiveReader openZipFile(Path zipFile) throws IOException {
|
||||
return openZipFileWithPossibleEncoding(zipFile, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static ZipArchiveReader openZipFile(Path zipFile, Charset charset) throws IOException {
|
||||
return new ZipArchiveReader(zipFile, charset, true, true);
|
||||
}
|
||||
|
||||
public static ZipArchiveReader openZipFileWithPossibleEncoding(Path zipFile, Charset possibleEncoding) throws IOException {
|
||||
if (possibleEncoding == null)
|
||||
possibleEncoding = StandardCharsets.UTF_8;
|
||||
|
||||
ZipArchiveReader zipReader = new ZipArchiveReader(zipFile, possibleEncoding, true, true);
|
||||
ZipArchiveReader zipReader = openZipFile(zipFile, StandardCharsets.UTF_8);
|
||||
|
||||
Charset suitableEncoding;
|
||||
try {
|
||||
if (possibleEncoding != StandardCharsets.UTF_8 && CompressingUtils.testEncoding(zipReader, possibleEncoding)) {
|
||||
suitableEncoding = possibleEncoding;
|
||||
} else {
|
||||
suitableEncoding = CompressingUtils.findSuitableEncoding(zipReader);
|
||||
if (suitableEncoding == StandardCharsets.UTF_8)
|
||||
return zipReader;
|
||||
}
|
||||
suitableEncoding = findSuitableEncoding(zipReader);
|
||||
} catch (Throwable e) {
|
||||
IOUtils.closeQuietly(zipReader, e);
|
||||
throw e;
|
||||
}
|
||||
if (suitableEncoding == StandardCharsets.UTF_8)
|
||||
return zipReader;
|
||||
|
||||
zipReader.close();
|
||||
return new ZipArchiveReader(zipFile, suitableEncoding, true, true);
|
||||
return openZipFile(zipFile, suitableEncoding);
|
||||
}
|
||||
|
||||
/// Opens a reader using the supplied charset for names without Unicode metadata.
|
||||
private static ZipArchiveReader openZipFile(Path zipFile, Charset charset) throws IOException {
|
||||
return new ZipArchiveReader(zipFile, charset, true, true);
|
||||
}
|
||||
|
||||
/// Configures a ZIP filesystem whose filename charset is detected when opened.
|
||||
public static final class Builder {
|
||||
private boolean autoDetectEncoding = false;
|
||||
private Charset encoding = StandardCharsets.UTF_8;
|
||||
/// Whether the provider should use temporary files for entry updates.
|
||||
private boolean useTempFile = false;
|
||||
/// Whether to pass the provider's archive-creation option.
|
||||
private final boolean create;
|
||||
/// Archive whose names are inspected before opening the filesystem.
|
||||
private final Path zip;
|
||||
|
||||
/// Creates a builder with the supplied archive path and creation option.
|
||||
public Builder(Path zip, boolean create) {
|
||||
this.zip = zip;
|
||||
this.create = create;
|
||||
}
|
||||
|
||||
public Builder setAutoDetectEncoding(boolean autoDetectEncoding) {
|
||||
this.autoDetectEncoding = autoDetectEncoding;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setEncoding(Charset encoding) {
|
||||
this.encoding = encoding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Sets temporary-file usage for entry updates and returns this builder.
|
||||
public Builder setUseTempFile(boolean useTempFile) {
|
||||
this.useTempFile = useTempFile;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Detects names in the existing archive and opens its filesystem.
|
||||
///
|
||||
/// The caller must close the returned filesystem. Encoding detection requires
|
||||
/// an existing archive even when the creation option is enabled.
|
||||
///
|
||||
/// @throws IOException if detection or opening the filesystem fails
|
||||
public FileSystem build() throws IOException {
|
||||
if (autoDetectEncoding) {
|
||||
if (!testEncoding(zip, encoding)) {
|
||||
encoding = findSuitableEncoding(zip);
|
||||
}
|
||||
}
|
||||
return createZipFileSystem(zip, create, useTempFile, encoding);
|
||||
return createZipFileSystem(zip, create, useTempFile, findSuitableEncoding(zip));
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a builder with archive creation disabled.
|
||||
public static Builder readonly(Path zipFile) {
|
||||
return new Builder(zipFile, false);
|
||||
}
|
||||
|
||||
/// Creates a builder with archive creation and temporary-file updates enabled.
|
||||
public static Builder writable(Path zipFile) {
|
||||
return new Builder(zipFile, true).setUseTempFile(true);
|
||||
}
|
||||
|
||||
/// Opens an existing ZIP filesystem using the provider's default filename charset.
|
||||
public static FileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException {
|
||||
return createReadOnlyZipFileSystem(zipFile, null);
|
||||
}
|
||||
|
||||
public static FileSystem createReadOnlyZipFileSystem(Path zipFile, Charset charset) throws IOException {
|
||||
/// Opens an existing ZIP filesystem with the supplied filename charset,
|
||||
/// or the provider's default when `charset` is `null`.
|
||||
public static FileSystem createReadOnlyZipFileSystem(Path zipFile, @Nullable Charset charset) throws IOException {
|
||||
return createZipFileSystem(zipFile, false, false, charset);
|
||||
}
|
||||
|
||||
/// Opens or creates a ZIP filesystem with temporary-file updates and the
|
||||
/// provider's default filename charset.
|
||||
public static FileSystem createWritableZipFileSystem(Path zipFile) throws IOException {
|
||||
return createWritableZipFileSystem(zipFile, null);
|
||||
}
|
||||
|
||||
public static FileSystem createWritableZipFileSystem(Path zipFile, Charset charset) throws IOException {
|
||||
/// Opens or creates a ZIP filesystem with temporary-file updates and the
|
||||
/// supplied filename charset, or the provider's default when `charset` is `null`.
|
||||
public static FileSystem createWritableZipFileSystem(Path zipFile, @Nullable Charset charset) throws IOException {
|
||||
return createZipFileSystem(zipFile, true, true, charset);
|
||||
}
|
||||
|
||||
public static FileSystem createZipFileSystem(Path zipFile, boolean create, boolean useTempFile, Charset encoding) throws IOException {
|
||||
/// Opens a ZIP filesystem with the supplied provider options.
|
||||
///
|
||||
/// The caller must close the returned filesystem. This method does not detect
|
||||
/// filename encoding or enforce read-only access.
|
||||
///
|
||||
/// @param zipFile archive to open
|
||||
/// @param create whether a missing archive may be created
|
||||
/// @param useTempFile whether to use temporary files for entry updates
|
||||
/// @param encoding filename charset, or `null` for the provider's default
|
||||
/// @return newly opened filesystem
|
||||
/// @throws IOException if opening fails, including when the ZIP provider is unavailable
|
||||
public static FileSystem createZipFileSystem(Path zipFile, boolean create, boolean useTempFile, @Nullable Charset encoding) throws IOException {
|
||||
Map<String, Object> env = new HashMap<>();
|
||||
if (create)
|
||||
env.put("create", "true");
|
||||
@@ -245,58 +285,28 @@ public final class CompressingUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the text content of a file in zip.
|
||||
*
|
||||
* @param zipFile the zip file
|
||||
* @param name the location of the text in zip file, something like A/B/C/D.txt
|
||||
* @return the plain text content of given file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
*/
|
||||
/// Reads a ZIP entry as UTF-8 text and closes the archive reader.
|
||||
///
|
||||
/// @param zipFile archive to read using the reader's default filename charset
|
||||
/// @param name entry path, such as `A/B/C/D.txt`
|
||||
/// @return decoded entry contents, replacing malformed UTF-8 sequences
|
||||
/// @throws IOException if the archive or entry cannot be read
|
||||
/// @throws NullPointerException if the entry does not exist
|
||||
public static String readTextZipEntry(Path zipFile, String name) throws IOException {
|
||||
try (ZipArchiveReader s = new ZipArchiveReader(zipFile)) {
|
||||
return readTextZipEntry(s, name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the text content of a file in zip.
|
||||
*
|
||||
* @param zipFile the zip file
|
||||
* @param name the location of the text in zip file, something like A/B/C/D.txt
|
||||
* @return the plain text content of given file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
*/
|
||||
/// Reads a ZIP entry as UTF-8 text, closing its stream but leaving the reader open.
|
||||
///
|
||||
/// @param zipFile archive reader
|
||||
/// @param name entry path, such as `A/B/C/D.txt`
|
||||
/// @return decoded entry contents, replacing malformed UTF-8 sequences
|
||||
/// @throws IOException if the entry cannot be read
|
||||
/// @throws NullPointerException if the entry does not exist
|
||||
public static String readTextZipEntry(ZipArchiveReader zipFile, String name) throws IOException {
|
||||
return IOUtils.readFullyAsString(zipFile.getInputStream(zipFile.getEntry(name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the text content of a file in zip.
|
||||
*
|
||||
* @param zipFile the zip file
|
||||
* @param name the location of the text in zip file, something like A/B/C/D.txt
|
||||
* @return the plain text content of given file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
*/
|
||||
public static String readTextZipEntry(Path zipFile, String name, Charset encoding) throws IOException {
|
||||
try (ZipArchiveReader s = openZipFile(zipFile, encoding)) {
|
||||
return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the text content of a file in zip.
|
||||
*
|
||||
* @param file the zip file
|
||||
* @param name the location of the text in zip file, something like A/B/C/D.txt
|
||||
* @return the plain text content of given file.
|
||||
*/
|
||||
public static Optional<String> readTextZipEntryQuietly(Path file, String name, Charset encoding) {
|
||||
try {
|
||||
return Optional.of(readTextZipEntry(file, name, encoding));
|
||||
} catch (IOException | NullPointerException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
*/
|
||||
public final class FileUtils {
|
||||
|
||||
public static final long KB = 1024;
|
||||
public static final long MB = KB * KB;
|
||||
public static final long GB = KB * MB;
|
||||
public static final long TB = KB * GB;
|
||||
|
||||
private FileUtils() {
|
||||
}
|
||||
|
||||
@@ -238,6 +243,18 @@ public final class FileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static String parseFileSize(long sizeInBytes) {
|
||||
if (sizeInBytes >= TB)
|
||||
return String.format(Locale.ROOT, "%.2f TiB", (float) sizeInBytes / TB);
|
||||
if (sizeInBytes >= GB)
|
||||
return String.format(Locale.ROOT, "%.2f GiB", (float) sizeInBytes / GB);
|
||||
if (sizeInBytes >= MB)
|
||||
return String.format(Locale.ROOT, "%.2f MiB", (float) sizeInBytes / MB);
|
||||
if (sizeInBytes >= KB)
|
||||
return String.format(Locale.ROOT, "%.2f KiB", (float) sizeInBytes / KB);
|
||||
return String.format(Locale.ROOT, "%d B", sizeInBytes);
|
||||
}
|
||||
|
||||
public static String readTextMaybeNativeEncoding(Path file) throws IOException {
|
||||
byte[] bytes = Files.readAllBytes(file);
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
|
||||
public final class Unzipper {
|
||||
@@ -34,7 +32,6 @@ public final class Unzipper {
|
||||
private boolean terminateIfSubDirectoryNotExists = false;
|
||||
private String subDirectory = "/";
|
||||
private EntryFilter filter;
|
||||
private Charset encoding = StandardCharsets.UTF_8;
|
||||
|
||||
/// Decompress the given zip file to a directory.
|
||||
///
|
||||
@@ -70,11 +67,6 @@ public final class Unzipper {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Unzipper setEncoding(Charset encoding) {
|
||||
this.encoding = encoding;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Unzipper setTerminateIfSubDirectoryNotExists() {
|
||||
this.terminateIfSubDirectoryNotExists = true;
|
||||
return this;
|
||||
@@ -92,7 +84,7 @@ public final class Unzipper {
|
||||
: new CopyOption[]{};
|
||||
|
||||
long entryCount = 0L;
|
||||
try (ZipArchiveReader reader = CompressingUtils.openZipFileWithPossibleEncoding(zipFile, encoding)) {
|
||||
try (ZipArchiveReader reader = CompressingUtils.openZipFile(zipFile)) {
|
||||
String pathPrefix = StringUtils.addSuffix(subDirectory, "/");
|
||||
|
||||
for (ZipArchiveEntry entry : reader.getEntries()) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/// Alternate for JavaFX version below 21
|
||||
@FunctionalInterface
|
||||
public interface Subscription {
|
||||
|
||||
Subscription EMPTY = () -> {};
|
||||
|
||||
static Subscription subscribe(Observable observable, Runnable invalidationSubscriber) {
|
||||
Objects.requireNonNull(invalidationSubscriber, "invalidationSubscriber cannot be null");
|
||||
InvalidationListener listener = obs -> invalidationSubscriber.run();
|
||||
|
||||
observable.addListener(listener);
|
||||
|
||||
return () -> observable.removeListener(listener);
|
||||
}
|
||||
|
||||
static Subscription combine(Subscription... subscriptions) {
|
||||
List<Subscription> list = List.of(subscriptions);
|
||||
|
||||
return () -> list.forEach(Subscription::unsubscribe);
|
||||
}
|
||||
|
||||
void unsubscribe();
|
||||
|
||||
default Subscription and(Subscription other) {
|
||||
Objects.requireNonNull(other, "other cannot be null");
|
||||
|
||||
return () -> {
|
||||
unsubscribe();
|
||||
other.unsubscribe();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,10 @@ public enum OperatingSystem {
|
||||
return this == LINUX || this == FREEBSD;
|
||||
}
|
||||
|
||||
public boolean isUnixLike() {
|
||||
return isLinuxOrBSD() || this == MACOS;
|
||||
}
|
||||
|
||||
public String getJavaExecutable() {
|
||||
return this == WINDOWS ? "java.exe" : "java";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.platform.macos;
|
||||
|
||||
import com.sun.jna.Library;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface LibSystem extends Library {
|
||||
/// The Objective-C runtime library instance.
|
||||
@Nullable LibSystem INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isMac()
|
||||
? NativeUtils.load("System", LibSystem.class)
|
||||
: null;
|
||||
|
||||
int geteuid();
|
||||
}
|
||||
@@ -711,7 +711,7 @@ public final class DefaultGameInstanceTest {
|
||||
/// @param manifest the MultiMC configuration
|
||||
/// @return the test modpack metadata
|
||||
private static Modpack createMultiMCModpack(MultiMCInstanceConfiguration manifest) {
|
||||
return new Modpack("Test", "", "", manifest.getGameVersion(), "", StandardCharsets.UTF_8, manifest) {
|
||||
return new Modpack("Test", "", "", manifest.getGameVersion(), "", manifest) {
|
||||
/// This test invokes the concrete installation task directly.
|
||||
@Override
|
||||
public Task<?> getInstallTask(
|
||||
|
||||
@@ -62,7 +62,7 @@ public final class CSVTableTest {
|
||||
table.set(1, 2, "c");
|
||||
table.set(3, 2, "d\"e\n");
|
||||
|
||||
assertEquals("a,b,,c\n,,,\n\"a,b\",c,,\"d\\\"e\\n\"\n", table.toString());
|
||||
assertEquals("a,b,,c\n,,,\n\"a,b\",c,,\"d\"\"e\n\"\n", table.toString());
|
||||
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user