From 23bb926acfa75e5d8de6d6e187168d8c437419de Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Sun, 29 Aug 2021 05:53:19 +0800 Subject: [PATCH] feat: WIP: Download Page --- .../org/jackhuang/hmcl/ui/Controllers.java | 7 + .../main/java/org/jackhuang/hmcl/ui/SVG.java | 6 + .../hmcl/ui/construct/TabControl.java | 4 + .../hmcl/ui/download/DownloadPage.java | 153 ++++++++++++++++++ .../org/jackhuang/hmcl/ui/main/RootPage.java | 9 ++ .../hmcl/ui/versions/VersionPage.java | 14 +- .../resources/assets/lang/I18N.properties | 3 + .../resources/assets/lang/I18N_zh.properties | 3 +- .../assets/lang/I18N_zh_CN.properties | 5 +- 9 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.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 dd4bcf6ad..ea0651887 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -44,6 +44,7 @@ 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.download.DownloadPage; import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.main.LauncherSettingsPage; import org.jackhuang.hmcl.ui.main.RootPage; @@ -93,6 +94,7 @@ public final class Controllers { } }; }); + private static Lazy downloadPage = new Lazy<>(DownloadPage::new); private static Lazy accountListPage = new Lazy<>(() -> { AccountListPage accountListPage = new AccountListPage(); accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty()); @@ -149,6 +151,11 @@ public final class Controllers { return accountListPage.get(); } + // FXThread + public static DownloadPage getDownloadPage() { + return downloadPage.get(); + } + // FXThread public static DecoratorController getDecorator() { return decorator; 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 4c808bbe1..6666e7380 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -307,6 +307,12 @@ public final class SVG { fill, width, height); } + public static Node textureBox(ObjectBinding fill, double width, double height) { + return createSVGPath( + "M20 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V4C22 2.9 21.11 2 20 2M4 6L6 4H10.9L4 10.9V6M4 13.7L13.7 4H18.6L4 18.6V13.7M20 18L18 20H13.1L20 13.1V18M20 10.3L10.3 20H5.4L20 5.4V10.3Z", + fill, width, height); + } + public static Node gamepad(ObjectBinding fill, double width, double height) { return createSVGPath( "M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z", diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabControl.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabControl.java index aff0819a9..47be8ebda 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabControl.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabControl.java @@ -259,6 +259,10 @@ public interface TabControl { this.userData.set(userData); } + public boolean isInitialized() { + return getNode() == null; + } + public boolean initializeIfNeeded() { if (getNode() == null) { if (getNodeSupplier() == null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java new file mode 100644 index 000000000..9f939d6b0 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -0,0 +1,153 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.download; + +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.scene.layout.BorderPane; +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.Profiles; +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.WeakListenerHolder; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.AdvancedListBox; +import org.jackhuang.hmcl.ui.construct.TabHeader; +import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +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.io.NetworkUtils; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; + +import static org.jackhuang.hmcl.ui.FXUtils.runInFX; +import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class DownloadPage extends BorderPane implements DecoratorPage { + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), 200)); + private final TabHeader tab; + private final TabHeader.Tab modTab = new TabHeader.Tab<>("modTab"); + private final TabHeader.Tab modpackTab = new TabHeader.Tab<>("modpackTab"); + private final TabHeader.Tab resourcePackTab = new TabHeader.Tab<>("resourcePackTab"); + private final TransitionPane transitionPane = new TransitionPane(); + + private WeakListenerHolder listenerHolder; + + public DownloadPage() { + modTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl)); + modpackTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_MOD, this::download)); + resourcePackTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_MOD, this::download)); + tab = new TabHeader(modTab, modpackTab, resourcePackTab); + + Profiles.registerVersionsListener(this::loadVersions); + + tab.getSelectionModel().select(modTab); + FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> { + if (newValue.initializeIfNeeded()) { + if (newValue.getNode() instanceof VersionPage.VersionLoadable) { + ((VersionPage.VersionLoadable) newValue.getNode()).loadVersion(Profiles.getSelectedProfile(), Profiles.getSelectedVersion()); + } + } + transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer()); + }); + + { + AdvancedListBox sideBar = new AdvancedListBox() + .addNavigationDrawerItem(item -> { + item.setTitle(i18n("mods")); + item.setLeftGraphic(wrap(SVG.puzzle(null, 20, 20))); + item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modTab)); + item.setOnAction(e -> tab.getSelectionModel().select(modTab)); + }) + .addNavigationDrawerItem(settingsItem -> { + settingsItem.setTitle(i18n("modpack")); + settingsItem.setLeftGraphic(wrap(SVG.pack(null, 20, 20))); + settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modpackTab)); + settingsItem.setOnAction(e -> tab.getSelectionModel().select(modpackTab)); + }) + .addNavigationDrawerItem(item -> { + item.setTitle(i18n("resourcepack")); + item.setLeftGraphic(wrap(SVG.textureBox(null, 20, 20))); + item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab)); + item.setOnAction(e -> tab.getSelectionModel().select(resourcePackTab)); + }); + FXUtils.setLimitWidth(sideBar, 200); + setLeft(sideBar); + } + + setCenter(transitionPane); + } + + private 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(); + } + + private void loadVersions(Profile profile) { + WeakListenerHolder listenerHolder = new WeakListenerHolder(); + runInFX(() -> { + if (profile == Profiles.getSelectedProfile()) { + profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> { + FXUtils.checkFxUserThread(); + + if (modTab.isInitialized()) { + modTab.getNode().loadVersion(profile, newValue); + } + if (modpackTab.isInitialized()) { + modpackTab.getNode().loadVersion(profile, newValue); + } + if (resourcePackTab.isInitialized()) { + resourcePackTab.getNode().loadVersion(profile, newValue); + } + })); + } + }); + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 737872b63..37c37568e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -167,6 +167,14 @@ public class RootPage extends DecoratorTabPage { gameItem.setTitle(i18n("version.manage")); gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage())); + // forth item in left sidebar + AdvancedListItem downloadItem = new AdvancedListItem(); + downloadItem + .setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/chest.png")).getKey()); + downloadItem.setActionButtonVisible(false); + downloadItem.setTitle(i18n("download")); + downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage())); + // fifth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); launcherSettingsItem @@ -182,6 +190,7 @@ public class RootPage extends DecoratorTabPage { .startCategory(i18n("version").toUpperCase()) .add(gameListItem) .add(gameItem) + .add(downloadItem) .add(launcherSettingsItem); // the root page, with the sidebar in left, navigator in center. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index 4245e3957..ca6b8537f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -107,15 +107,15 @@ public class VersionPage extends Control implements DecoratorPage, ModDownloadPa setVersion(version, profile); preferredVersionName = version; - if (versionSettingsTab.getNode() != null) + if (versionSettingsTab.isInitialized()) versionSettingsTab.getNode().loadVersion(profile, version); - if (modListTab.getNode() != null) + if (modListTab.isInitialized()) modListTab.getNode().loadVersion(profile, version); - if (curseModListTab.getNode() != null) + if (curseModListTab.isInitialized()) curseModListTab.getNode().loadVersion(profile, version); - if (installerListTab.getNode() != null) + if (installerListTab.isInitialized()) installerListTab.getNode().loadVersion(profile, version); - if (worldListTab.getNode() != null) + if (worldListTab.isInitialized()) worldListTab.getNode().loadVersion(profile, version); currentVersionUpgradable.set(profile.getRepository().isModpack(version)); } @@ -251,7 +251,7 @@ public class VersionPage extends Control implements DecoratorPage, ModDownloadPa AdvancedListItem modListItem = new AdvancedListItem(); modListItem.getStyleClass().add("navigation-drawer-item"); - modListItem.setTitle(i18n("mods")); + modListItem.setTitle(i18n("mods.manage")); modListItem.setLeftGraphic(wrap(SVG.puzzle(null, 20, 20))); modListItem.setActionButtonVisible(false); modListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.modListTab)); @@ -380,7 +380,7 @@ public class VersionPage extends Control implements DecoratorPage, ModDownloadPa return stackPane; } - interface VersionLoadable { + public interface VersionLoadable { void loadVersion(Profile profile, String version); } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 2732bf96d..26b35d465 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -397,6 +397,7 @@ mods.disable=Disable mods.download=Mod Downloads mods.download.title=Mod Downloads - %1s mods.enable=Enable +mods.manage=Mods mods.mcmod.page=MCWiki mods.mcmod.search=Search in MCWiki mods.name=Name @@ -443,6 +444,8 @@ profile.title=Game Directories profile.selected=Selected profile.use_relative_path=Use relative path for game directory if possible +resourcepack=Resource Pack + selector.choose=Choose selector.choose_file=Select a file selector.custom=Custom diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 355b27298..aa7e41bef 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -296,13 +296,14 @@ modpack.wizard.step.initialization.save=選擇要匯出到的遊戲整合包位 modpack.wizard.step.initialization.warning=在製作整合包前,請您確認您選擇的版本可以正常啟動,\n並保證您的 Minecraft 是正式版而非快照版,\n而且不應將不允許非官方途徑傳播的 Mod 模組、材質包等納入整合包。\n整合包會儲存您目前的下載來源設定 modpack.wizard.step.initialization.server=點選此處查看有關伺服器自動更新整合包的製作教學 -mods=模組管理 +mods=模組 mods.add=新增模組 mods.add.failed=新增模組 %s 失敗。 mods.add.success=成功新增模組 %s。 mods.choose_mod=選擇模組 mods.enable=啟用 mods.disable=停用 +mods.mangage=模組管理 mods.name=名稱 mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge 或 LiteLoader 才能進行模組管理。 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index e6e2944e8..6038ec4c1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -389,7 +389,7 @@ modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位 modpack.wizard.step.initialization.warning=在制作整合包前,请您确认您选择的版本可以正常启动,\n并保证您的 Minecraft 是正式版而非快照版,\n而且不应当将不允许非官方途径传播的 Mod、材质包等纳入整合包。\n整合包会保存您目前的下载源设置 modpack.wizard.step.initialization.server=点击此处查看有关服务器自动更新整合包的制作教程 -mods=模组管理 +mods=模组 mods.add=添加模组 mods.add.failed=添加模组 %s 失败。 mods.add.success=成功添加模组 %s。 @@ -399,6 +399,7 @@ mods.disable=禁用 mods.download=模组下载 mods.download.title=模组下载 - %1s mods.enable=启用 +mods.manage=模组管理 mods.mcmod.page=MC 百科页面 mods.mcmod.search=MC 百科搜索 mods.name=名称 @@ -445,6 +446,8 @@ profile.title=游戏目录 profile.selected=已选中 profile.use_relative_path=若可能,游戏目录使用相对路径 +resourcepack=资源包 + search=搜索 search.sort=排序