diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index 8a9f7cbb4..a4ad59dee 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -17,24 +17,17 @@ */ package org.jackhuang.hmcl.ui.download; -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXCheckBox; -import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXSpinner; -import com.jfoenix.controls.JFXTextField; -import javafx.animation.PauseTransition; -import javafx.application.Platform; +import com.jfoenix.controls.*; import javafx.beans.InvalidationListener; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; +import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.*; -import javafx.util.Duration; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.download.VersionList; @@ -55,291 +48,68 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.ComponentList; -import org.jackhuang.hmcl.ui.construct.HintPane; -import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem; -import org.jackhuang.hmcl.ui.construct.RipplerContainer; +import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.Refreshable; import org.jackhuang.hmcl.ui.wizard.WizardPage; import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; -import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; -import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap; +import static org.jackhuang.hmcl.ui.FXUtils.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public final class VersionsPage extends BorderPane implements WizardPage, Refreshable { +public final class VersionsPage extends Control implements WizardPage, Refreshable { private final String gameVersion; private final String libraryId; private final String title; private final Navigation navigation; - - private final JFXListView list; - private final JFXSpinner spinner; - private final StackPane failedPane; - private final StackPane emptyPane; - private final TransitionPane root; - private final JFXCheckBox chkRelease; - private final JFXCheckBox chkSnapshot; - private final JFXCheckBox chkOld; - private final ComponentList centrePane; - private final StackPane center; - private final VersionList versionList; - private Task executor; + private final Runnable callback; + private Task task; - private final HBox searchBar; - private final StringProperty queryString = new SimpleStringProperty(); + private final ObservableList versions = FXCollections.observableArrayList(); + private final ObjectProperty status = new SimpleObjectProperty<>(Status.LOADING); public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) { this.title = title; this.gameVersion = gameVersion; this.libraryId = libraryId; this.navigation = navigation; - - HintPane hintPane = new HintPane(); - hintPane.setText(i18n("sponsor.bmclapi")); - hintPane.getStyleClass().add("sponsor-pane"); - FXUtils.onClicked(hintPane, this::onSponsor); - BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10)); - this.setTop(hintPane); - - root = new TransitionPane(); - BorderPane toolbarPane = new BorderPane(); - JFXButton btnRefresh; - { - spinner = new JFXSpinner(); - - center = new StackPane(); - center.setStyle("-fx-padding: 10;"); - { - centrePane = new ComponentList(); - centrePane.getStyleClass().add("no-padding"); - { - HBox checkPane = new HBox(); - checkPane.setSpacing(10); - { - chkRelease = new JFXCheckBox(i18n("version.game.releases")); - chkRelease.setSelected(true); - HBox.setMargin(chkRelease, new Insets(10, 0, 10, 0)); - - chkSnapshot = new JFXCheckBox(i18n("version.game.snapshots")); - HBox.setMargin(chkSnapshot, new Insets(10, 0, 10, 0)); - - chkOld = new JFXCheckBox(i18n("version.game.old")); - HBox.setMargin(chkOld, new Insets(10, 0, 10, 0)); - - checkPane.getChildren().setAll(chkRelease, chkSnapshot, chkOld); - } - - list = new JFXListView<>(); - list.getStyleClass().add("jfx-list-view-float"); - VBox.setVgrow(list, Priority.ALWAYS); - - TransitionPane rightToolbarPane = new TransitionPane(); - { - HBox refreshPane = new HBox(); - refreshPane.setAlignment(Pos.CENTER_RIGHT); - - btnRefresh = new JFXButton(i18n("button.refresh")); - btnRefresh.getStyleClass().add("jfx-tool-bar-button"); - btnRefresh.setOnAction(e -> onRefresh()); - - JFXButton btnSearch = new JFXButton(i18n("search")); - btnSearch.getStyleClass().add("jfx-tool-bar-button"); - btnSearch.setGraphic(wrap(SVG.SEARCH.createIcon(Theme.blackFill(), -1))); - - searchBar = new HBox(); - { - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 0)); - - JFXTextField searchField = new JFXTextField(); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - - JFXButton closeSearchBar = new JFXButton(); - closeSearchBar.getStyleClass().add("jfx-tool-bar-button"); - closeSearchBar.setGraphic(wrap(SVG.CLOSE.createIcon(Theme.blackFill(), -1))); - closeSearchBar.setOnAction(e -> { - searchField.clear(); - rightToolbarPane.setContent(refreshPane, ContainerAnimations.FADE); - }); - onEscPressed(searchField, closeSearchBar::fire); - PauseTransition pause = new PauseTransition(Duration.millis(100)); - pause.setOnFinished(e -> queryString.set(searchField.getText())); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - pause.setRate(1); - pause.playFromStart(); - }); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - btnSearch.setOnAction(e -> { - rightToolbarPane.setContent(searchBar, ContainerAnimations.FADE); - searchField.requestFocus(); - }); - } - - refreshPane.getChildren().setAll(new HBox(btnSearch, btnRefresh)); - rightToolbarPane.setContent(refreshPane, ContainerAnimations.NONE); - } - - // ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here - ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - toolbarPane.setLeft(checkPane); - toolbarPane.setRight(rightToolbarPane); - - centrePane.getContent().setAll(toolbarPane, list); - } - - center.getChildren().setAll(centrePane); - } - - failedPane = new StackPane(); - failedPane.getStyleClass().add("notice-pane"); - { - Label label = new Label(i18n("download.failed.refresh")); - FXUtils.onClicked(label, this::onRefresh); - - failedPane.getChildren().setAll(label); - } - - emptyPane = new StackPane(); - emptyPane.getStyleClass().add("notice-pane"); - { - Label label = new Label(i18n("download.failed.empty")); - FXUtils.onClicked(label, this::onBack); - - emptyPane.getChildren().setAll(label); - } - } - this.setCenter(root); - - versionList = downloadProvider.getVersionListById(libraryId); - boolean hasType = versionList.hasType(); - chkRelease.setManaged(hasType); - chkRelease.setVisible(hasType); - chkSnapshot.setManaged(hasType); - chkSnapshot.setVisible(hasType); - chkOld.setManaged(hasType); - chkOld.setVisible(hasType); - - if (hasType) { - centrePane.getContent().setAll(toolbarPane, list); - } else { - centrePane.getContent().setAll(list); - } - ComponentList.setVgrow(list, Priority.ALWAYS); - - InvalidationListener listener = o -> { - List versions = loadVersions(); - String query = queryString.get(); - if (!StringUtils.isBlank(query)) { - Predicate predicate; - if (query.startsWith("regex:")) { - try { - Pattern pattern = Pattern.compile(query.substring("regex:".length())); - predicate = it -> pattern.matcher(it.getSelfVersion()).find(); - } catch (Throwable e) { - LOG.warning("Illegal regular expression", e); - return; - } - } else { - String lowerQueryString = query.toLowerCase(Locale.ROOT); - predicate = it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString); - } - - versions = versions.stream().filter(predicate).collect(Collectors.toList()); - } - - list.getItems().setAll(versions); - }; - chkRelease.selectedProperty().addListener(listener); - chkSnapshot.selectedProperty().addListener(listener); - chkOld.selectedProperty().addListener(listener); - queryString.addListener(listener); - - btnRefresh.setGraphic(wrap(SVG.REFRESH.createIcon(Theme.blackFill(), -1))); - - Holder lastCell = new Holder<>(); - list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, libraryId)); - - FXUtils.onClicked(list, () -> { - if (list.getSelectionModel().getSelectedIndex() < 0) - return; - navigation.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem()); - callback.run(); - }); + this.versionList = downloadProvider.getVersionListById(libraryId); + this.callback = callback; refresh(); } - private List loadVersions() { - return versionList.getVersions(gameVersion).stream() - .filter(it -> { - switch (it.getVersionType()) { - case RELEASE: - return chkRelease.isSelected(); - case PENDING: - case SNAPSHOT: - return chkSnapshot.isSelected(); - case OLD: - return chkOld.isSelected(); - default: - return true; - } - }) - .sorted().collect(Collectors.toList()); + @Override + protected Skin createDefaultSkin() { + return new VersionsPageSkin(this); } @Override public void refresh() { - VersionList currentVersionList = versionList; - root.setContent(spinner, ContainerAnimations.FADE); - executor = currentVersionList.refreshAsync(gameVersion).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> { - if (exception == null) { - List items = loadVersions(); - - Platform.runLater(() -> { - if (versionList != currentVersionList) return; - if (currentVersionList.getVersions(gameVersion).isEmpty()) { - root.setContent(emptyPane, ContainerAnimations.FADE); + status.set(Status.LOADING); + task = versionList.refreshAsync(gameVersion) + .thenSupplyAsync(() -> versionList.getVersions(gameVersion).stream().sorted().collect(Collectors.toList())) + .whenComplete(Schedulers.javafx(), (items, exception) -> { + if (exception == null) { + versions.setAll(items); + status.set(Status.SUCCESS); } else { - if (items.isEmpty()) { - chkRelease.setSelected(true); - chkSnapshot.setSelected(true); - chkOld.setSelected(true); - } else { - list.getItems().setAll(items); - } - root.setContent(center, ContainerAnimations.FADE); + LOG.warning("Failed to fetch versions list", exception); + status.set(Status.FAILED); } }); - } else { - LOG.warning("Failed to fetch versions list", exception); - Platform.runLater(() -> { - if (versionList != currentVersionList) return; - root.setContent(failedPane, ContainerAnimations.FADE); - }); - } - - // https://github.com/HMCL-dev/HMCL/issues/938 - System.gc(); - }); - executor.start(); + task.start(); } @Override @@ -350,9 +120,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres @Override public void cleanup(Map settings) { settings.remove(libraryId); - // fixme -// if (executor != null) -// executor.cancel(true); + if (task != null) + task.executor().cancel(); } private void onRefresh() { @@ -367,10 +136,23 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres FXUtils.openLink("https://bmclapidoc.bangbang93.com"); } + private enum Status { + LOADING, + FAILED, + SUCCESS, + } + + private enum VersionType { + RELEASE, + SNAPSHOTS, + APRIL_FOOLS, + OLD + } + private static class RemoteVersionListCell extends ListCell { - final IconedTwoLineListItem content = new IconedTwoLineListItem(); - final RipplerContainer ripplerContainer = new RipplerContainer(content); - final StackPane pane = new StackPane(); + private final IconedTwoLineListItem content = new IconedTwoLineListItem(); + private final RipplerContainer ripplerContainer = new RipplerContainer(content); + private final StackPane pane = new StackPane(); private final Holder lastCell; @@ -523,4 +305,188 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres return i18n("wiki.version.game.search", id); } } + + private static final class VersionsPageSkin extends SkinBase { + private final JFXListView list; + + private final TransitionPane transitionPane; + private final JFXSpinner spinner; + + private final JFXTextField nameField; + private final JFXComboBox categoryField = new JFXComboBox<>(); + + VersionsPageSkin(VersionsPage control) { + super(control); + + BorderPane root = new BorderPane(); + + GridPane searchPane = new GridPane(); + if (control.versionList.hasType()) + root.setTop(searchPane); + searchPane.getStyleClass().addAll("card"); + BorderPane.setMargin(searchPane, new Insets(10, 10, 0, 10)); + + ColumnConstraints nameColumn = new ColumnConstraints(); + nameColumn.setMinWidth(USE_PREF_SIZE); + ColumnConstraints column1 = new ColumnConstraints(); + column1.setHgrow(Priority.ALWAYS); + ColumnConstraints column2 = new ColumnConstraints(); + column2.setMaxWidth(150); + ColumnConstraints column3 = new ColumnConstraints(); + searchPane.getColumnConstraints().setAll(nameColumn, column1, nameColumn, column2, column3); + + searchPane.setHgap(16); + searchPane.setVgap(10); + + { + int rowIndex = 0; + + { + nameField = new JFXTextField(); + nameField.setPromptText(i18n("version.search.prompt")); + nameField.textProperty().addListener(o -> updateList()); + + categoryField.getItems().addAll(VersionType.values()); + categoryField.setConverter(stringConverter(type -> i18n("version.game." + type.name().toLowerCase(Locale.ROOT)))); + categoryField.getSelectionModel().select(0); + categoryField.getSelectionModel().selectedItemProperty().addListener(o -> updateList()); + + JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh")); + refreshButton.setOnAction(event -> control.onRefresh()); + + searchPane.addRow(rowIndex++, + new Label(i18n("version.search")), nameField, + new Label(i18n("version.game.type")), categoryField, + refreshButton + ); + } + +// { +// HBox actionsBox = new HBox(8); +// GridPane.setColumnSpan(actionsBox, 4); +// actionsBox.setAlignment(Pos.CENTER_RIGHT); +// +// JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh")); +// refreshButton.setOnAction(event -> control.onRefresh()); +// +// actionsBox.getChildren().setAll(refreshButton); +// +// searchPane.addRow(rowIndex++, actionsBox); +// } + } + + { + SpinnerPane spinnerPane = new SpinnerPane(); + root.setCenter(spinnerPane); + + transitionPane = new TransitionPane(); + spinner = new JFXSpinner(); + + StackPane centerWrapper = new StackPane(); + centerWrapper.setStyle("-fx-padding: 10;"); + { + ComponentList centrePane = new ComponentList(); + centrePane.getStyleClass().add("no-padding"); + { + list = new JFXListView<>(); + list.getStyleClass().add("jfx-list-view-float"); + VBox.setVgrow(list, Priority.ALWAYS); + + control.versions.addListener((InvalidationListener) o -> updateList()); + + Holder lastCell = new Holder<>(); + list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control.libraryId)); + + FXUtils.onClicked(list, () -> { + if (list.getSelectionModel().getSelectedIndex() < 0) + return; + control.navigation.getSettings().put(control.libraryId, list.getSelectionModel().getSelectedItem()); + control.callback.run(); + }); + + ComponentList.setVgrow(list, Priority.ALWAYS); + + // ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here + ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + + centrePane.getContent().setAll(list); + } + + centerWrapper.getChildren().setAll(centrePane); + } + + StackPane failedPane = new StackPane(); + failedPane.getStyleClass().add("notice-pane"); + { + Label label = new Label(i18n("download.failed.refresh")); + FXUtils.onClicked(label, control::onRefresh); + + failedPane.getChildren().setAll(label); + } + + StackPane emptyPane = new StackPane(); + emptyPane.getStyleClass().add("notice-pane"); + { + Label label = new Label(i18n("download.failed.empty")); + FXUtils.onClicked(label, control::onBack); + + emptyPane.getChildren().setAll(label); + } + + FXUtils.onChangeAndOperate(control.status, status -> { + if (status == Status.LOADING) + transitionPane.setContent(spinner, ContainerAnimations.FADE); + else if (status == Status.SUCCESS) + transitionPane.setContent(centerWrapper, ContainerAnimations.FADE); + else // if (status == Status.FAILED) + transitionPane.setContent(failedPane, ContainerAnimations.FADE); + }); + + root.setCenter(transitionPane); + } + + this.getChildren().setAll(root); + } + + private void updateList() { + Stream versions = getSkinnable().versions.stream(); + + VersionType versionType = categoryField.getSelectionModel().getSelectedItem(); + if (versionType != null) + versions = versions.filter(it -> { + switch (it.getVersionType()) { + case RELEASE: + return versionType == VersionType.RELEASE; + case PENDING: + return versionType == VersionType.SNAPSHOTS; + case SNAPSHOT: + return versionType == (GameVersionNumber.asGameVersion(it.getGameVersion()).isSpecial() + ? VersionType.APRIL_FOOLS + : VersionType.SNAPSHOTS); + case OLD: + return versionType == VersionType.OLD; + default: + return true; + } + }); + + String nameQuery = nameField.getText(); + if (!StringUtils.isBlank(nameQuery)) { + if (nameQuery.startsWith("regex:")) { + try { + Pattern pattern = Pattern.compile(nameQuery.substring("regex:".length())); + versions = versions.filter(it -> pattern.matcher(it.getSelfVersion()).find()); + } catch (Throwable e) { + LOG.warning("Illegal regular expression: " + nameQuery, e); + } + } else { + String lowerQueryString = nameQuery.toLowerCase(Locale.ROOT); + versions = versions.filter(it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString)); + } + } + + //noinspection DataFlowIssue + list.getItems().setAll(versions.collect(Collectors.toList())); + } + } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 45f864ef7..a64e0a31b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1428,11 +1428,13 @@ version.empty=No Instances version.empty.add=Add new instance version.empty.launch=No available instances. Clicking "OK" will take you to the "Download" page.\n\nYou can also download the game or switch game directories via the "Download" or "All Instances" buttons on the HMCL homepage. version.empty.hint=There are no Minecraft instances here.\nYou can try switching to another game directory or clicking here to download one. +version.game.april_fools=April Fools version.game.old=Historical version.game.release=Release version.game.releases=Releases version.game.snapshot=Snapshot version.game.snapshots=Snapshots +version.game.type=Type version.launch=Launch Game version.launch.test=Test Launch version.switch=Switch Instance @@ -1458,6 +1460,8 @@ version.manage.remove_libraries=Delete All Libraries version.manage.rename=Rename Instance version.manage.rename.message=Enter New Instance Name version.manage.rename.fail=Failed to rename the instance. Some files might be in use, or the name contains an invalid character. +version.search=Name +version.search.prompt=Enter the version name to search version.settings=Settings version.update=Update Modpack diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 99342e9fc..3a2d831d2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1221,11 +1221,13 @@ version.empty=沒有遊戲實例 version.empty.add=進入下載頁安裝遊戲 version.empty.launch=沒有可啟動的遊戲。點擊「確定」將進入「下載」頁面。\n你也可以點擊 HMCL 主介面左側的「下載」按鈕安裝遊戲,或在「實例清單」切換遊戲目錄。 version.empty.hint=沒有已安裝的遊戲。\n你可以切換其他遊戲目錄,或者點擊此處進入遊戲下載頁面。 +version.game.april_fools=愚人節 version.game.old=遠古版 version.game.release=正式版 version.game.releases=正式版 version.game.snapshot=快照 version.game.snapshots=快照 +version.game.type=版本類型 version.launch=啟動遊戲 version.launch.test=測試遊戲 version.switch=切換實例 @@ -1251,6 +1253,8 @@ version.manage.remove_libraries=刪除所有支援庫檔案 version.manage.rename=重新命名該實例 version.manage.rename.message=請輸入新名稱 version.manage.rename.fail=重新命名實例失敗,可能檔案被佔用或者名稱有特殊字元。 +version.search=名稱 +version.search.prompt=輸入版本名稱進行搜尋 version.settings=遊戲設定 version.update=更新模組包 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 888375b98..b77fa243f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1231,11 +1231,13 @@ version.empty=没有游戏版本 version.empty.add=进入下载页安装游戏 version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“版本列表”切换游戏文件夹。 version.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹,或者点击此处进入游戏下载页面。 +version.game.april_fools=愚人节 version.game.old=远古版 version.game.release=正式版 version.game.releases=正式版 version.game.snapshot=快照 version.game.snapshots=快照 +version.game.type=版本类型 version.launch=启动游戏 version.launch.test=测试游戏 version.switch=切换版本 @@ -1261,6 +1263,8 @@ version.manage.remove_libraries=删除所有库文件 version.manage.rename=重命名该版本 version.manage.rename.message=请输入要修改的名称 version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符。 +version.search=名称 +version.search.prompt=输入版本名称进行搜索 version.settings=游戏设置 version.update=更新整合包 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java index fff98ea6a..4239702f1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java @@ -98,6 +98,10 @@ public abstract class GameVersionNumber implements Comparable this.value = value; } + public boolean isSpecial() { + return this instanceof Special; + } + enum Type { PRE_CLASSIC, CLASSIC, INFDEV, ALPHA, BETA, NEW }