From 7a20af462b23fbe6613658fd5f9442934da53a73 Mon Sep 17 00:00:00 2001 From: Yuhui Huang Date: Mon, 2 Aug 2021 23:11:30 +0800 Subject: [PATCH] feat: (WIP) Curse mod/modpack download --- .../org/jackhuang/hmcl/ui/ListPageSkin.java | 3 +- .../hmcl/ui/profile/ProfileListItemSkin.java | 1 - .../hmcl/ui/versions/ModDownloadListPage.java | 153 ++++++ .../hmcl/ui/versions/ModDownloadPage.java | 38 ++ .../hmcl/ui/versions/VersionPage.java | 13 + HMCL/src/main/resources/assets/css/root.css | 5 + .../assets/fxml/version/version-settings.fxml | 2 +- .../resources/assets/lang/I18N.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 62 +++ .../jackhuang/hmcl/mod/curse/CurseAddon.java | 435 ++++++++++++++++++ .../hmcl/mod/curse/CurseModManager.java | 121 +++++ 11 files changed, 830 insertions(+), 4 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadPage.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModManager.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ListPageSkin.java index 529ee22e8..237cf51f5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ListPageSkin.java @@ -61,10 +61,9 @@ public class ListPageSkin extends SkinBase> { VBox vBox = new VBox(); { + vBox.getStyleClass().add("card-list"); vBox.setAlignment(Pos.BOTTOM_RIGHT); vBox.setPickOnBounds(false); - vBox.setPadding(new Insets(15)); - vBox.setSpacing(15); JFXButton btnAdd = new JFXButton(); FXUtils.setLimitWidth(btnAdd, 40); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java index ee0ace3b1..db9f3dced 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItemSkin.java @@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.profile; import com.jfoenix.controls.JFXButton; import javafx.css.PseudoClass; -import javafx.event.ActionEvent; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.SkinBase; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java new file mode 100644 index 000000000..80fb0162a --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java @@ -0,0 +1,153 @@ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXTextField; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +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.layout.*; +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.construct.SpinnerPane; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class ModDownloadListPage extends Control { + private final BooleanProperty loading = new SimpleBooleanProperty(false); + private final BooleanProperty failed = new SimpleBooleanProperty(false); + /** + * @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) { + this.section = section; + } + + public void loadVersion(Profile profile, String version) { + this.profile = profile; + this.version = version; + + setLoading(false); + setFailed(false); + } + + public boolean isFailed() { + return failed.get(); + } + + public BooleanProperty failedProperty() { + return failed; + } + + public void setFailed(boolean failed) { + this.failed.set(failed); + } + + public boolean isLoading() { + return loading.get(); + } + + public BooleanProperty loadingProperty() { + return loading; + } + + public void setLoading(boolean loading) { + this.loading.set(loading); + } + + public void search(String gameVersion, 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) -> { + setLoading(false); + if (exception == null) { + + } else { + + } + }); + } + + @Override + protected Skin createDefaultSkin() { + return new ModDownloadListPageSkin(this); + } + + private static class ModDownloadListPageSkin extends SkinBase { + + protected ModDownloadListPageSkin(ModDownloadListPage control) { + super(control); + + VBox pane = new VBox(); + pane.getStyleClass().add("card-list"); + + GridPane searchPane = new GridPane(); + searchPane.getStyleClass().add("card"); + + ColumnConstraints column1 = new ColumnConstraints(); + column1.setPercentWidth(50); + column1.setHgrow(Priority.ALWAYS); + ColumnConstraints column2 = new ColumnConstraints(); + column2.setHgrow(Priority.ALWAYS); + column2.setPercentWidth(50); + searchPane.getColumnConstraints().setAll(column1, column2); + + searchPane.setHgap(16); + searchPane.setVgap(10); + + { + JFXTextField nameField = new JFXTextField(); + nameField.setPromptText(i18n("mods.name")); + searchPane.add(nameField, 0, 0); + + JFXTextField gameVersionField = new JFXTextField(); + gameVersionField.setPromptText(i18n("world.game_version")); + searchPane.add(gameVersionField, 1, 0); + + JFXTextField categoryField = new JFXTextField(); + categoryField.setPromptText(i18n("mods.category")); + searchPane.add(categoryField, 0, 1); + + JFXTextField sortField = new JFXTextField(); + sortField.setPromptText(i18n("search.sort")); + searchPane.add(sortField, 1, 1); + + VBox vbox = new VBox(); + vbox.setAlignment(Pos.CENTER_RIGHT); + searchPane.add(vbox, 0, 2, 2, 1); + + JFXButton searchButton = new JFXButton(); + searchButton.setText(i18n("search")); + searchButton.setOnAction(e -> { + getSkinnable().search(gameVersionField.getText(), categoryField.getText(), 0, nameField.getText(), sortField.getText()) + .whenComplete(); + }); + searchPane.add(searchButton, 0, 2); + vbox.getChildren().setAll(searchButton); + } + + TransitionPane transitionPane = new TransitionPane(); + { + + SpinnerPane spinnerPane = new SpinnerPane(); + spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty()); + + } + + pane.getChildren().setAll(searchPane, transitionPane); + + getChildren().setAll(pane); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadPage.java new file mode 100644 index 000000000..b79bde795 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadPage.java @@ -0,0 +1,38 @@ +package org.jackhuang.hmcl.ui.versions; + +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.SkinBase; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.mod.curse.CurseAddon; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; + +public class ModDownloadPage extends Control implements DecoratorPage { + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); + private final CurseAddon addon; + private final Version version; + + public ModDownloadPage(CurseAddon addon, Version version) { + this.addon = addon; + this.version = version; + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); + } + + @Override + protected Skin createDefaultSkin() { + return new ModDownloadPageSkin(this); + } + + private static class ModDownloadPageSkin extends SkinBase { + + protected ModDownloadPageSkin(ModDownloadPage control) { + super(control); + } + } +} 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 f489caa1c..31e478556 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 @@ -50,6 +50,8 @@ public class VersionPage extends Control implements DecoratorPage { private final VersionSettingsPage versionSettingsPage = new VersionSettingsPage(); 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 TabHeader.Tab installerListTab = new TabHeader.Tab("installerListTab"); private final InstallerListPage installerListPage = new InstallerListPage(); private final TabHeader.Tab worldListTab = new TabHeader.Tab("worldList"); @@ -64,6 +66,7 @@ public class VersionPage extends Control implements DecoratorPage { { versionSettingsTab.setNode(versionSettingsPage); modListTab.setNode(modListPage); + curseModListTab.setNode(curseModListPage); installerListTab.setNode(installerListPage); worldListTab.setNode(worldListPage); @@ -92,6 +95,7 @@ public class VersionPage extends Control implements DecoratorPage { preferredVersionName = version; versionSettingsPage.loadVersion(profile, version); + curseModListPage.loadVersion(profile, version); currentVersionUpgradable.set(profile.getRepository().isModpack(version)); CompletableFuture.allOf( @@ -245,6 +249,14 @@ public class VersionPage extends Control implements DecoratorPage { modListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.modListTab)); modListItem.setOnAction(e -> control.selectedTab.set(control.modListTab)); + AdvancedListItem curseModListItem = new AdvancedListItem(); + curseModListItem.getStyleClass().add("navigation-drawer-item"); + curseModListItem.setTitle(i18n("mods.download")); + curseModListItem.setLeftGraphic(wrap(SVG.fire(Theme.blackFillBinding(), 20, 20))); + curseModListItem.setActionButtonVisible(false); + curseModListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.curseModListTab)); + curseModListItem.setOnAction(e -> control.selectedTab.set(control.curseModListTab)); + AdvancedListItem installerListItem = new AdvancedListItem(); installerListItem.getStyleClass().add("navigation-drawer-item"); installerListItem.setTitle(i18n("settings.tabs.installers")); @@ -264,6 +276,7 @@ public class VersionPage extends Control implements DecoratorPage { AdvancedListBox sideBar = new AdvancedListBox() .add(versionSettingsItem) .add(modListItem) + .add(curseModListItem) .add(installerListItem) .add(worldListItem); left.setCenter(sideBar); diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 67644922c..897fdba1e 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -783,6 +783,11 @@ -fx-background-color: derive(-fx-base-color, 60%); } +.card-list { + -fx-padding: 10; + -fx-spacing: 10; +} + .options-sublist { -fx-background-color: white; } diff --git a/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml b/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml index ef53d2f20..dffc268de 100644 --- a/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml +++ b/HMCL/src/main/resources/assets/fxml/version/version-settings.fxml @@ -11,7 +11,7 @@ xmlns:fx="http://javafx.com/fxml" type="StackPane"> - + diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index ce7098497..e184d6785 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -307,6 +307,7 @@ mods.add=Install mods mods.add.failed=Failed to install mods %s. mods.add.success=Successfully installed mods %s. mods.choose_mod=Choose your mods +mods.download=Mod Downloads mods.enable=Enable mods.disable=Disable mods.name=Name 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 21aa60e24..198d25e62 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -98,6 +98,63 @@ 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=魔法 + +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 + download=下载 download.code.404=远程服务器不包含需要下载的文件: %s download.failed=下载失败: %1$s,错误码:%2$d @@ -307,9 +364,11 @@ mods.add=添加模组 mods.add.failed=添加模组 %s 失败。 mods.add.success=成功添加模组 %s。 mods.choose_mod=选择模组 +mods.download=模组下载 mods.enable=启用 mods.disable=禁用 mods.name=名称 +mods.category=类别 mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。 datapack=数据包 @@ -352,6 +411,9 @@ profile.title=游戏目录 profile.selected=已选中 profile.use_relative_path=若可能,游戏目录使用相对路径 +search=搜索 +search.sort=排序 + selector.choose=选择 selector.choose_file=选择文件 selector.custom=自定义 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java new file mode 100644 index 000000000..2af9684e5 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -0,0 +1,435 @@ +package org.jackhuang.hmcl.mod.curse; + +import org.jackhuang.hmcl.util.Immutable; + +import java.util.List; + +@Immutable +public class CurseAddon { + private final int id; + private final String name; + private final List authors; + private final String websiteUrl; + private final int gameId; + private final String summary; + private final int defaultFileId; + private final List latestFiles; + private final List categories; + private final int status; + private final int primaryCategoryId; + private final String slug; + private final List gameVersionLatestFiles; + private final boolean isFeatured; + private final double popularityScore; + private final int gamePopularityRank; + private final String primaryLanguage; // e.g. enUS + private final List modLoaders; + private final boolean isAvailable; + private final boolean isExperimental; + + public CurseAddon(int id, String name, List authors, String websiteUrl, int gameId, String summary, int defaultFileId, List latestFiles, List categories, int status, int primaryCategoryId, String slug, List gameVersionLatestFiles, boolean isFeatured, double popularityScore, int gamePopularityRank, String primaryLanguage, List modLoaders, boolean isAvailable, boolean isExperimental) { + this.id = id; + this.name = name; + this.authors = authors; + this.websiteUrl = websiteUrl; + this.gameId = gameId; + this.summary = summary; + this.defaultFileId = defaultFileId; + this.latestFiles = latestFiles; + this.categories = categories; + this.status = status; + this.primaryCategoryId = primaryCategoryId; + this.slug = slug; + this.gameVersionLatestFiles = gameVersionLatestFiles; + this.isFeatured = isFeatured; + this.popularityScore = popularityScore; + this.gamePopularityRank = gamePopularityRank; + this.primaryLanguage = primaryLanguage; + this.modLoaders = modLoaders; + this.isAvailable = isAvailable; + this.isExperimental = isExperimental; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public List getAuthors() { + return authors; + } + + public String getWebsiteUrl() { + return websiteUrl; + } + + public int getGameId() { + return gameId; + } + + public String getSummary() { + return summary; + } + + public int getDefaultFileId() { + return defaultFileId; + } + + public List getLatestFiles() { + return latestFiles; + } + + public List getCategories() { + return categories; + } + + public int getStatus() { + return status; + } + + public int getPrimaryCategoryId() { + return primaryCategoryId; + } + + public String getSlug() { + return slug; + } + + public List getGameVersionLatestFiles() { + return gameVersionLatestFiles; + } + + public boolean isFeatured() { + return isFeatured; + } + + public double getPopularityScore() { + return popularityScore; + } + + public int getGamePopularityRank() { + return gamePopularityRank; + } + + public String getPrimaryLanguage() { + return primaryLanguage; + } + + public List getModLoaders() { + return modLoaders; + } + + public boolean isAvailable() { + return isAvailable; + } + + public boolean isExperimental() { + return isExperimental; + } + + @Immutable + public static class Author { + private final String name; + private final String url; + private final int projectId; + private final int id; + private final int userId; + private final int twitchId; + + public Author(String name, String url, int projectId, int id, int userId, int twitchId) { + this.name = name; + this.url = url; + this.projectId = projectId; + this.id = id; + this.userId = userId; + this.twitchId = twitchId; + } + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } + + public int getProjectId() { + return projectId; + } + + public int getId() { + return id; + } + + public int getUserId() { + return userId; + } + + public int getTwitchId() { + return twitchId; + } + } + + @Immutable + public static class Dependency { + private final int id; + private final int addonId; + private final int type; + private final int fileId; + + public Dependency() { + this(0, 0, 0, 0); + } + + public Dependency(int id, int addonId, int type, int fileId) { + this.id = id; + this.addonId = addonId; + this.type = type; + this.fileId = fileId; + } + + public int getId() { + return id; + } + + public int getAddonId() { + return addonId; + } + + public int getType() { + return type; + } + + public int getFileId() { + return fileId; + } + } + + @Immutable + public static class LatestFile { + private final int id; + private final String displayName; + private final String fileDate; + private final int fileLength; + private final int releaseType; + private final int fileStatus; + private final String downloadUrl; + private final boolean isAlternate; + private final int alternateFileId; + private final List dependencies; + private final boolean isAvailable; + private final List gameVersion; + private final boolean hasInstallScript; + private final boolean isCompatibleWIthClient; + private final int categorySectionPackageType; + private final int restrictProjectFileAccess; + private final int projectStatus; + private final int projectId; + private final int 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 dependencies, boolean isAvailable, List gameVersion, boolean hasInstallScript, boolean isCompatibleWIthClient, int categorySectionPackageType, int restrictProjectFileAccess, int projectStatus, int projectId, int isServerPack, int serverPackFileId) { + this.id = id; + this.displayName = displayName; + this.fileDate = fileDate; + this.fileLength = fileLength; + this.releaseType = releaseType; + this.fileStatus = fileStatus; + this.downloadUrl = downloadUrl; + this.isAlternate = isAlternate; + this.alternateFileId = alternateFileId; + this.dependencies = dependencies; + this.isAvailable = isAvailable; + this.gameVersion = gameVersion; + this.hasInstallScript = hasInstallScript; + this.isCompatibleWIthClient = isCompatibleWIthClient; + this.categorySectionPackageType = categorySectionPackageType; + this.restrictProjectFileAccess = restrictProjectFileAccess; + this.projectStatus = projectStatus; + this.projectId = projectId; + this.isServerPack = isServerPack; + this.serverPackFileId = serverPackFileId; + } + + public int getId() { + return id; + } + + public String getDisplayName() { + return displayName; + } + + public String getFileDate() { + return fileDate; + } + + public int getFileLength() { + return fileLength; + } + + public int getReleaseType() { + return releaseType; + } + + public int getFileStatus() { + return fileStatus; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public boolean isAlternate() { + return isAlternate; + } + + public int getAlternateFileId() { + return alternateFileId; + } + + public List getDependencies() { + return dependencies; + } + + public boolean isAvailable() { + return isAvailable; + } + + public List getGameVersion() { + return gameVersion; + } + + public boolean isHasInstallScript() { + return hasInstallScript; + } + + public boolean isCompatibleWIthClient() { + return isCompatibleWIthClient; + } + + public int getCategorySectionPackageType() { + return categorySectionPackageType; + } + + public int getRestrictProjectFileAccess() { + return restrictProjectFileAccess; + } + + public int getProjectStatus() { + return projectStatus; + } + + public int getProjectId() { + return projectId; + } + + public int getIsServerPack() { + return isServerPack; + } + + public int getServerPackFileId() { + return serverPackFileId; + } + } + + @Immutable + public static class Category { + private final int categoryId; + private final String name; + private final String url; + private final String avatarUrl; + private final int parentId; + private final int rootId; + private final int projectId; + private final int avatarId; + private final int gameId; + + public Category(int categoryId, String name, String url, String avatarUrl, int parentId, int rootId, int projectId, int avatarId, int gameId) { + this.categoryId = categoryId; + this.name = name; + this.url = url; + this.avatarUrl = avatarUrl; + this.parentId = parentId; + this.rootId = rootId; + this.projectId = projectId; + this.avatarId = avatarId; + this.gameId = gameId; + } + + public int getCategoryId() { + return categoryId; + } + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public int getParentId() { + return parentId; + } + + public int getRootId() { + return rootId; + } + + public int getProjectId() { + return projectId; + } + + public int getAvatarId() { + return avatarId; + } + + public int getGameId() { + return gameId; + } + } + + @Immutable + public static class GameVersionLatestFile { + private final String gameVersion; + private final String projectFileId; + private final String projectFileName; + private final int fileType; + private final Integer modLoader; // optional + + public GameVersionLatestFile(String gameVersion, String projectFileId, String projectFileName, int fileType, Integer modLoader) { + this.gameVersion = gameVersion; + this.projectFileId = projectFileId; + this.projectFileName = projectFileName; + this.fileType = fileType; + this.modLoader = modLoader; + } + + public String getGameVersion() { + return gameVersion; + } + + public String getProjectFileId() { + return projectFileId; + } + + public String getProjectFileName() { + return projectFileName; + } + + public int getFileType() { + return fileType; + } + + public Integer getModLoader() { + return modLoader; + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModManager.java new file mode 100644 index 000000000..7316af858 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModManager.java @@ -0,0 +1,121 @@ +package org.jackhuang.hmcl.mod.curse; + +import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.NetworkUtils; + +import java.io.IOException; +import java.net.URL; +import java.util.*; + +import static org.jackhuang.hmcl.util.Lang.mapOf; +import static org.jackhuang.hmcl.util.Pair.pair; + +public class CurseModManager { + + public static List searchPaginated(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws IOException { + String response = NetworkUtils.doGet(new URL(NetworkUtils.withQuery("https://addons-ecs.forgesvc.net/api/v2/addon/search", mapOf( + pair("categoryId", Integer.toString(category)), + pair("gameId", "432"), + pair("gameVersion", gameVersion), + pair("index", Integer.toString(pageOffset)), + pair("pageSize", "25"), + pair("searchFilter", searchFilter), + pair("sectionId", Integer.toString(section)), + pair("sort", Integer.toString(sort)) + )))); + return JsonUtils.fromNonNullJson(response, new TypeToken>() { + }.getType()); + } + + public static List getCategories(int section) throws IOException { + String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/category/section/" + section)); + List categories = JsonUtils.fromNonNullJson(response, new TypeToken>() { + }.getType()); + return reorganizeCategories(categories, section); + } + + private static List reorganizeCategories(List categories, int rootId) { + List result = new ArrayList<>(); + + Map categoryMap = new HashMap<>(); + for (Category category : categories) { + categoryMap.put(category.id, category); + } + for (Category category : categories) { + if (category.parentGameCategoryId == rootId) { + result.add(category); + } else { + Category parentCategory = categoryMap.get(category.parentGameCategoryId); + if (parentCategory == null) { + // Category list is not correct, so we ignore this item. + continue; + } + parentCategory.subcategories.add(category); + } + } + return result; + } + + public static final int SECTION_MODPACK = 4471; + public static final int SECTION_MOD = 6; + + public static class Category { + private final int id; + private final String name; + private final String slug; + private final String avatarUrl; + private final int parentGameCategoryId; + private final int rootGameCategoryId; + private final int gameId; + + private final List subcategories; + + public Category() { + this(0, "", "", "", 0, 0, 0, new ArrayList<>()); + } + + public Category(int id, String name, String slug, String avatarUrl, int parentGameCategoryId, int rootGameCategoryId, int gameId, List subcategories) { + this.id = id; + this.name = name; + this.slug = slug; + this.avatarUrl = avatarUrl; + this.parentGameCategoryId = parentGameCategoryId; + this.rootGameCategoryId = rootGameCategoryId; + this.gameId = gameId; + this.subcategories = subcategories; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getSlug() { + return slug; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public int getParentGameCategoryId() { + return parentGameCategoryId; + } + + public int getRootGameCategoryId() { + return rootGameCategoryId; + } + + public int getGameId() { + return gameId; + } + + public List getSubcategories() { + return subcategories; + } + } +}