From af7cf393dc3c8f3bbca99fead6c610b7d7fd4931 Mon Sep 17 00:00:00 2001 From: Yuhui Huang Date: Tue, 3 Aug 2021 22:07:19 +0800 Subject: [PATCH] feat: download mods and modpacks from CurseForge. --- .../org/jackhuang/hmcl/ui/Controllers.java | 52 +++-- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 11 + .../main/java/org/jackhuang/hmcl/ui/SVG.java | 12 + .../hmcl/ui/ToolbarListPageSkin.java | 8 +- .../hmcl/ui/construct/SpinnerPane.java | 107 ++++----- .../hmcl/ui/construct/TwoLineListItem.java | 36 ++- .../hmcl/ui/versions/GameItemSkin.java | 4 +- .../hmcl/ui/versions/GameListPage.java | 10 +- .../hmcl/ui/versions/ModDownloadListPage.java | 161 ++++++++++--- .../hmcl/ui/versions/ModDownloadPage.java | 217 +++++++++++++++++- .../hmcl/ui/versions/ModListPageSkin.java | 1 - .../hmcl/ui/versions/VersionPage.java | 35 ++- .../jackhuang/hmcl/ui/versions/Versions.java | 41 ++++ .../java/org/jackhuang/hmcl/util/Lazy.java | 44 ++++ HMCL/src/main/resources/assets/css/root.css | 26 +++ .../resources/assets/lang/I18N.properties | 59 +++++ .../assets/lang/I18N_zh_CN.properties | 116 +++++----- .../jackhuang/hmcl/mod/curse/CurseAddon.java | 75 +++++- .../hmcl/mod/curse/CurseModManager.java | 6 + 19 files changed, 833 insertions(+), 188 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/util/Lazy.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index ba6e15534..a855163ea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -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 = new Lazy<>(VersionPage::new); + private static Lazy 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 = new Lazy<>(RootPage::new); private static DecoratorController decorator; + private static Lazy 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; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index cf29bad26..b9cad02c2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -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(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index d1efefed9..2c2862b3a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -246,4 +246,16 @@ public final class SVG { public static Node texture(ObjectBinding 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 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 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 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); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java index 05f9d5799..f398060ab 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java @@ -70,7 +70,13 @@ public abstract class ToolbarListPageSkin 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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/SpinnerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/SpinnerPane.java index a2bf9198f..0bcde0259 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/SpinnerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/SpinnerPane.java @@ -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 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; } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java index 2cc9ea024..6a8ce025a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java @@ -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 tags = FXCollections.observableArrayList(); private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle"); + private final ObservableList