feat: download mods and modpacks from CurseForge.

This commit is contained in:
Yuhui Huang
2021-08-03 22:07:19 +08:00
parent 7a20af462b
commit af7cf393dc
19 changed files with 833 additions and 188 deletions

View File

@@ -28,6 +28,7 @@ import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Task;
@@ -41,11 +42,15 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.versions.GameListPage;
import org.jackhuang.hmcl.ui.versions.ModDownloadListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
@@ -65,11 +70,26 @@ public final class Controllers {
private static Scene scene;
private static Stage stage;
private static VersionPage versionPage = null;
private static GameListPage gameListPage = null;
private static Lazy<VersionPage> versionPage = new Lazy<>(VersionPage::new);
private static Lazy<GameListPage> gameListPage = new Lazy<>(() -> {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
return gameListPage;
});
private static AuthlibInjectorServersPage serversPage = null;
private static RootPage rootPage;
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
private static DecoratorController decorator;
private static Lazy<ModDownloadListPage> modDownloadListPage = new Lazy<>(() ->
new ModDownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl) {
{
state.set(State.fromTitle(i18n("modpack.download")));
}
});
private Controllers() {
}
@@ -84,30 +104,17 @@ public final class Controllers {
// FXThread
public static VersionPage getVersionPage() {
if (versionPage == null)
versionPage = new VersionPage();
return versionPage;
return versionPage.get();
}
// FXThread
public static GameListPage getGameListPage() {
if (gameListPage == null) {
gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
}
return gameListPage;
return gameListPage.get();
}
// FXThread
public static RootPage getRootPage() {
if (rootPage == null)
rootPage = new RootPage();
return rootPage;
return rootPage.get();
}
// FXThread
@@ -117,6 +124,11 @@ public final class Controllers {
return serversPage;
}
// FXThread
public static ModDownloadListPage getModpackDownloadListPage() {
return modDownloadListPage.get();
}
// FXThread
public static DecoratorController getDecorator() {
return decorator;
@@ -233,6 +245,8 @@ public final class Controllers {
rootPage = null;
versionPage = null;
serversPage = null;
gameListPage = null;
modDownloadListPage = null;
decorator = null;
stage = null;
scene = null;

View File

@@ -24,6 +24,8 @@ import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -112,6 +114,15 @@ public final class FXUtils {
return onWeakChange(value, consumer);
}
public static WeakInvalidationListener observeWeak(Runnable runnable, Observable... observables) {
WeakInvalidationListener listener = new WeakInvalidationListener(observable -> runnable.run());
for (Observable observable : observables) {
observable.addListener(listener);
}
runnable.run();
return listener;
}
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
if (condition.getAsBoolean()) Platform.runLater(() -> runLaterIf(condition, runnable));
else runnable.run();

View File

@@ -246,4 +246,16 @@ public final class SVG {
public static Node texture(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z", fill, width, height);
}
public static Node alphaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", fill, width, height);
}
public static Node betaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z", fill, width, height);
}
public static Node releaseCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z", fill, width, height);
}
}

View File

@@ -70,7 +70,13 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
root.setCenter(scrollPane);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
FXUtils.onChangeAndOperate(skinnable.loadingProperty(), loading -> {
if (loading) {
spinnerPane.showSpinner();
} else {
spinnerPane.hideSpinner();
}
});
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);

View File

@@ -18,34 +18,30 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.*;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationHandler;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
@DefaultProperty("content")
public class SpinnerPane extends Control {
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
public void showSpinner() {
setLoading(true);
}
public void hideSpinner() {
setFailedReason(null);
setLoading(false);
}
@@ -73,6 +69,18 @@ public class SpinnerPane extends Control {
this.loading.set(loading);
}
public String getFailedReason() {
return failedReason.get();
}
public StringProperty failedReasonProperty() {
return failedReason;
}
public void setFailedReason(String failedReason) {
this.failedReason.set(failedReason);
}
@Override
protected Skin createDefaultSkin() {
return new Skin(this);
@@ -82,62 +90,57 @@ public class SpinnerPane extends Control {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
private final StackPane root = new StackPane();
private Timeline animation;
private final TransitionPane root = new TransitionPane();
private final StackPane failedPane = new StackPane();
private final Label failedReasonLabel = new Label();
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final WeakInvalidationListener observer;
protected Skin(SpinnerPane control) {
super(control);
root.getStyleClass().add("spinner-pane");
topPane.getChildren().setAll(spinner);
root.getChildren().setAll(contentPane, topPane);
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> contentPane.getChildren().setAll(newValue));
topPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
contentPane.getChildren().clear();
} else {
contentPane.getChildren().setAll(newValue);
}
});
getChildren().setAll(root);
FXUtils.onChangeAndOperate(getSkinnable().loadingProperty(), newValue -> {
Timeline prev = animation;
if (prev != null) prev.stop();
observer = FXUtils.observeWeak(() -> {
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE.getAnimationProducer());
} else {
root.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
}
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
}
}
AnimationProducer transition;
topPane.setMouseTransparent(true);
topPane.setVisible(true);
topPane.getStyleClass().add("gray-background");
if (newValue)
transition = ContainerAnimations.FADE_IN.getAnimationProducer();
else
transition = ContainerAnimations.FADE_OUT.getAnimationProducer();
public interface State {}
AnimationHandler handler = new AnimationHandler() {
@Override
public Duration getDuration() {
return Duration.millis(160);
}
public static class LoadedState implements State {}
@Override
public Pane getCurrentRoot() {
return root;
}
public static class LoadingState implements State {}
@Override
public Node getPreviousNode() {
return null;
}
public static class FailedState implements State {
private final String reason;
@Override
public Node getCurrentNode() {
return topPane;
}
};
public FailedState(String reason) {
this.reason = reason;
}
Timeline now = new Timeline();
now.getKeyFrames().addAll(transition.animate(handler));
now.getKeyFrames().add(new KeyFrame(handler.getDuration(), e -> {
topPane.setMouseTransparent(!newValue);
topPane.setVisible(newValue);
}));
now.play();
animation = now;
});
public String getReason() {
return reason;
}
}
}

View File

@@ -20,18 +20,22 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty tag = new SimpleStringProperty(this, "tag");
private final ObservableList<String> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final ObservableList<Label> tagLabels;
public TwoLineListItem(String titleString, String subtitleString) {
this();
@@ -48,15 +52,17 @@ public class TwoLineListItem extends VBox {
lblTitle.getStyleClass().add("title");
lblTitle.textProperty().bind(title);
Label lblTag = new Label();
lblTag.getStyleClass().add("tag");
lblTag.textProperty().bind(tag);
HBox tagContainer = new HBox();
lblTag.visibleProperty().bind(Bindings.createBooleanBinding(
() -> StringUtils.isNotBlank(tag.getValue()),
tag));
tagLabels = MappedObservableList.create(tags, tag -> {
Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
return tagLabel;
});
Bindings.bindContent(tagContainer.getChildren(), tagLabels);
firstLine.getChildren().addAll(lblTitle, lblTag);
firstLine.getChildren().addAll(lblTitle, tagContainer);
Label lblSubtitle = new Label();
lblSubtitle.getStyleClass().add("subtitle");
@@ -90,16 +96,8 @@ public class TwoLineListItem extends VBox {
this.subtitle.set(subtitle);
}
public String getTag() {
return tag.get();
}
public StringProperty tagProperty() {
return tag;
}
public void setTag(String tag) {
this.tag.set(tag);
public ObservableList<String> getTags() {
return tags;
}
@Override

View File

@@ -45,7 +45,9 @@ public class GameItemSkin extends SkinBase<GameItem> {
TwoLineListItem item = new TwoLineListItem();
item.titleProperty().bind(skinnable.titleProperty());
item.tagProperty().bind(skinnable.tagProperty());
FXUtils.onChangeAndOperate(skinnable.tagProperty(), tag -> {
item.getTags().setAll(tag);
});
item.subtitleProperty().bind(skinnable.subtitleProperty());
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);

View File

@@ -152,6 +152,13 @@ public class GameListPage extends ListPageBase<GameListItem> implements Decorato
installModpackItem.setLeftGraphic(VersionPage.wrap(SVG.pack(Theme.blackFillBinding(), 24, 24)));
installModpackItem.setOnAction(e -> Versions.importModpack());
AdvancedListItem downloadModpackItem = new AdvancedListItem();
downloadModpackItem.getStyleClass().add("navigation-drawer-item");
downloadModpackItem.setTitle(i18n("modpack.download"));
downloadModpackItem.setActionButtonVisible(false);
downloadModpackItem.setLeftGraphic(VersionPage.wrap(SVG.fire(Theme.blackFillBinding(), 24, 24)));
downloadModpackItem.setOnAction(e -> Versions.downloadModpack());
AdvancedListItem refreshItem = new AdvancedListItem();
refreshItem.getStyleClass().add("navigation-drawer-item");
refreshItem.setTitle(i18n("button.refresh"));
@@ -169,9 +176,10 @@ public class GameListPage extends ListPageBase<GameListItem> implements Decorato
AdvancedListBox bottomLeftCornerList = new AdvancedListBox()
.add(installNewGameItem)
.add(installModpackItem)
.add(downloadModpackItem)
.add(refreshItem)
.add(globalManageItem);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 4 + 12 * 2);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 5 + 12 * 2);
left.setBottom(bottomLeftCornerList);
}
}

View File

@@ -1,42 +1,74 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Control;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModDownloadListPage extends Control {
public class ModDownloadListPage extends Control implements DecoratorPage {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final BooleanProperty failed = new SimpleBooleanProperty(false);
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private final ListProperty<CurseAddon> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final ModDownloadPage.DownloadCallback callback;
/**
* @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MODPACK
* @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MOD
*/
private final int section;
private Profile profile;
private String version;
public ModDownloadListPage(int section) {
public ModDownloadListPage(int section, ModDownloadPage.DownloadCallback callback) {
this.section = section;
this.callback = callback;
}
public void loadVersion(Profile profile, String version) {
this.profile = profile;
this.version = version;
this.version.set(new Profile.ProfileVersion(profile, version));
setLoading(false);
setFailed(false);
@@ -66,17 +98,34 @@ public class ModDownloadListPage extends Control {
this.loading.set(loading);
}
public void search(String gameVersion, int category, int pageOffset, String searchFilter, int sort) {
public void search(String userGameVersion, int category, int pageOffset, String searchFilter, int sort) {
setLoading(true);
Task.supplyAsync(() -> CurseModManager.searchPaginated(gameVersion, category, section, pageOffset, searchFilter, sort))
.whenComplete(Schedulers.javafx(), (exception, result) -> {
File versionJar = StringUtils.isNotBlank(version.get().getVersion())
? version.get().getProfile().getRepository().getVersionJar(version.get().getVersion())
: null;
Task.supplyAsync(() -> {
String gameVersion;
if (StringUtils.isBlank(version.get().getVersion())) {
gameVersion = userGameVersion;
} else {
gameVersion = GameVersion.minecraftVersion(versionJar).orElse("");
}
return gameVersion;
}).thenApplyAsync(gameVersion -> {
return CurseModManager.searchPaginated(gameVersion, category, section, pageOffset, searchFilter, sort);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
setLoading(false);
if (exception == null) {
items.setAll(result);
} else {
failed.set(true);
}
});
}).start();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
@Override
@@ -90,10 +139,10 @@ public class ModDownloadListPage extends Control {
super(control);
VBox pane = new VBox();
pane.getStyleClass().add("card-list");
GridPane searchPane = new GridPane();
searchPane.getStyleClass().add("card");
searchPane.getStyleClass().addAll("card");
VBox.setMargin(searchPane, new Insets(10, 10, 0, 10));
ColumnConstraints column1 = new ColumnConstraints();
column1.setPercentWidth(50);
@@ -115,6 +164,16 @@ public class ModDownloadListPage extends Control {
gameVersionField.setPromptText(i18n("world.game_version"));
searchPane.add(gameVersionField, 1, 0);
FXUtils.onChangeAndOperate(getSkinnable().version, version -> {
searchPane.getChildren().remove(gameVersionField);
if (StringUtils.isNotBlank(version.getVersion())) {
GridPane.setColumnSpan(nameField, 2);
} else {
searchPane.add(gameVersionField, 1, 0);
GridPane.setColumnSpan(nameField, 1);
}
});
JFXTextField categoryField = new JFXTextField();
categoryField.setPromptText(i18n("mods.category"));
searchPane.add(categoryField, 0, 1);
@@ -130,22 +189,70 @@ public class ModDownloadListPage extends Control {
JFXButton searchButton = new JFXButton();
searchButton.setText(i18n("search"));
searchButton.setOnAction(e -> {
getSkinnable().search(gameVersionField.getText(), categoryField.getText(), 0, nameField.getText(), sortField.getText())
.whenComplete();
getSkinnable().search(gameVersionField.getText(), 0, 0, nameField.getText(), 0);
});
searchPane.add(searchButton, 0, 2);
vbox.getChildren().setAll(searchButton);
}
TransitionPane transitionPane = new TransitionPane();
SpinnerPane spinnerPane = new SpinnerPane();
{
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else {
return null;
}
}, getSkinnable().failedProperty()));
JFXListView<CurseAddon> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
listView.setOnMouseClicked(e -> {
if (listView.getSelectionModel().getSelectedIndex() < 0)
return;
CurseAddon selectedItem = listView.getSelectionModel().getSelectedItem();
Controllers.navigate(new ModDownloadPage(selectedItem, getSkinnable().version.get(), getSkinnable().callback));
});
listView.setCellFactory(x -> new FloatListCell<CurseAddon>() {
TwoLineListItem content = new TwoLineListItem();
ImageView imageView = new ImageView();
{
Region clippedContainer = (Region) listView.lookup(".clipped-container");
setPrefWidth(0);
HBox container = new HBox(8);
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
}
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
}
@Override
protected void updateControl(CurseAddon dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getName());
content.setSubtitle(dataItem.getSummary());
content.getTags().setAll(dataItem.getCategories().stream()
.map(category -> i18n("curse.category." + category.getCategoryId()))
.collect(Collectors.toList()));
for (CurseAddon.Attachment attachment : dataItem.getAttachments()) {
if (attachment.isDefault()) {
imageView.setImage(new Image(attachment.getThumbnailUrl(), 40, 40, true, true, true));
}
}
}
});
}
pane.getChildren().setAll(searchPane, transitionPane);
pane.getChildren().setAll(searchPane, spinnerPane);
getChildren().setAll(pane);
}

View File

@@ -1,22 +1,127 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import org.jackhuang.hmcl.game.Version;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModDownloadPage extends Control implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final ListProperty<CurseAddon.LatestFile> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final BooleanProperty failed = new SimpleBooleanProperty(false);
private final CurseAddon addon;
private final Version version;
private final Profile.ProfileVersion version;
private final DownloadCallback callback;
public ModDownloadPage(CurseAddon addon, Version version) {
public ModDownloadPage(CurseAddon addon, Profile.ProfileVersion version, DownloadCallback callback) {
this.addon = addon;
this.version = version;
this.callback = callback;
File versionJar = StringUtils.isNotBlank(version.getVersion())
? version.getProfile().getRepository().getVersionJar(version.getVersion())
: null;
Task.runAsync(() -> {
if (StringUtils.isNotBlank(version.getVersion())) {
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
if (gameVersion.isPresent()) {
List<CurseAddon.LatestFile> files = CurseModManager.getFiles(addon);
items.setAll(files.stream()
.filter(file -> file.getGameVersion().contains(gameVersion.get()))
.collect(Collectors.toList()));
return;
}
}
List<CurseAddon.LatestFile> files = CurseModManager.getFiles(addon);
items.setAll(files);
}).start();
this.state.set(State.fromTitle(i18n("mods.download.title", addon.getName())));
}
public CurseAddon getAddon() {
return addon;
}
public Profile.ProfileVersion getVersion() {
return version;
}
public boolean isLoading() {
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public boolean isFailed() {
return failed.get();
}
public BooleanProperty failedProperty() {
return failed;
}
public void setFailed(boolean failed) {
this.failed.set(failed);
}
public void download(CurseAddon.LatestFile file) {
this.callback.download(version.getProfile(), version.getVersion(), file);
}
@Override
@@ -33,6 +138,108 @@ public class ModDownloadPage extends Control implements DecoratorPage {
protected ModDownloadPageSkin(ModDownloadPage control) {
super(control);
BorderPane pane = new BorderPane();
HBox descriptionPane = new HBox(8);
descriptionPane.setAlignment(Pos.CENTER);
pane.setTop(descriptionPane);
descriptionPane.getStyleClass().add("card");
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
content.setTitle(getSkinnable().addon.getName());
content.setSubtitle(getSkinnable().addon.getSummary());
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
.map(category -> i18n("curse.category." + category.getCategoryId()))
.collect(Collectors.toList()));
ImageView imageView = new ImageView();
for (CurseAddon.Attachment attachment : getSkinnable().addon.getAttachments()) {
if (attachment.isDefault()) {
imageView.setImage(new Image(attachment.getThumbnailUrl(), 40, 40, true, true, true));
}
}
JFXButton openUrlButton = new JFXButton();
openUrlButton.getStyleClass().add("toggle-icon4");
openUrlButton.setGraphic(SVG.launchOutline(Theme.blackFillBinding(), -1, -1));
openUrlButton.setOnAction(e -> FXUtils.openLink(getSkinnable().addon.getWebsiteUrl()));
descriptionPane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content, openUrlButton);
SpinnerPane spinnerPane = new SpinnerPane();
pane.setCenter(spinnerPane);
{
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else {
return null;
}
}, getSkinnable().failedProperty()));
JFXListView<CurseAddon.LatestFile> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
listView.setCellFactory(x -> new FloatListCell<CurseAddon.LatestFile>() {
TwoLineListItem content = new TwoLineListItem();
StackPane graphicPane = new StackPane();
{
Region clippedContainer = (Region)listView.lookup(".clipped-container");
setPrefWidth(0);
HBox container = new HBox(8);
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
}
container.getChildren().setAll(graphicPane, content);
}
@Override
protected void updateControl(CurseAddon.LatestFile dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getDisplayName());
content.getTags().setAll(dataItem.getGameVersion());
switch (dataItem.getReleaseType()) {
case 1: // release
graphicPane.getChildren().setAll(SVG.releaseCircleOutline(Theme.blackFillBinding(), 24, 24));
content.getTags().add(i18n("version.game.release"));
break;
case 2: // beta
graphicPane.getChildren().setAll(SVG.betaCircleOutline(Theme.blackFillBinding(), 24, 24));
content.getTags().add(i18n("version.game.snapshot"));
break;
case 3: // 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;
CurseAddon.LatestFile selectedItem = listView.getSelectionModel().getSelectedItem();
getSkinnable().download(selectedItem);
});
}
getChildren().setAll(pane);
}
}
public interface DownloadCallback {
void download(Profile profile, @Nullable String version, CurseAddon.LatestFile file);
}
}

View File

@@ -86,7 +86,6 @@ class ModListPageSkin extends SkinBase<ModListPage> {
BooleanProperty booleanProperty;
{
Region clippedContainer = (Region)listView.lookup(".clipped-container");
setPrefWidth(0);
HBox container = new HBox(8);

View File

@@ -27,8 +27,14 @@ import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -36,14 +42,17 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class VersionPage extends Control implements DecoratorPage {
public class VersionPage extends Control implements DecoratorPage, ModDownloadPage.DownloadCallback {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final BooleanProperty loading = new SimpleBooleanProperty();
private final TabHeader.Tab versionSettingsTab = new TabHeader.Tab("versionSettingsTab");
@@ -51,7 +60,7 @@ public class VersionPage extends Control implements DecoratorPage {
private final TabHeader.Tab modListTab = new TabHeader.Tab("modListTab");
private final ModListPage modListPage = new ModListPage(modListTab);
private final TabHeader.Tab curseModListTab = new TabHeader.Tab("modListTab");
private final ModDownloadListPage curseModListPage = new ModDownloadListPage();
private final ModDownloadListPage curseModListPage = new ModDownloadListPage(CurseModManager.SECTION_MOD, this);
private final TabHeader.Tab installerListTab = new TabHeader.Tab("installerListTab");
private final InstallerListPage installerListPage = new InstallerListPage();
private final TabHeader.Tab worldListTab = new TabHeader.Tab("worldList");
@@ -182,6 +191,28 @@ public class VersionPage extends Control implements DecoratorPage {
return state.getReadOnlyProperty();
}
@Override
public void download(Profile profile, @Nullable String version, CurseAddon.LatestFile file) {
if (version == null) {
throw new InternalError();
}
Path dest = profile.getRepository().getRunDirectory(version).toPath().resolve("mods").resolve(file.getFileName());
TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getDownloadUrl()), dest.toFile());
task.setName(file.getDisplayName());
return task;
}).executor(false);
downloadingPane.setExecutor(executor, true);
Controllers.dialog(downloadingPane);
executor.start();
}
public static class Skin extends SkinBase<VersionPage> {
private JFXPopup listViewItemPopup;

View File

@@ -22,9 +22,11 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -43,9 +45,13 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.download.LocalModpackPage.MODPACK_FILE;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Versions {
@@ -66,6 +72,41 @@ public final class Versions {
}
}
public static void downloadModpack() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getModpackDownloadListPage().loadVersion(profile, null);
Controllers.navigate(Controllers.getModpackDownloadListPage());
}
}
public static void downloadModpackImpl(Profile profile, String version, CurseAddon.LatestFile file) {
Path modpack;
URL downloadURL;
try {
modpack = Files.createTempFile("modpack", ".zip");
downloadURL = new URL(file.getDownloadUrl());
} catch (IOException e) {
Controllers.dialog(
i18n("install.failed.downloading.detail", file.getDownloadUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed"), MessageDialogPane.MessageType.ERROR);
return;
}
Controllers.taskDialog(
new FileDownloadTask(downloadURL, modpack.toFile())
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack.toFile()));
} else {
Controllers.dialog(
i18n("install.failed.downloading.detail", file.getDownloadUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed"), MessageDialogPane.MessageType.ERROR);
}
}).executor(true),
i18n("message.downloading")
);
}
public static void deleteVersion(Profile profile, String version) {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == GameDirectoryType.VERSION_FOLDER;
boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported();

View File

@@ -0,0 +1,44 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Non thread-safe lazy initialization wrapper.
*
* @param <T> value type
*/
public class Lazy<T> {
private Supplier<T> supplier;
private T value = null;
public Lazy(Supplier<T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
public T get() {
if (value == null) {
value = Objects.requireNonNull(supplier.get());
supplier = null;
}
return value;
}
}

View File

@@ -773,10 +773,36 @@
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
}
.depth-0 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0), 0, 0, 0, 0);
}
.depth-1 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
}
.depth-2 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 15, 0.16, 0, 4);
}
.depth-3 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 20, 0.19, 0, 6);
}
.depth-4 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 25, 0.25, 0, 8);
}
.depth-5 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 30, 0.3, 0, 10);
}
.card {
-fx-background-color: rgba(255, 255, 255, 0.8);
-fx-background-radius: 4;
-fx-padding: 8px;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
}
.card:selected {

View File

@@ -98,6 +98,64 @@ color.custom=Custom Color
crash.NoClassDefFound=Please verify that the "Hello Minecraft! Launcher" software is not corrupted.
crash.user_fault=Your OS or Java environment may not be properly installed which may result in a crash, please check your Java Runtime Environment or your computer!
curse.category.4474=Sci-Fi
curse.category.4481=Small / Light
curse.category.4483=Combat
curse.category.4477=Mini Game
curse.category.4478=Quests
curse.category.4484=Multiplayer
curse.category.4476=Exploration
curse.category.4736=Skyblock
curse.category.4475=Adventure and RPG
curse.category.4487=FTB
curse.category.4480=Map Based
curse.category.4479=Hardcore
curse.category.4482=Extra Large
curse.category.4472=Tech
curse.category.4473=Magic
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.423=Map and Information
curse.category.426=Addons
curse.category.434=Armor, Tools, and Weapons
curse.category.409=Structures
curse.category.4485=Blood Magic
curse.category.420=Storage
curse.category.429=Industrial Craft
curse.category.419=Magic
curse.category.412=Technology
curse.category.4557=Redstone
curse.category.428=Tinker's Construct
curse.category.414=Player Transport
curse.category.4486=Lucky Blocks
curse.category.432=Buildcraft
curse.category.418=Genetics
curse.category.4671=Twitch Integration
curse.category.408=Ores and Resources
curse.category.4773=CraftTweaker
curse.category.430=Thaumcraft
curse.category.422=Adventure and RPG
curse.category.413=Processing
curse.category.417=Energy
curse.category.415=Energy, Fluid and Item Transport
curse.category.433=Forestry
curse.category.425=Miscellaneous
curse.category.4545=Applied Energistics 2
curse.category.416=Farming
curse.category.421=API and Library
curse.category.4780=Fabric
curse.category.424=Cosmetic
curse.category.406=World gen
curse.category.435=Server Utility
curse.category.411=Mobs
curse.category.407=Biomes
curse.category.427=Thermal Expansion
curse.category.410=Dimensions
curse.category.436=Food
curse.category.4558=Redstone
curse.category.4843=Automation
curse.category.4906=MCreator
download=Download
download.code.404=File not found on the remote server: %s
download.failed=Failed to download %1$s, response code: %2$d
@@ -242,6 +300,7 @@ modpack.choose.remote.detail=Requires a direct download link to the remote modpa
modpack.choose.remote.tooltip=A direct download link to the remote modpack file
modpack.desc=Describe your modpack, including precautions and changelog. Markdown and online pictures are supported.
modpack.description=Description
modpack.download=Modpack Downloads
modpack.enter_name=Enter a name for this modpack.
modpack.export=Export Modpack
modpack.export.as=Export Modpack As...

View File

@@ -98,62 +98,64 @@ color.custom=自定义颜色
crash.NoClassDefFound=请确认 Hello Minecraft! Launcher 本体是否完整,或更新您的 Java。
crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩溃,请检查您的 Java 环境或您的电脑!可以尝试重新安装 Java。
curse.category.modpack.sci-fi=科幻
curse.category.modpack.small-light=轻量整合包
curse.category.modpack.combat-pvp=战斗 PVP
curse.category.modpack.mini-game=小游戏
curse.category.modpack.quests=任务
curse.category.modpack.multiplayer=多人
curse.category.modpack.exploration=探索
curse.category.modpack.skyblock=空岛
curse.category.modpack.adventure-and-rpg=冒险 RPG
curse.category.modpack.ftb-official-pack=FTB 整合包
curse.category.modpack.map-based=有特定地图
curse.category.modpack.hardcore=高难度
curse.category.modpack.extra-large=大型整合包
curse.category.modpack.tech=科技
curse.category.modpack.magic=魔法
# https://addons-ecs.forgesvc.net/api/v2/category/section/4471
curse.category.4474=科幻
curse.category.4481=轻量整合包
curse.category.4483=战斗 PVP
curse.category.4477=小游戏
curse.category.4478=任务
curse.category.4484=多人
curse.category.4476=探索
curse.category.4736=空岛
curse.category.4475=冒险 RPG
curse.category.4487=FTB 整合包
curse.category.4480=有特定地图
curse.category.4479=高难度
curse.category.4482=大型整合包
curse.category.4472=科技
curse.category.4473=魔法
curse.category.mod.map-information=信息展示
curse.category.mod.mc-addons=模组扩展
curse.category.mod.armor-weapons-tools=装备武器
curse.category.mod.world-structures=自然生成
curse.category.mod.blood-magic=血魔法
curse.category.mod.storage=存储
curse.category.mod.addons-industrialcraft=工业 (Industrialcraft)
curse.category.mod.magic=魔法
curse.category.mod.technology=科技
curse.category.mod.[4557]redstone=红石
curse.category.mod.addons-tinkers-construct=匠魂
curse.category.mod.technology-player-transport=交通运输
curse.category.mod.[4486]lucky-blocks=Lucky Blocks
curse.category.mod.addons-buildcraft=建筑 (Buildcraft)
curse.category.mod.technology-genetics=基因
curse.category.mod.twitch-integration=Twitch
curse.category.mod.world-ores-resources=矿物资源
curse.category.mod.crafttweaker=CraftTweaker
curse.category.mod.addons-thaumcraft=神秘 (Thaumcraft)
curse.category.mod.adventure-rpg=冒险 RPG
curse.category.mod.technology-processing=机器处理
curse.category.mod.technology-energy=能源
curse.category.mod.technology-item-fluid-energy-transport=物流运输
curse.category.mod.addons-forestry=林业 (Forestry)
curse.category.mod.mc-miscellaneous=其他
curse.category.mod.applied-energistics-2=应用能源 2 (Applied Energistics 2)
curse.category.mod.technology-farming=农业
curse.category.mod.library-api=支持库
curse.category.mod.fabric=Fabric
curse.category.mod.cosmetic=装饰
curse.category.mod.world-gen=世界生成
curse.category.mod.server-utility=服务器
curse.category.mod.world-mobs=生物
curse.category.mod.world-biomes=生物群系
curse.category.mod.addons-thermalexpansion=热力膨胀 (Thermal Expansion)
curse.category.mod.world-dimensions=维度
curse.category.mod.mc-food=食物
curse.category.mod.redstone=红石
curse.category.mod.technology-automation=自动化
curse.category.mod.mc-creator=MCreator
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.423=信息展示
curse.category.426=模组扩展
curse.category.434=装备武器
curse.category.409=自然生成
curse.category.4485=血魔法
curse.category.420=存储
curse.category.429=工业 (Industrialcraft)
curse.category.419=魔法
curse.category.412=科技
curse.category.4557=红石
curse.category.428=匠魂
curse.category.414=交通运输
curse.category.4486=Lucky Blocks
curse.category.432=建筑 (Buildcraft)
curse.category.418=基因
curse.category.4671=Twitch
curse.category.408=矿物资源
curse.category.4773=CraftTweaker
curse.category.430=神秘 (Thaumcraft)
curse.category.422=冒险 RPG
curse.category.413=机器处理
curse.category.417=能源
curse.category.415=物流运输
curse.category.433=林业 (Forestry)
curse.category.425=其他
curse.category.4545=应用能源 2 (Applied Energistics 2)
curse.category.416=农业
curse.category.421=支持库
curse.category.4780=Fabric
curse.category.424=装饰
curse.category.406=世界生成
curse.category.435=服务器
curse.category.411=生物
curse.category.407=生物群系
curse.category.427=热力膨胀 (Thermal Expansion)
curse.category.410=维度
curse.category.436=食物
curse.category.4558=红石
curse.category.4843=自动化
curse.category.4906=MCreator
download=下载
download.code.404=远程服务器不包含需要下载的文件: %s
@@ -299,6 +301,7 @@ modpack.choose.remote.detail=需要提供整合包的下载链接
modpack.choose.remote.tooltip=要下载的整合包的链接
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML图片请用网络图
modpack.description=整合包描述
modpack.download=下载整合包
modpack.enter_name=给游戏起个你喜欢的名字
modpack.export=导出整合包
modpack.export.as=请选择整合包类型 (若无法决定,请选择我的世界中文论坛整合包标准)
@@ -365,6 +368,7 @@ mods.add.failed=添加模组 %s 失败。
mods.add.success=成功添加模组 %s。
mods.choose_mod=选择模组
mods.download=模组下载
mods.download.title=模组下载 - %1s
mods.enable=启用
mods.disable=禁用
mods.name=名称
@@ -514,7 +518,7 @@ version.empty.add=进入版本列表安装
version.empty.launch=没有可启动的游戏,你可以点击左侧游戏栏内的设置按钮进入版本列表安装游戏
version.forbidden_name=此版本名称不受支持,请换一个名字
version.game.old=远古版
version.game.release=稳定
version.game.release=正式
version.game.snapshot=测试版
version.launch=启动游戏
version.launch.test=测试游戏

View File

@@ -9,6 +9,7 @@ public class CurseAddon {
private final int id;
private final String name;
private final List<Author> authors;
private final List<Attachment> attachments;
private final String websiteUrl;
private final int gameId;
private final String summary;
@@ -27,10 +28,11 @@ public class CurseAddon {
private final boolean isAvailable;
private final boolean isExperimental;
public CurseAddon(int id, String name, List<Author> authors, String websiteUrl, int gameId, String summary, int defaultFileId, List<LatestFile> latestFiles, List<Category> categories, int status, int primaryCategoryId, String slug, List<GameVersionLatestFile> gameVersionLatestFiles, boolean isFeatured, double popularityScore, int gamePopularityRank, String primaryLanguage, List<String> modLoaders, boolean isAvailable, boolean isExperimental) {
public CurseAddon(int id, String name, List<Author> authors, List<Attachment> attachments, String websiteUrl, int gameId, String summary, int defaultFileId, List<LatestFile> latestFiles, List<Category> categories, int status, int primaryCategoryId, String slug, List<GameVersionLatestFile> gameVersionLatestFiles, boolean isFeatured, double popularityScore, int gamePopularityRank, String primaryLanguage, List<String> modLoaders, boolean isAvailable, boolean isExperimental) {
this.id = id;
this.name = name;
this.authors = authors;
this.attachments = attachments;
this.websiteUrl = websiteUrl;
this.gameId = gameId;
this.summary = summary;
@@ -62,6 +64,10 @@ public class CurseAddon {
return authors;
}
public List<Attachment> getAttachments() {
return attachments;
}
public String getWebsiteUrl() {
return websiteUrl;
}
@@ -173,6 +179,61 @@ public class CurseAddon {
}
}
@Immutable
public static class Attachment {
private final int id;
private final int projectId;
private final String description;
private final boolean isDefault;
private final String thumbnailUrl;
private final String title;
private final String url;
private final int status;
public Attachment(int id, int projectId, String description, boolean isDefault, String thumbnailUrl, String title, String url, int status) {
this.id = id;
this.projectId = projectId;
this.description = description;
this.isDefault = isDefault;
this.thumbnailUrl = thumbnailUrl;
this.title = title;
this.url = url;
this.status = status;
}
public int getId() {
return id;
}
public int getProjectId() {
return projectId;
}
public String getDescription() {
return description;
}
public boolean isDefault() {
return isDefault;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
public String getTitle() {
return title;
}
public String getUrl() {
return url;
}
public int getStatus() {
return status;
}
}
@Immutable
public static class Dependency {
private final int id;
@@ -212,6 +273,7 @@ public class CurseAddon {
public static class LatestFile {
private final int id;
private final String displayName;
private final String fileName;
private final String fileDate;
private final int fileLength;
private final int releaseType;
@@ -228,12 +290,13 @@ public class CurseAddon {
private final int restrictProjectFileAccess;
private final int projectStatus;
private final int projectId;
private final int isServerPack;
private final boolean isServerPack;
private final int serverPackFileId;
public LatestFile(int id, String displayName, String fileDate, int fileLength, int releaseType, int fileStatus, String downloadUrl, boolean isAlternate, int alternateFileId, List<Dependency> dependencies, boolean isAvailable, List<String> gameVersion, boolean hasInstallScript, boolean isCompatibleWIthClient, int categorySectionPackageType, int restrictProjectFileAccess, int projectStatus, int projectId, int isServerPack, int serverPackFileId) {
public LatestFile(int id, String displayName, String fileName, String fileDate, int fileLength, int releaseType, int fileStatus, String downloadUrl, boolean isAlternate, int alternateFileId, List<Dependency> dependencies, boolean isAvailable, List<String> gameVersion, boolean hasInstallScript, boolean isCompatibleWIthClient, int categorySectionPackageType, int restrictProjectFileAccess, int projectStatus, int projectId, boolean isServerPack, int serverPackFileId) {
this.id = id;
this.displayName = displayName;
this.fileName = fileName;
this.fileDate = fileDate;
this.fileLength = fileLength;
this.releaseType = releaseType;
@@ -262,6 +325,10 @@ public class CurseAddon {
return displayName;
}
public String getFileName() {
return fileName;
}
public String getFileDate() {
return fileDate;
}
@@ -326,7 +393,7 @@ public class CurseAddon {
return projectId;
}
public int getIsServerPack() {
public boolean isServerPack() {
return isServerPack;
}

View File

@@ -28,6 +28,12 @@ public class CurseModManager {
}.getType());
}
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"));
return JsonUtils.fromNonNullJson(response, new TypeToken<List<CurseAddon.LatestFile>>() {
}.getType());
}
public static List<Category> getCategories(int section) throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/category/section/" + section));
List<Category> categories = JsonUtils.fromNonNullJson(response, new TypeToken<List<Category>>() {