From f088bfa11414e60bee25d49a48f0a5e02e1750ee Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Sun, 12 Sep 2021 20:14:39 +0800 Subject: [PATCH] feat: search modrinth --- .../hmcl/setting/DownloadProviders.java | 26 + .../hmcl/ui/download/DownloadPage.java | 17 +- .../hmcl/ui/versions/DownloadListPage.java | 104 +++- .../hmcl/ui/versions/DownloadPage.java | 105 ++-- .../hmcl/ui/versions/ModDownloadListPage.java | 40 +- .../hmcl/ui/versions/VersionPage.java | 10 +- .../jackhuang/hmcl/ui/versions/Versions.java | 12 +- .../resources/assets/lang/I18N.properties | 15 + .../resources/assets/lang/I18N_zh.properties | 15 + .../assets/lang/I18N_zh_CN.properties | 15 + .../jackhuang/hmcl/mod/DownloadManager.java | 204 ++++++++ .../jackhuang/hmcl/mod/curse/CurseAddon.java | 81 ++- .../hmcl/mod/curse/CurseModManager.java | 17 + .../jackhuang/hmcl/mod/modrinth/Modrinth.java | 479 ++++++++++++++++++ .../java/org/jackhuang/hmcl/util/Lang.java | 5 + .../hmcl/util/gson/InstantTypeAdapter.java | 51 ++ .../jackhuang/hmcl/util/gson/JsonUtils.java | 2 + .../jackhuang/hmcl/util/io/NetworkUtils.java | 8 +- 18 files changed, 1099 insertions(+), 107 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/DownloadManager.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/Modrinth.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/InstantTypeAdapter.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/DownloadProviders.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/DownloadProviders.java index 1be743de7..8c368df9a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/DownloadProviders.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/DownloadProviders.java @@ -19,9 +19,15 @@ package org.jackhuang.hmcl.setting; import javafx.beans.InvalidationListener; import org.jackhuang.hmcl.download.*; +import org.jackhuang.hmcl.task.DownloadException; import org.jackhuang.hmcl.task.FetchTask; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.i18n.I18n; +import org.jackhuang.hmcl.util.io.ResponseCodeException; +import java.net.SocketTimeoutException; +import java.net.URL; import java.util.Arrays; import java.util.Map; import java.util.Optional; @@ -32,6 +38,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class DownloadProviders { private DownloadProviders() {} @@ -127,4 +134,23 @@ public final class DownloadProviders { public static DownloadProvider getDownloadProvider() { return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider; } + + public static String localizeErrorMessage(Exception exception) { + if (exception instanceof DownloadException) { + URL url = ((DownloadException) exception).getUrl(); + if (exception.getCause() instanceof SocketTimeoutException) { + return i18n("install.failed.downloading.timeout", url); + } else if (exception.getCause() instanceof ResponseCodeException) { + ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause(); + if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) { + return i18n("download.code." + responseCodeException.getResponseCode(), url); + } else { + return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()); + } + } else { + return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()); + } + } + return StringUtils.getStackTrace(exception); + } } 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 index 62e3652e5..a6a610cee 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -23,7 +23,7 @@ import javafx.scene.Node; import javafx.scene.layout.BorderPane; import org.jackhuang.hmcl.download.*; import org.jackhuang.hmcl.download.game.GameRemoteVersion; -import org.jackhuang.hmcl.mod.curse.CurseAddon; +import org.jackhuang.hmcl.mod.DownloadManager; import org.jackhuang.hmcl.mod.curse.CurseModManager; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.Profile; @@ -40,6 +40,7 @@ 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.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.TabHeader; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -143,19 +144,25 @@ public class DownloadPage extends BorderPane implements DecoratorPage { setCenter(transitionPane); } - private void download(Profile profile, @Nullable String version, CurseAddon.LatestFile file, String subdirectoryName) { + private void download(Profile profile, @Nullable String version, DownloadManager.Version file, String subdirectoryName) { if (version == null) version = profile.getSelectedVersion(); Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath(); - Path dest = runDirectory.resolve(subdirectoryName).resolve(file.getFileName()); + Path dest = runDirectory.resolve(subdirectoryName).resolve(file.getFile().getFilename()); TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> { }); TaskExecutor executor = Task.composeAsync(() -> { - FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getDownloadUrl()), dest.toFile()); - task.setName(file.getDisplayName()); + FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile()); + task.setName(file.getName()); return task; + }).whenComplete(exception -> { + if (exception != null) { + Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR); + } else { + Controllers.showToast(i18n("install.success")); + } }).executor(false); downloadingPane.setExecutor(executor, true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index 99a5531ae..bdd342422 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -25,6 +25,7 @@ import javafx.beans.binding.Bindings; import javafx.beans.binding.ObjectBinding; import javafx.beans.property.*; import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; @@ -37,27 +38,34 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.*; import org.jackhuang.hmcl.game.GameVersion; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.mod.DownloadManager; 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.task.TaskExecutor; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.WeakListenerHolder; 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.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jackhuang.hmcl.util.versioning.VersionNumber; import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor; public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable { protected final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); @@ -65,10 +73,16 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP private final BooleanProperty failed = new SimpleBooleanProperty(false); private final boolean versionSelection; private final ObjectProperty version = new SimpleObjectProperty<>(); - private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); + private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); + private final ObservableList versions = FXCollections.observableArrayList(); + private final StringProperty selectedVersion = new SimpleStringProperty(); private final DownloadPage.DownloadCallback callback; private boolean searchInitialized = false; protected final BooleanProperty supportChinese = new SimpleBooleanProperty(); + protected final ListProperty downloadSources = new SimpleListProperty<>(this, "downloadSources", FXCollections.observableArrayList()); + protected final StringProperty downloadSource = new SimpleStringProperty(); + private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); + private TaskExecutor executor; /** * @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MODPACK @@ -101,6 +115,16 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP searchInitialized = true; search("", 0, 0, "", 0); } + + if (versionSelection) { + versions.setAll(profile.getRepository().getVersions().stream() + .filter(v -> !v.isHidden()) + .sorted(Comparator.comparing((Version v) -> v.getReleaseTime() == null ? new Date(0L) : v.getReleaseTime()) + .thenComparing(v -> VersionNumber.asVersion(v.getId()))) + .map(Version::getId) + .collect(Collectors.toList())); + selectedVersion.set(profile.getSelectedVersion()); + } } public boolean isFailed() { @@ -133,7 +157,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP File versionJar = StringUtils.isNotBlank(version.get().getVersion()) ? version.get().getProfile().getRepository().getVersionJar(version.get().getVersion()) : null; - Task.supplyAsync(() -> { + if (executor != null && !executor.isCancelled()) { + executor.cancel(); + } + + executor = Task.supplyAsync(() -> { String gameVersion; if (StringUtils.isBlank(version.get().getVersion())) { gameVersion = userGameVersion; @@ -146,16 +174,32 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP }).whenComplete(Schedulers.javafx(), (result, exception) -> { setLoading(false); if (exception == null) { - items.setAll(result); + items.setAll(result.collect(Collectors.toList())); failed.set(false); } else { failed.set(true); } - }).start(); + }).executor(true); } - protected List searchImpl(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws Exception { - return CurseModManager.searchPaginated(gameVersion, category, section, pageOffset, searchFilter, sort); + protected Stream searchImpl(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws Exception { + return CurseModManager.searchPaginated(gameVersion, category, section, pageOffset, searchFilter, sort).stream().map(CurseAddon::toMod); + } + + protected String getLocalizedCategory(String category) { + return i18n("curse.category." + category); + } + + protected String getLocalizedOfficialPage() { + return i18n("mods.curseforge"); + } + + protected Profile.ProfileVersion getProfileVersion() { + if (versionSelection) { + return new Profile.ProfileVersion(version.get().getProfile(), selectedVersion.get()); + } else { + return version.get(); + } } @Override @@ -194,18 +238,28 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP { int rowIndex = 0; - if (control.versionSelection) { + if (control.versionSelection && control.downloadSources.getSize() > 1) { JFXComboBox versionsComboBox = new JFXComboBox<>(); - GridPane.setColumnSpan(versionsComboBox, 3); versionsComboBox.setMaxWidth(Double.MAX_VALUE); + Bindings.bindContent(versionsComboBox.getItems(), control.versions); + selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion); - searchPane.addRow(rowIndex++, new Label(i18n("version")), versionsComboBox); + JFXComboBox downloadSourceComboBox = new JFXComboBox<>(); + downloadSourceComboBox.setMaxWidth(Double.MAX_VALUE); + downloadSourceComboBox.getItems().setAll(control.downloadSources.get()); + downloadSourceComboBox.setConverter(stringConverter(I18n::i18n)); + selectedItemPropertyFor(downloadSourceComboBox).bindBidirectional(control.downloadSource); + + searchPane.addRow(rowIndex++, new Label(i18n("version")), versionsComboBox, new Label(i18n("settings.launcher.download_source")), downloadSourceComboBox); } JFXTextField nameField = new JFXTextField(); nameField.setPromptText(getSkinnable().supportChinese.get() ? i18n("search.hint.chinese") : i18n("search.hint.english")); - JFXTextField gameVersionField = new JFXTextField(); + JFXComboBox gameVersionField = new JFXComboBox<>(); + gameVersionField.setMaxWidth(Double.MAX_VALUE); + gameVersionField.setEditable(true); + gameVersionField.getItems().setAll(DownloadManager.DEFAULT_GAME_VERSIONS); Label lblGameVersion = new Label(i18n("world.game_version")); searchPane.addRow(rowIndex++, new Label(i18n("mods.name")), nameField, lblGameVersion, gameVersionField); @@ -267,7 +321,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP searchPane.addRow(rowIndex++, searchBox); EventHandler searchAction = e -> getSkinnable() - .search(gameVersionField.getText(), + .search(gameVersionField.getSelectionModel().getSelectedItem(), Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem()) .map(CategoryIndented::getCategoryId) .orElse(0), @@ -293,16 +347,16 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP } }, getSkinnable().failedProperty())); - JFXListView listView = new JFXListView<>(); + JFXListView 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 DownloadPage(selectedItem, getSkinnable().version.get(), getSkinnable().callback)); + DownloadManager.Mod selectedItem = listView.getSelectionModel().getSelectedItem(); + Controllers.navigate(new DownloadPage(getSkinnable(), selectedItem, getSkinnable().getProfileVersion(), getSkinnable().callback)); }); - listView.setCellFactory(x -> new FloatListCell(listView) { + listView.setCellFactory(x -> new FloatListCell(listView) { TwoLineListItem content = new TwoLineListItem(); ImageView imageView = new ImageView(); @@ -315,19 +369,17 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP } @Override - protected void updateControl(CurseAddon dataItem, boolean empty) { + protected void updateControl(DownloadManager.Mod dataItem, boolean empty) { if (empty) return; ModTranslations.Mod mod = ModTranslations.getModByCurseForgeId(dataItem.getSlug()); - content.setTitle(mod != null ? mod.getDisplayName() : dataItem.getName()); - content.setSubtitle(dataItem.getSummary()); + content.setTitle(mod != null ? mod.getDisplayName() : dataItem.getTitle()); + content.setSubtitle(dataItem.getDescription()); content.getTags().setAll(dataItem.getCategories().stream() - .map(category -> i18n("curse.category." + category.getCategoryId())) + .map(category -> getSkinnable().getLocalizedCategory(category)) .collect(Collectors.toList())); - for (CurseAddon.Attachment attachment : dataItem.getAttachments()) { - if (attachment.isDefault()) { - imageView.setImage(new Image(attachment.getThumbnailUrl(), 40, 40, true, true, true)); - } + if (StringUtils.isNotBlank(dataItem.getIconUrl())) { + imageView.setImage(new Image(dataItem.getIconUrl(), 40, 40, true, true, true)); } } }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 2ee8177fe..11145498d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -35,12 +35,12 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; import org.jackhuang.hmcl.game.GameVersion; +import org.jackhuang.hmcl.mod.DownloadManager; import org.jackhuang.hmcl.mod.ModManager; -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.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; @@ -59,7 +59,6 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.Comparator; -import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; @@ -68,15 +67,17 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DownloadPage extends Control implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); - private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); + private final ListProperty 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 DownloadManager.Mod addon; private final ModTranslations.Mod mod; private final Profile.ProfileVersion version; private final DownloadCallback callback; + private final DownloadListPage page; - public DownloadPage(CurseAddon addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) { + public DownloadPage(DownloadListPage page, DownloadManager.Mod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) { + this.page = page; this.addon = addon; this.mod = ModTranslations.getModByCurseForgeId(addon.getSlug()); this.version = version; @@ -86,27 +87,33 @@ public class DownloadPage extends Control implements DecoratorPage { ? version.getProfile().getRepository().getVersionJar(version.getVersion()) : null; - Task.runAsync(() -> { + setLoading(true); + setFailed(false); + Task.supplyAsync(() -> { if (StringUtils.isNotBlank(version.getVersion())) { Optional gameVersion = GameVersion.minecraftVersion(versionJar); if (gameVersion.isPresent()) { - List files = CurseModManager.getFiles(addon); - items.setAll(files.stream() - .filter(file -> file.getGameVersion().contains(gameVersion.get())) - .sorted(Comparator.comparing(CurseAddon.LatestFile::getParsedFileDate).reversed()) - .collect(Collectors.toList())); - return; + return addon.getData().loadVersions() + .filter(file -> file.getGameVersions().contains(gameVersion.get())); } } - List files = CurseModManager.getFiles(addon); - files.sort(Comparator.comparing(CurseAddon.LatestFile::getParsedFileDate).reversed()); - items.setAll(files); + return addon.getData().loadVersions(); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + items.setAll(result + .sorted(Comparator.comparing(DownloadManager.Version::getDatePublished).reversed()) + .collect(Collectors.toList())); + setFailed(false); + } else { + setFailed(true); + } + setLoading(false); }).start(); - this.state.set(State.fromTitle(addon.getName())); + this.state.set(State.fromTitle(addon.getTitle())); } - public CurseAddon getAddon() { + public DownloadManager.Mod getAddon() { return addon; } @@ -138,7 +145,7 @@ public class DownloadPage extends Control implements DecoratorPage { this.failed.set(failed); } - public void download(CurseAddon.LatestFile file) { + public void download(DownloadManager.Version file) { if (this.callback == null) { saveAs(file); } else { @@ -146,20 +153,20 @@ public class DownloadPage extends Control implements DecoratorPage { } } - public void saveAs(CurseAddon.LatestFile file) { - String extension = StringUtils.substringAfterLast(file.getFileName(), '.'); + public void saveAs(DownloadManager.Version file) { + String extension = StringUtils.substringAfterLast(file.getFile().getFilename(), '.'); FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(i18n("button.save_as")); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("file"), "*." + extension)); - fileChooser.setInitialFileName(file.getFileName()); + fileChooser.setInitialFileName(file.getFile().getFilename()); File dest = fileChooser.showSaveDialog(Controllers.getStage()); if (dest == null) { return; } Controllers.taskDialog( - new FileDownloadTask(NetworkUtils.toURL(file.getDownloadUrl()), dest).executor(true), + new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest).executor(true), i18n("message.downloading") ); } @@ -188,20 +195,18 @@ public class DownloadPage extends Control implements DecoratorPage { BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11)); 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)); - } + if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) { + imageView.setImage(new Image(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true)); } descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40)); TwoLineListItem content = new TwoLineListItem(); HBox.setHgrow(content, Priority.ALWAYS); ModTranslations.Mod mod = ModTranslations.getModByCurseForgeId(getSkinnable().addon.getSlug()); - content.setTitle(mod != null ? mod.getDisplayName() : getSkinnable().addon.getName()); - content.setSubtitle(getSkinnable().addon.getSummary()); + content.setTitle(mod != null ? mod.getDisplayName() : getSkinnable().addon.getTitle()); + content.setSubtitle(getSkinnable().addon.getDescription()); content.getTags().setAll(getSkinnable().addon.getCategories().stream() - .map(category -> i18n("curse.category." + category.getCategoryId())) + .map(category -> getSkinnable().page.getLocalizedCategory(category)) .collect(Collectors.toList())); descriptionPane.getChildren().add(content); @@ -217,8 +222,8 @@ public class DownloadPage extends Control implements DecoratorPage { } } - JFXHyperlink openUrlButton = new JFXHyperlink(i18n("mods.curseforge")); - openUrlButton.setOnAction(e -> FXUtils.openLink(getSkinnable().addon.getWebsiteUrl())); + JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage()); + openUrlButton.setOnAction(e -> FXUtils.openLink(getSkinnable().addon.getPageUrl())); descriptionPane.getChildren().add(openUrlButton); @@ -234,10 +239,10 @@ public class DownloadPage extends Control implements DecoratorPage { } }, getSkinnable().failedProperty())); - JFXListView listView = new JFXListView<>(); + JFXListView listView = new JFXListView<>(); spinnerPane.setContent(listView); Bindings.bindContent(listView.getItems(), getSkinnable().items); - listView.setCellFactory(x -> new FloatListCell(listView) { + listView.setCellFactory(x -> new FloatListCell(listView) { TwoLineListItem content = new TwoLineListItem(); StackPane graphicPane = new StackPane(); JFXButton saveAsButton = new JFXButton(); @@ -255,23 +260,23 @@ public class DownloadPage extends Control implements DecoratorPage { } @Override - protected void updateControl(CurseAddon.LatestFile dataItem, boolean empty) { + protected void updateControl(DownloadManager.Version dataItem, boolean empty) { if (empty) return; - content.setTitle(dataItem.getDisplayName()); - content.setSubtitle(FORMATTER.format(dataItem.getParsedFileDate())); - content.getTags().setAll(dataItem.getGameVersion()); + content.setTitle(dataItem.getName()); + content.setSubtitle(FORMATTER.format(dataItem.getDatePublished())); + content.getTags().setAll(dataItem.getGameVersions()); saveAsButton.setOnMouseClicked(e -> getSkinnable().saveAs(dataItem)); - switch (dataItem.getReleaseType()) { - case 1: // release + switch (dataItem.getVersionType()) { + case Release: graphicPane.getChildren().setAll(SVG.releaseCircleOutline(Theme.blackFillBinding(), 24, 24)); content.getTags().add(i18n("version.game.release")); break; - case 2: // beta + case Beta: graphicPane.getChildren().setAll(SVG.betaCircleOutline(Theme.blackFillBinding(), 24, 24)); content.getTags().add(i18n("version.game.snapshot")); break; - case 3: // alpha + case Alpha: graphicPane.getChildren().setAll(SVG.alphaCircleOutline(Theme.blackFillBinding(), 24, 24)); content.getTags().add(i18n("version.game.snapshot")); break; @@ -282,7 +287,7 @@ public class DownloadPage extends Control implements DecoratorPage { listView.setOnMouseClicked(e -> { if (listView.getSelectionModel().getSelectedIndex() < 0) return; - CurseAddon.LatestFile selectedItem = listView.getSelectionModel().getSelectedItem(); + DownloadManager.Version selectedItem = listView.getSelectionModel().getSelectedItem(); getSkinnable().download(selectedItem); }); } @@ -293,19 +298,7 @@ public class DownloadPage extends Control implements DecoratorPage { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault()); - public interface Project { - - } - - public interface ProjectVersion { - - } - - public interface DownloadSource { - - } - public interface DownloadCallback { - void download(Profile profile, @Nullable String version, CurseAddon.LatestFile file); + void download(Profile profile, @Nullable String version, DownloadManager.Version file); } } 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 index 0f476d1fd..58a19d2d5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java @@ -17,21 +17,29 @@ */ package org.jackhuang.hmcl.ui.versions; +import org.jackhuang.hmcl.mod.DownloadManager; import org.jackhuang.hmcl.mod.curse.CurseAddon; +import org.jackhuang.hmcl.mod.curse.CurseModManager; +import org.jackhuang.hmcl.mod.modrinth.Modrinth; import org.jackhuang.hmcl.util.StringUtils; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class ModDownloadListPage extends DownloadListPage { public ModDownloadListPage(int section, DownloadPage.DownloadCallback callback, boolean versionSelection) { super(section, callback, versionSelection); supportChinese.set(true); + downloadSources.get().setAll("mods.curseforge", "mods.modrinth"); + downloadSource.set("mods.curseforge"); } @Override - protected List searchImpl(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws Exception { + protected Stream searchImpl(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws Exception { if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { List mods = ModTranslations.searchMod(searchFilter); List searchFilters = new ArrayList<>(); @@ -47,9 +55,35 @@ public class ModDownloadListPage extends DownloadListPage { count++; if (count >= 3) break; } - return super.searchImpl(gameVersion, category, section, pageOffset, String.join(" ", searchFilters), sort); + return search(gameVersion, category, section, pageOffset, String.join(" ", searchFilters), sort); } else { - return super.searchImpl(gameVersion, category, section, pageOffset, searchFilter, sort); + return search(gameVersion, category, section, pageOffset, searchFilter, sort); + } + } + + private Stream search(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws Exception { + if ("mods.modrinth".equals(downloadSource.get())) { + return Modrinth.searchPaginated(gameVersion, pageOffset, searchFilter).stream().map(Modrinth.ModResult::toMod); + } else { + return CurseModManager.searchPaginated(gameVersion, category, section, pageOffset, searchFilter, sort).stream().map(CurseAddon::toMod); + } + } + + @Override + protected String getLocalizedCategory(String category) { + if ("mods.modrinth".equals(downloadSource.get())) { + return i18n("modrinth.category." + category); + } else { + return i18n("curse.category." + category); + } + } + + @Override + protected String getLocalizedOfficialPage() { + if ("mods.modrinth".equals(downloadSource.get())) { + return i18n("mods.modrinth"); + } else { + return i18n("mods.curseforge"); } } } 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 7f5bec29a..d9970e90e 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 @@ -27,7 +27,7 @@ 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.DownloadManager; import org.jackhuang.hmcl.mod.curse.CurseModManager; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Theme; @@ -199,19 +199,19 @@ public class VersionPage extends Control implements DecoratorPage, DownloadPage. } @Override - public void download(Profile profile, @Nullable String version, CurseAddon.LatestFile file) { + public void download(Profile profile, @Nullable String version, DownloadManager.Version file) { if (version == null) { throw new InternalError(); } - Path dest = profile.getRepository().getRunDirectory(version).toPath().resolve("mods").resolve(file.getFileName()); + Path dest = profile.getRepository().getRunDirectory(version).toPath().resolve("mods").resolve(file.getFile().getFilename()); TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> { }); TaskExecutor executor = Task.composeAsync(() -> { - FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getDownloadUrl()), dest.toFile()); - task.setName(file.getDisplayName()); + FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile()); + task.setName(file.getName()); return task; }).executor(false); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index 1cd3c1271..b2932f456 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -18,16 +18,14 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; - import javafx.application.Platform; import javafx.stage.FileChooser; - import org.jackhuang.hmcl.auth.Account; 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.mod.DownloadManager; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; @@ -87,15 +85,15 @@ public final class Versions { } } - public static void downloadModpackImpl(Profile profile, String version, CurseAddon.LatestFile file) { + public static void downloadModpackImpl(Profile profile, String version, DownloadManager.Version file) { Path modpack; URL downloadURL; try { modpack = Files.createTempFile("modpack", ".zip"); - downloadURL = new URL(file.getDownloadUrl()); + downloadURL = new URL(file.getFile().getUrl()); } catch (IOException e) { Controllers.dialog( - i18n("install.failed.downloading.detail", file.getDownloadUrl()) + "\n" + StringUtils.getStackTrace(e), + i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e), i18n("download.failed"), MessageDialogPane.MessageType.ERROR); return; } @@ -106,7 +104,7 @@ public final class Versions { Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack.toFile())); } else { Controllers.dialog( - i18n("install.failed.downloading.detail", file.getDownloadUrl()) + "\n" + StringUtils.getStackTrace(e), + i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e), i18n("download.failed"), MessageDialogPane.MessageType.ERROR); } }).executor(true), diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index edba98bda..bc80585c3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -499,6 +499,20 @@ modpack.wizard.step.initialization.save=Export to... modpack.wizard.step.initialization.warning=Before creating a modpack, you should ensure that the game can launch successfully,\nand that your Minecraft is a release version.\nDo NOT add mods which cannot be redistributed. modpack.wizard.step.initialization.server=Click here for more information about server auto-update modpack +modrinth.category.adventure=Adventure +modrinth.category.cursed=Cursed +modrinth.category.decoration=Decoration +modrinth.category.equipment=Equipment +modrinth.category.fabric=Fabric +modrinth.category.food=Food +modrinth.category.library=Library +modrinth.category.magic=Magic +modrinth.category.misc=Misc +modrinth.category.storage=Storage +modrinth.category.technology=Technology +modrinth.category.utility=Utility +modrinth.category.worldgen=Worldgen + mods=Mods mods.add=Install mods mods.add.failed=Failed to install mods %s. @@ -515,6 +529,7 @@ mods.mcbbs=MCBBS mods.mcmod=MCMOD mods.mcmod.page=MCMOD mods.mcmod.search=Search in MCMOD +mods.modrinth=Modrinth mods.name=Name mods.not_modded=You should install a modloader first (Fabric, Forge or LiteLoader) mods.url=Official Page diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 92e4a6984..edd582701 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -506,6 +506,20 @@ modpack.wizard.step.initialization.save=選擇要匯出到的遊戲整合包位 modpack.wizard.step.initialization.warning=在製作整合包前,請您確認您選擇的版本可以正常啟動,\n並保證您的 Minecraft 是正式版而非快照版,\n而且不應將不允許非官方途徑傳播的 Mod 模組、材質包等納入整合包。\n整合包會儲存您目前的下載來源設定 modpack.wizard.step.initialization.server=點選此處查看有關伺服器自動更新整合包的製作教學 +modrinth.category.adventure=冒險 +modrinth.category.cursed=Cursed +modrinth.category.decoration=裝飾 +modrinth.category.equipment=裝備 +modrinth.category.fabric=Fabric +modrinth.category.food=食物 +modrinth.category.library=支持庫 +modrinth.category.magic=魔法 +modrinth.category.misc=其他 +modrinth.category.storage=儲存 +modrinth.category.technology=科技 +modrinth.category.utility=實用 +modrinth.category.worldgen=世界生成 + mods=模組 mods.add=新增模組 mods.add.failed=新增模組 %s 失敗。 @@ -522,6 +536,7 @@ mods.mcbbs=MCBBS mods.mcmod=MC 百科 mods.mcmod.page=MC 百科頁面 mods.mcmod.search=MC 百科蒐索 +mods.modrinth=Modrinth mods.name=名稱 mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge 或 LiteLoader 才能進行模組管理。 mods.url=官方頁面 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 1fd808d40..08643d2be 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -499,6 +499,20 @@ modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位 modpack.wizard.step.initialization.warning=在制作整合包前,请您确认您选择的版本可以正常启动,\n并保证您的 Minecraft 是正式版而非快照版,\n而且不应当将不允许非官方途径传播的 Mod、材质包等纳入整合包。\n整合包会保存您目前的下载源设置 modpack.wizard.step.initialization.server=点击此处查看有关服务器自动更新整合包的制作教程 +modrinth.category.adventure=冒险 +modrinth.category.cursed=Cursed +modrinth.category.decoration=装饰 +modrinth.category.equipment=装备 +modrinth.category.fabric=Fabric +modrinth.category.food=食物 +modrinth.category.library=支持库 +modrinth.category.magic=魔法 +modrinth.category.misc=其他 +modrinth.category.storage=存储 +modrinth.category.technology=科技 +modrinth.category.utility=实用 +modrinth.category.worldgen=世界生成 + mods=模组 mods.add=添加模组 mods.add.failed=添加模组 %s 失败。 @@ -515,6 +529,7 @@ mods.mcbbs=MCBBS mods.mcmod=MC 百科 mods.mcmod.page=MC 百科页面 mods.mcmod.search=MC 百科搜索 +mods.modrinth=Modrinth mods.name=名称 mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。 mods.url=官方页面 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/DownloadManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/DownloadManager.java new file mode 100644 index 000000000..689d276ff --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/DownloadManager.java @@ -0,0 +1,204 @@ +/* + * 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.mod; + +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +public final class DownloadManager { + private DownloadManager() { + } + + public interface IMod { + Stream loadVersions() throws IOException; + } + + public static class Mod { + private final String slug; + private final String author; + private final String title; + private final String description; + private final List categories; + private final String pageUrl; + private final String iconUrl; + private final IMod data; + + public Mod(String slug, String author, String title, String description, List categories, String pageUrl, String iconUrl, IMod data) { + this.slug = slug; + this.author = author; + this.title = title; + this.description = description; + this.categories = categories; + this.pageUrl = pageUrl; + this.iconUrl = iconUrl; + this.data = data; + } + + public String getSlug() { + return slug; + } + + public String getAuthor() { + return author; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public List getCategories() { + return categories; + } + + public String getPageUrl() { + return pageUrl; + } + + public String getIconUrl() { + return iconUrl; + } + + public IMod getData() { + return data; + } + } + + public enum VersionType { + Release, + Beta, + Alpha + } + + public static class Version { + private final Object self; + private final String name; + private final String version; + private final String changelog; + private final Instant datePublished; + private final VersionType versionType; + private final File file; + private final List dependencies; + private final List gameVersions; + private final List loaders; + + public Version(Object self, String name, String version, String changelog, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { + this.self = self; + this.name = name; + this.version = version; + this.changelog = changelog; + this.datePublished = datePublished; + this.versionType = versionType; + this.file = file; + this.dependencies = dependencies; + this.gameVersions = gameVersions; + this.loaders = loaders; + } + + public Object getSelf() { + return self; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public String getChangelog() { + return changelog; + } + + public Instant getDatePublished() { + return datePublished; + } + + public VersionType getVersionType() { + return versionType; + } + + public File getFile() { + return file; + } + + public List getDependencies() { + return dependencies; + } + + public List getGameVersions() { + return gameVersions; + } + + public List getLoaders() { + return loaders; + } + } + + public static class File { + private final Map hashes; + private final String url; + private final String filename; + + public File(Map hashes, String url, String filename) { + this.hashes = hashes; + this.url = url; + this.filename = filename; + } + + public Map getHashes() { + return hashes; + } + + public String getUrl() { + return url; + } + + public String getFilename() { + return filename; + } + } + + public static final String[] DEFAULT_GAME_VERSIONS = new String[]{ + "1.17.1", "1.17", + "1.16.5", "1.16.4", "1.16.3", "1.16.2", "1.16.1", "1.16", + "1.15.2", "1.15.1", "1.15", + "1.14.4", "1.14.3", "1.14.2", "1.14.1", "1.14", + "1.13.2", "1.13.1", "1.13", + "1.12.2", "1.12.1", "1.12", + "1.11.2", "1.11.1", "1.11", + "1.10.2", "1.10.1", "1.10", + "1.9.4", "1.9.3", "1.9.2", "1.9.1", "1.9", + "1.8.9", "1.8.8", "1.8.7", "1.8.6", "1.8.5", "1.8.4", "1.8.3", "1.8.2", "1.8.1", "1.8", + "1.7.10", "1.7.9", "1.7.8", "1.7.7", "1.7.6", "1.7.5", "1.7.4", "1.7.3", "1.7.2", + "1.6.4", "1.6.2", "1.6.1", + "1.5.2", "1.5.1", + "1.4.7", "1.4.6", "1.4.5", "1.4.4", "1.4.2", + "1.3.2", "1.3.1", + "1.2.5", "1.2.4", "1.2.3", "1.2.2", "1.2.1", + "1.1", + "1.0" + }; +} 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 index abbb3432f..594e29e79 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -1,12 +1,34 @@ +/* + * 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.mod.curse; +import org.jackhuang.hmcl.mod.DownloadManager; import org.jackhuang.hmcl.util.Immutable; +import java.io.IOException; import java.time.Instant; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Immutable -public class CurseAddon { +public class CurseAddon implements DownloadManager.IMod { private final int id; private final String name; private final List authors; @@ -137,6 +159,32 @@ public class CurseAddon { return isExperimental; } + @Override + public Stream loadVersions() throws IOException { + return CurseModManager.getFiles(this).stream() + .map(CurseAddon.LatestFile::toVersion); + } + + public DownloadManager.Mod toMod() { + String iconUrl = null; + for (CurseAddon.Attachment attachment : attachments) { + if (attachment.isDefault()) { + iconUrl = attachment.getThumbnailUrl(); + } + } + + return new DownloadManager.Mod( + slug, + "", + name, + summary, + categories.stream().map(category -> Integer.toString(category.getCategoryId())).collect(Collectors.toList()), + websiteUrl, + iconUrl, + this + ); + } + @Immutable public static class Author { private final String name; @@ -410,6 +458,37 @@ public class CurseAddon { } return fileDataInstant; } + + public DownloadManager.Version toVersion() { + DownloadManager.VersionType versionType; + switch (getReleaseType()) { + case 1: + versionType = DownloadManager.VersionType.Release; + break; + case 2: + versionType = DownloadManager.VersionType.Beta; + break; + case 3: + versionType = DownloadManager.VersionType.Alpha; + break; + default: + versionType = DownloadManager.VersionType.Release; + break; + } + + return new DownloadManager.Version( + this, + getDisplayName(), + null, + null, + getParsedFileDate(), + versionType, + new DownloadManager.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), + Collections.emptyList(), + gameVersion, + Collections.emptyList() + ); + } } @Immutable 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 index c6b3b0ad4..3aa4de14e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModManager.java @@ -1,3 +1,20 @@ +/* + * 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.mod.curse; import com.google.gson.reflect.TypeToken; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/Modrinth.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/Modrinth.java new file mode 100644 index 000000000..eb47f5447 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/Modrinth.java @@ -0,0 +1,479 @@ +/* + * 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.mod.modrinth; + +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.mod.DownloadManager; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.HttpRequest; +import org.jackhuang.hmcl.util.io.NetworkUtils; + +import java.io.IOException; +import java.time.Instant; +import java.util.*; +import java.util.stream.Stream; + +import static org.jackhuang.hmcl.util.Lang.mapOf; +import static org.jackhuang.hmcl.util.Pair.pair; + +public final class Modrinth { + private Modrinth() { + } + + public static List searchPaginated(String gameVersion, int pageOffset, String searchFilter) throws IOException { + Map query = mapOf( + pair("query", searchFilter), + pair("offset", Integer.toString(pageOffset)), + pair("limit", "25") + ); + if (StringUtils.isNotBlank(gameVersion)) { + query.put("version", "versions=" + gameVersion); + } + Response response = HttpRequest.GET(NetworkUtils.withQuery("https://api.modrinth.com/api/v1/mod", query)) + .getJson(new TypeToken>() { + }.getType()); + return response.getHits(); + } + + public static List getFiles(ModResult mod) throws IOException { + String id = StringUtils.removePrefix(mod.getModId(), "local-"); + List versions = HttpRequest.GET("https://api.modrinth.com/api/v1/mod/" + id + "/version") + .getJson(new TypeToken>() { + }.getType()); + return versions; + } + + public static List getCategories() throws IOException { + List categories = HttpRequest.GET("https://api.modrinth.com/api/v1/tag/category").getJson(new TypeToken>() { + }.getType()); + return categories; + } + + public static class Mod { + private final String id; + + private final String slug; + + private final String team; + + private final String title; + + private final String description; + + private final Instant published; + + private final Instant updated; + + private final List categories; + + private final List versions; + + private final int downloads; + + @SerializedName("icon_url") + private final String iconUrl; + + public Mod(String id, String slug, String team, String title, String description, Instant published, Instant updated, List categories, List versions, int downloads, String iconUrl) { + this.id = id; + this.slug = slug; + this.team = team; + this.title = title; + this.description = description; + this.published = published; + this.updated = updated; + this.categories = categories; + this.versions = versions; + this.downloads = downloads; + this.iconUrl = iconUrl; + } + + public String getId() { + return id; + } + + public String getSlug() { + return slug; + } + + public String getTeam() { + return team; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public Instant getPublished() { + return published; + } + + public Instant getUpdated() { + return updated; + } + + public List getCategories() { + return categories; + } + + public List getVersions() { + return versions; + } + + public int getDownloads() { + return downloads; + } + + public String getIconUrl() { + return iconUrl; + } + } + + public static class ModVersion { + private final String id; + + @SerializedName("mod_id") + private final String modId; + + @SerializedName("author_id") + private final String authorId; + + private final String name; + + @SerializedName("version_number") + private final String versionNumber; + + private final String changelog; + + @SerializedName("date_published") + private final Instant datePublished; + + private final int downloads; + + @SerializedName("version_type") + private final String versionType; + + private final List files; + + private final List dependencies; + + @SerializedName("game_versions") + private final List gameVersions; + + private final List loaders; + + public ModVersion(String id, String modId, String authorId, String name, String versionNumber, String changelog, Instant datePublished, int downloads, String versionType, List files, List dependencies, List gameVersions, List loaders) { + this.id = id; + this.modId = modId; + this.authorId = authorId; + this.name = name; + this.versionNumber = versionNumber; + this.changelog = changelog; + this.datePublished = datePublished; + this.downloads = downloads; + this.versionType = versionType; + this.files = files; + this.dependencies = dependencies; + this.gameVersions = gameVersions; + this.loaders = loaders; + } + + public String getId() { + return id; + } + + public String getModId() { + return modId; + } + + public String getAuthorId() { + return authorId; + } + + public String getName() { + return name; + } + + public String getVersionNumber() { + return versionNumber; + } + + public String getChangelog() { + return changelog; + } + + public Instant getDatePublished() { + return datePublished; + } + + public int getDownloads() { + return downloads; + } + + public String getVersionType() { + return versionType; + } + + public List getFiles() { + return files; + } + + public List getDependencies() { + return dependencies; + } + + public List getGameVersions() { + return gameVersions; + } + + public List getLoaders() { + return loaders; + } + + public Optional toVersion() { + DownloadManager.VersionType type; + if ("release".equals(versionType)) { + type = DownloadManager.VersionType.Release; + } else if ("beta".equals(versionType)) { + type = DownloadManager.VersionType.Beta; + } else if ("alpha".equals(versionType)) { + type = DownloadManager.VersionType.Alpha; + } else { + type = DownloadManager.VersionType.Release; + } + + if (files.size() == 0) { + return Optional.empty(); + } + + return Optional.of(new DownloadManager.Version( + this, + name, + versionNumber, + changelog, + datePublished, + type, + files.get(0).toFile(), + dependencies, + gameVersions, + loaders + )); + } + } + + public static class ModVersionFile { + private final Map hashes; + private final String url; + private final String filename; + + public ModVersionFile(Map hashes, String url, String filename) { + this.hashes = hashes; + this.url = url; + this.filename = filename; + } + + public Map getHashes() { + return hashes; + } + + public String getUrl() { + return url; + } + + public String getFilename() { + return filename; + } + + public DownloadManager.File toFile() { + return new DownloadManager.File(hashes, url, filename); + } + } + + public static class ModResult implements DownloadManager.IMod { + @SerializedName("mod_id") + private final String modId; + + private final String slug; + + private final String author; + + private final String title; + + private final String description; + + private final List categories; + + private final List versions; + + private final int downloads; + + @SerializedName("page_url") + private final String pageUrl; + + @SerializedName("icon_url") + private final String iconUrl; + + @SerializedName("author_url") + private final String authorUrl; + + @SerializedName("date_created") + private final Instant dateCreated; + + @SerializedName("date_modified") + private final Instant dateModified; + + @SerializedName("latest_version") + private final String latestVersion; + + public ModResult(String modId, String slug, String author, String title, String description, List categories, List versions, int downloads, String pageUrl, String iconUrl, String authorUrl, Instant dateCreated, Instant dateModified, String latestVersion) { + this.modId = modId; + this.slug = slug; + this.author = author; + this.title = title; + this.description = description; + this.categories = categories; + this.versions = versions; + this.downloads = downloads; + this.pageUrl = pageUrl; + this.iconUrl = iconUrl; + this.authorUrl = authorUrl; + this.dateCreated = dateCreated; + this.dateModified = dateModified; + this.latestVersion = latestVersion; + } + + public String getModId() { + return modId; + } + + public String getSlug() { + return slug; + } + + public String getAuthor() { + return author; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public List getCategories() { + return categories; + } + + public List getVersions() { + return versions; + } + + public int getDownloads() { + return downloads; + } + + public String getPageUrl() { + return pageUrl; + } + + public String getIconUrl() { + return iconUrl; + } + + public String getAuthorUrl() { + return authorUrl; + } + + public Instant getDateCreated() { + return dateCreated; + } + + public Instant getDateModified() { + return dateModified; + } + + public String getLatestVersion() { + return latestVersion; + } + + @Override + public Stream loadVersions() throws IOException { + return Modrinth.getFiles(this).stream() + .map(ModVersion::toVersion) + .flatMap(Lang::toStream); + } + + public DownloadManager.Mod toMod() { + return new DownloadManager.Mod( + slug, + author, + title, + description, + categories, + pageUrl, + iconUrl, + this + ); + } + } + + public static class Response { + private final int offset; + + private final int limit; + + @SerializedName("total_hits") + private final int totalHits; + + private final List hits; + + public Response() { + this(0, 0, Collections.emptyList()); + } + + public Response(int offset, int limit, List hits) { + this.offset = offset; + this.limit = limit; + this.totalHits = hits.size(); + this.hits = hits; + } + + public int getOffset() { + return offset; + } + + public int getLimit() { + return limit; + } + + public int getTotalHits() { + return totalHits; + } + + public List getHits() { + return hits; + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 145dd8d98..db4ac804f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -23,6 +23,7 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.*; +import java.util.stream.Stream; /** * @@ -315,6 +316,10 @@ public final class Lang { }; } + public static Stream toStream(Optional optional) { + return optional.map(Stream::of).orElseGet(Stream::empty); + } + /** * This is a useful function to prevent exceptions being eaten when using CompletableFuture. * You can write: diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/InstantTypeAdapter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/InstantTypeAdapter.java new file mode 100644 index 000000000..89468f347 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/InstantTypeAdapter.java @@ -0,0 +1,51 @@ +/* + * 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.util.gson; + +import com.google.gson.*; + +import java.lang.reflect.Type; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public final class InstantTypeAdapter implements JsonSerializer, JsonDeserializer { + public static final InstantTypeAdapter INSTANCE = new InstantTypeAdapter(); + + private InstantTypeAdapter() { + } + + @Override + public Instant deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (!(json instanceof JsonPrimitive)) { + throw new JsonParseException("The instant should be a string value"); + } else { + Instant instant = Instant.parse(json.getAsString()); + if (typeOfT == Instant.class) { + return instant; + } else { + throw new IllegalArgumentException(this.getClass() + " cannot be deserialized to " + typeOfT); + } + } + } + + @Override + public JsonElement serialize(Instant src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneId.systemDefault()).format(src)); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index 995693777..a40d08675 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -24,6 +24,7 @@ import com.google.gson.JsonSyntaxException; import java.io.File; import java.lang.reflect.Type; +import java.time.Instant; import java.util.Date; import java.util.UUID; @@ -63,6 +64,7 @@ public final class JsonUtils { return new GsonBuilder() .enableComplexMapKeySerialization() .setPrettyPrinting() + .registerTypeAdapter(Instant.class, InstantTypeAdapter.INSTANCE) .registerTypeAdapter(Date.class, DateTypeAdapter.INSTANCE) .registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE) .registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java index 834b93bfa..a4bbf5efa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java @@ -87,8 +87,8 @@ public final class NetworkUtils { public static URLConnection createConnection(URL url) throws IOException { URLConnection connection = url.openConnection(); connection.setUseCaches(false); - connection.setConnectTimeout(15000); - connection.setReadTimeout(15000); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); connection.setRequestProperty("Accept-Language", Locale.getDefault().toString()); return connection; } @@ -143,8 +143,8 @@ public final class NetworkUtils { while (true) { conn.setUseCaches(false); - conn.setConnectTimeout(15000); - conn.setReadTimeout(15000); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); conn.setInstanceFollowRedirects(false); Map> properties = conn.getRequestProperties(); String method = conn.getRequestMethod();