feat: Mod dependencies & Mod downloads should be classified.

This commit is contained in:
huanghongxun
2021-09-20 17:35:49 +08:00
parent f2741f9725
commit 0b2e9c3f62
12 changed files with 272 additions and 134 deletions

View File

@@ -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;

View File

@@ -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) ->

View File

@@ -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;
}
} }
} }

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
} }

View File

@@ -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()
); );
} }

View File

@@ -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>>() {

View File

@@ -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()

View File

@@ -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