feat: Mod dependencies & Mod downloads should be classified.
This commit is contained in:
@@ -109,6 +109,7 @@ public class ComponentList extends Control {
|
|||||||
public void onExpand() {
|
public void onExpand() {
|
||||||
if (!expanded && lazyInitializer != null) {
|
if (!expanded && lazyInitializer != null) {
|
||||||
lazyInitializer.accept(this);
|
lazyInitializer.accept(this);
|
||||||
|
setNeedsLayout(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
expanded = true;
|
expanded = true;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import javafx.animation.Animation;
|
|||||||
import javafx.animation.KeyFrame;
|
import javafx.animation.KeyFrame;
|
||||||
import javafx.animation.KeyValue;
|
import javafx.animation.KeyValue;
|
||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
@@ -137,25 +138,31 @@ class ComponentListCell extends StackPane {
|
|||||||
|
|
||||||
setExpanded(!isExpanded());
|
setExpanded(!isExpanded());
|
||||||
|
|
||||||
double newAnimatedHeight = content.prefHeight(-1) * (isExpanded() ? 1 : -1);
|
|
||||||
double newHeight = isExpanded() ? getHeight() + newAnimatedHeight : prefHeight(-1);
|
|
||||||
double contentHeight = isExpanded() ? newAnimatedHeight : 0;
|
|
||||||
|
|
||||||
if (isExpanded()) {
|
if (isExpanded()) {
|
||||||
updateClip(newHeight);
|
|
||||||
list.onExpand();
|
list.onExpand();
|
||||||
|
list.layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
|
Platform.runLater(() -> {
|
||||||
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
|
double newAnimatedHeight = content.prefHeight(-1) * (isExpanded() ? 1 : -1);
|
||||||
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
|
double newHeight = isExpanded() ? getHeight() + newAnimatedHeight : prefHeight(-1);
|
||||||
));
|
double contentHeight = isExpanded() ? newAnimatedHeight : 0;
|
||||||
|
|
||||||
if (!isExpanded()) {
|
if (isExpanded()) {
|
||||||
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
|
updateClip(newHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
expandAnimation.play();
|
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
|
||||||
|
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
|
||||||
|
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!isExpanded()) {
|
||||||
|
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
expandAnimation.play();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expandedProperty().addListener((a, b, newValue) ->
|
expandedProperty().addListener((a, b, newValue) ->
|
||||||
|
|||||||
@@ -18,23 +18,24 @@
|
|||||||
package org.jackhuang.hmcl.ui.versions;
|
package org.jackhuang.hmcl.ui.versions;
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
import com.jfoenix.controls.JFXListView;
|
import com.jfoenix.controls.JFXScrollPane;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Control;
|
import javafx.scene.control.Control;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.Skin;
|
import javafx.scene.control.Skin;
|
||||||
import javafx.scene.control.SkinBase;
|
import javafx.scene.control.SkinBase;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.Priority;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import org.jackhuang.hmcl.game.GameVersion;
|
|
||||||
import org.jackhuang.hmcl.mod.DownloadManager;
|
import org.jackhuang.hmcl.mod.DownloadManager;
|
||||||
import org.jackhuang.hmcl.mod.ModManager;
|
import org.jackhuang.hmcl.mod.ModManager;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
@@ -45,29 +46,27 @@ import org.jackhuang.hmcl.task.Task;
|
|||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.construct.FloatListCell;
|
import org.jackhuang.hmcl.ui.construct.*;
|
||||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
|
||||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||||
|
import org.jackhuang.hmcl.util.SimpleMultimap;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.FormatStyle;
|
import java.time.format.FormatStyle;
|
||||||
import java.util.Comparator;
|
import java.util.*;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public class DownloadPage extends Control implements DecoratorPage {
|
public class DownloadPage extends Control implements DecoratorPage {
|
||||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||||
private final ListProperty<DownloadManager.Version> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
private final BooleanProperty loaded = new SimpleBooleanProperty(false);
|
||||||
private final BooleanProperty loading = new SimpleBooleanProperty(false);
|
private final BooleanProperty loading = new SimpleBooleanProperty(false);
|
||||||
private final BooleanProperty failed = new SimpleBooleanProperty(false);
|
private final BooleanProperty failed = new SimpleBooleanProperty(false);
|
||||||
private final DownloadManager.Mod addon;
|
private final DownloadManager.Mod addon;
|
||||||
@@ -76,6 +75,9 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
private final DownloadCallback callback;
|
private final DownloadCallback callback;
|
||||||
private final DownloadListPage page;
|
private final DownloadListPage page;
|
||||||
|
|
||||||
|
private List<DownloadManager.Mod> dependencies;
|
||||||
|
private SimpleMultimap<String, DownloadManager.Version> versions;
|
||||||
|
|
||||||
public DownloadPage(DownloadListPage page, DownloadManager.Mod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) {
|
public DownloadPage(DownloadListPage page, DownloadManager.Mod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.addon = addon;
|
this.addon = addon;
|
||||||
@@ -89,30 +91,57 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setFailed(false);
|
setFailed(false);
|
||||||
Task.supplyAsync(() -> {
|
|
||||||
if (StringUtils.isNotBlank(version.getVersion())) {
|
Task.allOf(
|
||||||
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
Task.supplyAsync(() -> addon.getData().loadDependencies()),
|
||||||
if (gameVersion.isPresent()) {
|
Task.supplyAsync(() -> {
|
||||||
return addon.getData().loadVersions()
|
Stream<DownloadManager.Version> versions = addon.getData().loadVersions();
|
||||||
.filter(file -> file.getGameVersions().contains(gameVersion.get()));
|
// if (StringUtils.isNotBlank(version.getVersion())) {
|
||||||
}
|
// Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
||||||
}
|
// if (gameVersion.isPresent()) {
|
||||||
return addon.getData().loadVersions();
|
// return sortVersions(
|
||||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
// .filter(file -> file.getGameVersions().contains(gameVersion.get())));
|
||||||
if (exception == null) {
|
// }
|
||||||
items.setAll(result
|
// }
|
||||||
.sorted(Comparator.comparing(DownloadManager.Version::getDatePublished).reversed())
|
return sortVersions(versions);
|
||||||
.collect(Collectors.toList()));
|
}))
|
||||||
setFailed(false);
|
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
} else {
|
if (exception == null) {
|
||||||
setFailed(true);
|
@SuppressWarnings("unchecked")
|
||||||
}
|
List<DownloadManager.Mod> dependencies = (List<DownloadManager.Mod>) result.get(0);
|
||||||
setLoading(false);
|
@SuppressWarnings("unchecked")
|
||||||
}).start();
|
SimpleMultimap<String, DownloadManager.Version> versions = (SimpleMultimap<String, DownloadManager.Version>) result.get(1);
|
||||||
|
|
||||||
|
this.dependencies = dependencies;
|
||||||
|
this.versions = versions;
|
||||||
|
|
||||||
|
loaded.set(true);
|
||||||
|
setFailed(false);
|
||||||
|
} else {
|
||||||
|
setFailed(true);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}).start();
|
||||||
|
|
||||||
this.state.set(State.fromTitle(addon.getTitle()));
|
this.state.set(State.fromTitle(addon.getTitle()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SimpleMultimap<String, DownloadManager.Version> sortVersions(Stream<DownloadManager.Version> versions) {
|
||||||
|
SimpleMultimap<String, DownloadManager.Version> classifiedVersions
|
||||||
|
= new SimpleMultimap<String, DownloadManager.Version>(HashMap::new, ArrayList::new);
|
||||||
|
versions.forEach(version -> {
|
||||||
|
for (String gameVersion : version.getGameVersions()) {
|
||||||
|
classifiedVersions.put(gameVersion, version);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (String gameVersion : classifiedVersions.keys()) {
|
||||||
|
List<DownloadManager.Version> versionList = (List<DownloadManager.Version>) classifiedVersions.get(gameVersion);
|
||||||
|
versionList.sort(Comparator.comparing(DownloadManager.Version::getDatePublished).reversed());
|
||||||
|
}
|
||||||
|
return classifiedVersions;
|
||||||
|
}
|
||||||
|
|
||||||
public DownloadManager.Mod getAddon() {
|
public DownloadManager.Mod getAddon() {
|
||||||
return addon;
|
return addon;
|
||||||
}
|
}
|
||||||
@@ -186,49 +215,79 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
protected ModDownloadPageSkin(DownloadPage control) {
|
protected ModDownloadPageSkin(DownloadPage control) {
|
||||||
super(control);
|
super(control);
|
||||||
|
|
||||||
BorderPane pane = new BorderPane();
|
VBox pane = new VBox(8);
|
||||||
|
pane.getStyleClass().add("gray-background");
|
||||||
|
pane.setPadding(new Insets(10));
|
||||||
|
ScrollPane scrollPane = new ScrollPane(pane);
|
||||||
|
JFXScrollPane.smoothScrolling(scrollPane);
|
||||||
|
scrollPane.setFitToWidth(true);
|
||||||
|
scrollPane.setFitToHeight(true);
|
||||||
|
|
||||||
HBox descriptionPane = new HBox(8);
|
HBox descriptionPane = new HBox(8);
|
||||||
descriptionPane.setAlignment(Pos.CENTER);
|
descriptionPane.setAlignment(Pos.CENTER);
|
||||||
pane.setTop(descriptionPane);
|
pane.getChildren().add(descriptionPane);
|
||||||
descriptionPane.getStyleClass().add("card");
|
descriptionPane.getStyleClass().add("card-non-transparent");
|
||||||
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
|
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
|
||||||
|
{
|
||||||
ImageView imageView = new ImageView();
|
ImageView imageView = new ImageView();
|
||||||
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
|
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
|
||||||
imageView.setImage(new Image(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true));
|
imageView.setImage(new Image(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true));
|
||||||
}
|
|
||||||
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
|
|
||||||
|
|
||||||
TwoLineListItem content = new TwoLineListItem();
|
|
||||||
HBox.setHgrow(content, Priority.ALWAYS);
|
|
||||||
ModTranslations.Mod mod = ModTranslations.getModByCurseForgeId(getSkinnable().addon.getSlug());
|
|
||||||
content.setTitle(mod != null ? mod.getDisplayName() : getSkinnable().addon.getTitle());
|
|
||||||
content.setSubtitle(getSkinnable().addon.getDescription());
|
|
||||||
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
|
|
||||||
.map(category -> getSkinnable().page.getLocalizedCategory(category))
|
|
||||||
.collect(Collectors.toList()));
|
|
||||||
descriptionPane.getChildren().add(content);
|
|
||||||
|
|
||||||
if (getSkinnable().mod != null) {
|
|
||||||
JFXHyperlink openMcmodButton = new JFXHyperlink(i18n("mods.mcmod"));
|
|
||||||
openMcmodButton.setOnAction(e -> FXUtils.openLink(ModManager.getMcmodUrl(getSkinnable().mod.getMcmod())));
|
|
||||||
descriptionPane.getChildren().add(openMcmodButton);
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) {
|
|
||||||
JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
|
|
||||||
openMcbbsButton.setOnAction(e -> FXUtils.openLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs())));
|
|
||||||
descriptionPane.getChildren().add(openMcbbsButton);
|
|
||||||
}
|
}
|
||||||
|
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
|
||||||
|
|
||||||
|
TwoLineListItem content = new TwoLineListItem();
|
||||||
|
HBox.setHgrow(content, Priority.ALWAYS);
|
||||||
|
ModTranslations.Mod mod = ModTranslations.getModByCurseForgeId(getSkinnable().addon.getSlug());
|
||||||
|
content.setTitle(mod != null ? mod.getDisplayName() : getSkinnable().addon.getTitle());
|
||||||
|
content.setSubtitle(getSkinnable().addon.getDescription());
|
||||||
|
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
|
||||||
|
.map(category -> getSkinnable().page.getLocalizedCategory(category))
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
descriptionPane.getChildren().add(content);
|
||||||
|
|
||||||
|
if (getSkinnable().mod != null) {
|
||||||
|
JFXHyperlink openMcmodButton = new JFXHyperlink(i18n("mods.mcmod"));
|
||||||
|
openMcmodButton.setOnAction(e -> FXUtils.openLink(ModManager.getMcmodUrl(getSkinnable().mod.getMcmod())));
|
||||||
|
descriptionPane.getChildren().add(openMcmodButton);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) {
|
||||||
|
JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
|
||||||
|
openMcbbsButton.setOnAction(e -> FXUtils.openLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs())));
|
||||||
|
descriptionPane.getChildren().add(openMcbbsButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage());
|
||||||
|
openUrlButton.setOnAction(e -> FXUtils.openLink(getSkinnable().addon.getPageUrl()));
|
||||||
|
descriptionPane.getChildren().add(openUrlButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage());
|
{
|
||||||
openUrlButton.setOnAction(e -> FXUtils.openLink(getSkinnable().addon.getPageUrl()));
|
ComponentList dependencyPane = new ComponentList();
|
||||||
descriptionPane.getChildren().add(openUrlButton);
|
dependencyPane.getStyleClass().add("no-padding");
|
||||||
|
|
||||||
|
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
|
||||||
|
if (loaded) {
|
||||||
|
dependencyPane.getContent().setAll(control.dependencies.stream()
|
||||||
|
.map(dependency -> new DependencyModItem(getSkinnable().page, dependency, control.version, control.callback))
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Node title = ComponentList.createComponentListTitle(i18n("mods.dependencies"));
|
||||||
|
|
||||||
|
BooleanBinding show = Bindings.createBooleanBinding(() -> !control.dependencies.isEmpty(), control.loaded);
|
||||||
|
title.managedProperty().bind(show);
|
||||||
|
title.visibleProperty().bind(show);
|
||||||
|
dependencyPane.managedProperty().bind(show);
|
||||||
|
dependencyPane.visibleProperty().bind(show);
|
||||||
|
|
||||||
|
pane.getChildren().addAll(title, dependencyPane);
|
||||||
|
}
|
||||||
|
|
||||||
SpinnerPane spinnerPane = new SpinnerPane();
|
SpinnerPane spinnerPane = new SpinnerPane();
|
||||||
pane.setCenter(spinnerPane);
|
VBox.setVgrow(spinnerPane, Priority.ALWAYS);
|
||||||
|
pane.getChildren().add(spinnerPane);
|
||||||
{
|
{
|
||||||
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
|
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
|
||||||
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
|
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
|
||||||
@@ -239,60 +298,101 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
}
|
}
|
||||||
}, getSkinnable().failedProperty()));
|
}, getSkinnable().failedProperty()));
|
||||||
|
|
||||||
JFXListView<DownloadManager.Version> listView = new JFXListView<>();
|
ComponentList list = new ComponentList();
|
||||||
spinnerPane.setContent(listView);
|
StackPane.setAlignment(list, Pos.TOP_CENTER);
|
||||||
Bindings.bindContent(listView.getItems(), getSkinnable().items);
|
spinnerPane.setContent(list);
|
||||||
listView.setCellFactory(x -> new FloatListCell<DownloadManager.Version>(listView) {
|
|
||||||
TwoLineListItem content = new TwoLineListItem();
|
|
||||||
StackPane graphicPane = new StackPane();
|
|
||||||
JFXButton saveAsButton = new JFXButton();
|
|
||||||
|
|
||||||
{
|
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
|
||||||
HBox container = new HBox(8);
|
if (control.versions == null) return;
|
||||||
container.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
pane.getChildren().add(container);
|
|
||||||
|
|
||||||
saveAsButton.getStyleClass().add("toggle-icon4");
|
for (String gameVersion : control.versions.keys().stream()
|
||||||
saveAsButton.setGraphic(SVG.contentSaveMoveOutline(Theme.blackFillBinding(), -1, -1));
|
.sorted(VersionNumber.VERSION_COMPARATOR.reversed())
|
||||||
|
.collect(Collectors.toList())) {
|
||||||
|
ComponentList sublist = new ComponentList();
|
||||||
|
sublist.setLazyInitializer(self -> {
|
||||||
|
self.getContent().setAll(control.versions.get(gameVersion).stream()
|
||||||
|
.map(version -> new ModItem(version, control))
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
});
|
||||||
|
sublist.getStyleClass().add("no-padding");
|
||||||
|
sublist.setTitle(gameVersion);
|
||||||
|
|
||||||
HBox.setHgrow(content, Priority.ALWAYS);
|
list.getContent().add(sublist);
|
||||||
container.getChildren().setAll(graphicPane, content, saveAsButton);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void updateControl(DownloadManager.Version dataItem, boolean empty) {
|
|
||||||
if (empty) return;
|
|
||||||
content.setTitle(dataItem.getName());
|
|
||||||
content.setSubtitle(FORMATTER.format(dataItem.getDatePublished()));
|
|
||||||
content.getTags().setAll(dataItem.getGameVersions());
|
|
||||||
saveAsButton.setOnMouseClicked(e -> getSkinnable().saveAs(dataItem));
|
|
||||||
|
|
||||||
switch (dataItem.getVersionType()) {
|
|
||||||
case Release:
|
|
||||||
graphicPane.getChildren().setAll(SVG.releaseCircleOutline(Theme.blackFillBinding(), 24, 24));
|
|
||||||
content.getTags().add(i18n("version.game.release"));
|
|
||||||
break;
|
|
||||||
case Beta:
|
|
||||||
graphicPane.getChildren().setAll(SVG.betaCircleOutline(Theme.blackFillBinding(), 24, 24));
|
|
||||||
content.getTags().add(i18n("version.game.snapshot"));
|
|
||||||
break;
|
|
||||||
case Alpha:
|
|
||||||
graphicPane.getChildren().setAll(SVG.alphaCircleOutline(Theme.blackFillBinding(), 24, 24));
|
|
||||||
content.getTags().add(i18n("version.game.snapshot"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listView.setOnMouseClicked(e -> {
|
|
||||||
if (listView.getSelectionModel().getSelectedIndex() < 0)
|
|
||||||
return;
|
|
||||||
DownloadManager.Version selectedItem = listView.getSelectionModel().getSelectedItem();
|
|
||||||
getSkinnable().download(selectedItem);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildren().setAll(pane);
|
getChildren().setAll(scrollPane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DependencyModItem extends StackPane {
|
||||||
|
|
||||||
|
DependencyModItem(DownloadListPage page, DownloadManager.Mod addon, Profile.ProfileVersion version, DownloadCallback callback) {
|
||||||
|
HBox pane = new HBox(8);
|
||||||
|
pane.setPadding(new Insets(8));
|
||||||
|
pane.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
TwoLineListItem content = new TwoLineListItem();
|
||||||
|
HBox.setHgrow(content, Priority.ALWAYS);
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
pane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
|
||||||
|
|
||||||
|
RipplerContainer container = new RipplerContainer(pane);
|
||||||
|
container.setOnMouseClicked(e -> Controllers.navigate(new DownloadPage(page, addon, version, callback)));
|
||||||
|
getChildren().setAll(container);
|
||||||
|
|
||||||
|
ModTranslations.Mod mod = ModTranslations.getModByCurseForgeId(addon.getSlug());
|
||||||
|
content.setTitle(mod != null ? mod.getDisplayName() : addon.getTitle());
|
||||||
|
content.setSubtitle(addon.getDescription());
|
||||||
|
content.getTags().setAll(addon.getCategories().stream()
|
||||||
|
.map(page::getLocalizedCategory)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(addon.getIconUrl())) {
|
||||||
|
imageView.setImage(new Image(addon.getIconUrl(), 40, 40, true, true, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ModItem extends StackPane {
|
||||||
|
ModItem(DownloadManager.Version dataItem, DownloadPage selfPage) {
|
||||||
|
HBox pane = new HBox(8);
|
||||||
|
pane.setPadding(new Insets(8));
|
||||||
|
pane.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
TwoLineListItem content = new TwoLineListItem();
|
||||||
|
StackPane graphicPane = new StackPane();
|
||||||
|
JFXButton saveAsButton = new JFXButton();
|
||||||
|
|
||||||
|
RipplerContainer container = new RipplerContainer(pane);
|
||||||
|
container.setOnMouseClicked(e -> {
|
||||||
|
selfPage.download(dataItem);
|
||||||
|
});
|
||||||
|
getChildren().setAll(container);
|
||||||
|
|
||||||
|
saveAsButton.getStyleClass().add("toggle-icon4");
|
||||||
|
saveAsButton.setGraphic(SVG.contentSaveMoveOutline(Theme.blackFillBinding(), -1, -1));
|
||||||
|
|
||||||
|
HBox.setHgrow(content, Priority.ALWAYS);
|
||||||
|
pane.getChildren().setAll(graphicPane, content, saveAsButton);
|
||||||
|
|
||||||
|
content.setTitle(dataItem.getName());
|
||||||
|
content.setSubtitle(FORMATTER.format(dataItem.getDatePublished()));
|
||||||
|
saveAsButton.setOnMouseClicked(e -> selfPage.saveAs(dataItem));
|
||||||
|
|
||||||
|
switch (dataItem.getVersionType()) {
|
||||||
|
case Release:
|
||||||
|
graphicPane.getChildren().setAll(SVG.releaseCircleOutline(Theme.blackFillBinding(), 24, 24));
|
||||||
|
content.getTags().add(i18n("version.game.release"));
|
||||||
|
break;
|
||||||
|
case Beta:
|
||||||
|
graphicPane.getChildren().setAll(SVG.betaCircleOutline(Theme.blackFillBinding(), 24, 24));
|
||||||
|
content.getTags().add(i18n("version.game.snapshot"));
|
||||||
|
break;
|
||||||
|
case Alpha:
|
||||||
|
graphicPane.getChildren().setAll(SVG.alphaCircleOutline(Theme.blackFillBinding(), 24, 24));
|
||||||
|
content.getTags().add(i18n("version.game.snapshot"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ public class GameListPage extends ListPageBase<GameListItem> implements Decorato
|
|||||||
|
|
||||||
Profiles.registerVersionsListener(this::loadVersions);
|
Profiles.registerVersionsListener(this::loadVersions);
|
||||||
|
|
||||||
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadVersions(Profile profile) {
|
private void loadVersions(Profile profile) {
|
||||||
|
|||||||
@@ -533,6 +533,7 @@ mods.add.success=Successfully installed mods %s.
|
|||||||
mods.category=Category
|
mods.category=Category
|
||||||
mods.choose_mod=Choose your mods
|
mods.choose_mod=Choose your mods
|
||||||
mods.curseforge=CurseForge
|
mods.curseforge=CurseForge
|
||||||
|
mods.dependencies=Dependencies
|
||||||
mods.disable=Disable
|
mods.disable=Disable
|
||||||
mods.download=Mod Downloads
|
mods.download=Mod Downloads
|
||||||
mods.download.title=Mod Downloads - %1s
|
mods.download.title=Mod Downloads - %1s
|
||||||
|
|||||||
@@ -533,6 +533,7 @@ mods.add.success=成功新增模組 %s。
|
|||||||
mods.category=類別
|
mods.category=類別
|
||||||
mods.choose_mod=選擇模組
|
mods.choose_mod=選擇模組
|
||||||
mods.curseforge=CurseForge
|
mods.curseforge=CurseForge
|
||||||
|
mods.dependencies=前置 Mod
|
||||||
mods.disable=停用
|
mods.disable=停用
|
||||||
mods.download=模組下載
|
mods.download=模組下載
|
||||||
mods.download.title=模組下載 - %1s
|
mods.download.title=模組下載 - %1s
|
||||||
|
|||||||
@@ -533,6 +533,7 @@ mods.add.success=成功添加模组 %s。
|
|||||||
mods.category=类别
|
mods.category=类别
|
||||||
mods.choose_mod=选择模组
|
mods.choose_mod=选择模组
|
||||||
mods.curseforge=CurseForge
|
mods.curseforge=CurseForge
|
||||||
|
mods.dependencies=前置 Mod
|
||||||
mods.disable=禁用
|
mods.disable=禁用
|
||||||
mods.download=模组下载
|
mods.download=模组下载
|
||||||
mods.download.title=模组下载 - %1s
|
mods.download.title=模组下载 - %1s
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public final class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public interface IMod {
|
public interface IMod {
|
||||||
|
List<Mod> loadDependencies() throws IOException;
|
||||||
Stream<Version> loadVersions() throws IOException;
|
Stream<Version> loadVersions() throws IOException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ import org.jackhuang.hmcl.util.Immutable;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@@ -159,6 +161,19 @@ public class CurseAddon implements DownloadManager.IMod {
|
|||||||
return isExperimental;
|
return isExperimental;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DownloadManager.Mod> loadDependencies() throws IOException {
|
||||||
|
Set<Integer> dependencies = latestFiles.stream()
|
||||||
|
.flatMap(latestFile -> latestFile.getDependencies().stream())
|
||||||
|
.map(Dependency::getAddonId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
List<DownloadManager.Mod> mods = new ArrayList<>();
|
||||||
|
for (int dependencyId : dependencies) {
|
||||||
|
mods.add(CurseModManager.getAddon(dependencyId).toMod());
|
||||||
|
}
|
||||||
|
return mods;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<DownloadManager.Version> loadVersions() throws IOException {
|
public Stream<DownloadManager.Version> loadVersions() throws IOException {
|
||||||
return CurseModManager.getFiles(this).stream()
|
return CurseModManager.getFiles(this).stream()
|
||||||
@@ -485,7 +500,7 @@ public class CurseAddon implements DownloadManager.IMod {
|
|||||||
versionType,
|
versionType,
|
||||||
new DownloadManager.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
|
new DownloadManager.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
|
||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
gameVersion,
|
gameVersion.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
|
||||||
Collections.emptyList()
|
Collections.emptyList()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ public final class CurseModManager {
|
|||||||
}.getType());
|
}.getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CurseAddon getAddon(int id) throws IOException {
|
||||||
|
String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/addon/" + id));
|
||||||
|
return JsonUtils.fromNonNullJson(response, CurseAddon.class);
|
||||||
|
}
|
||||||
|
|
||||||
public static List<CurseAddon.LatestFile> getFiles(CurseAddon addon) throws IOException {
|
public static List<CurseAddon.LatestFile> getFiles(CurseAddon addon) throws IOException {
|
||||||
String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/addon/" + addon.getId() + "/files"));
|
String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/addon/" + addon.getId() + "/files"));
|
||||||
return JsonUtils.fromNonNullJson(response, new TypeToken<List<CurseAddon.LatestFile>>() {
|
return JsonUtils.fromNonNullJson(response, new TypeToken<List<CurseAddon.LatestFile>>() {
|
||||||
|
|||||||
@@ -418,6 +418,11 @@ public final class Modrinth {
|
|||||||
return latestVersion;
|
return latestVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DownloadManager.Mod> loadDependencies() throws IOException {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<DownloadManager.Version> loadVersions() throws IOException {
|
public Stream<DownloadManager.Version> loadVersions() throws IOException {
|
||||||
return Modrinth.getFiles(this).stream()
|
return Modrinth.getFiles(this).stream()
|
||||||
|
|||||||
@@ -865,7 +865,7 @@ public abstract class Task<T> {
|
|||||||
* @param tasks the Tasks
|
* @param tasks the Tasks
|
||||||
* @return a new Task that is completed when all of the given Tasks complete
|
* @return a new Task that is completed when all of the given Tasks complete
|
||||||
*/
|
*/
|
||||||
public static Task<Void> allOf(Task<?>... tasks) {
|
public static Task<List<Object>> allOf(Task<?>... tasks) {
|
||||||
return allOf(Arrays.asList(tasks));
|
return allOf(Arrays.asList(tasks));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -880,14 +880,15 @@ public abstract class Task<T> {
|
|||||||
* @param tasks the Tasks
|
* @param tasks the Tasks
|
||||||
* @return a new Task that is completed when all of the given Tasks complete
|
* @return a new Task that is completed when all of the given Tasks complete
|
||||||
*/
|
*/
|
||||||
public static Task<Void> allOf(Collection<Task<?>> tasks) {
|
public static Task<List<Object>> allOf(Collection<Task<?>> tasks) {
|
||||||
return new Task<Void>() {
|
return new Task<List<Object>>() {
|
||||||
{
|
{
|
||||||
setSignificance(TaskSignificance.MINOR);
|
setSignificance(TaskSignificance.MINOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() {
|
public void execute() {
|
||||||
|
setResult(tasks.stream().map(Task::getResult).collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user