优化游戏下载页版本分类功能 (#4141)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Glavo
2025-08-11 16:52:03 +08:00
committed by GitHub
parent de221b9dae
commit ba58905c9c
5 changed files with 248 additions and 266 deletions

View File

@@ -17,24 +17,17 @@
*/ */
package org.jackhuang.hmcl.ui.download; package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.*;
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 javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList; 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.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.*;
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.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable; import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage; import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n; 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.Locale;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; 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.*;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; 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 gameVersion;
private final String libraryId; private final String libraryId;
private final String title; private final String title;
private final Navigation navigation; private final Navigation navigation;
private final JFXListView<RemoteVersion> 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 final VersionList<?> versionList;
private Task<?> executor; private final Runnable callback;
private Task<?> task;
private final HBox searchBar; private final ObservableList<RemoteVersion> versions = FXCollections.observableArrayList();
private final StringProperty queryString = new SimpleStringProperty(); private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) { public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
this.title = title; this.title = title;
this.gameVersion = gameVersion; this.gameVersion = gameVersion;
this.libraryId = libraryId; this.libraryId = libraryId;
this.navigation = navigation; this.navigation = navigation;
this.versionList = downloadProvider.getVersionListById(libraryId);
HintPane hintPane = new HintPane(); this.callback = callback;
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<RemoteVersion> versions = loadVersions();
String query = queryString.get();
if (!StringUtils.isBlank(query)) {
Predicate<RemoteVersion> 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<RemoteVersionListCell> 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();
});
refresh(); refresh();
} }
private List<RemoteVersion> loadVersions() { @Override
return versionList.getVersions(gameVersion).stream() protected Skin<?> createDefaultSkin() {
.filter(it -> { return new VersionsPageSkin(this);
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 @Override
public void refresh() { public void refresh() {
VersionList<?> currentVersionList = versionList; status.set(Status.LOADING);
root.setContent(spinner, ContainerAnimations.FADE); task = versionList.refreshAsync(gameVersion)
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> { .thenSupplyAsync(() -> versionList.getVersions(gameVersion).stream().sorted().collect(Collectors.toList()))
if (exception == null) { .whenComplete(Schedulers.javafx(), (items, exception) -> {
List<RemoteVersion> items = loadVersions(); if (exception == null) {
versions.setAll(items);
Platform.runLater(() -> { status.set(Status.SUCCESS);
if (versionList != currentVersionList) return;
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
root.setContent(emptyPane, ContainerAnimations.FADE);
} else { } else {
if (items.isEmpty()) { LOG.warning("Failed to fetch versions list", exception);
chkRelease.setSelected(true); status.set(Status.FAILED);
chkSnapshot.setSelected(true);
chkOld.setSelected(true);
} else {
list.getItems().setAll(items);
}
root.setContent(center, ContainerAnimations.FADE);
} }
}); });
} else { task.start();
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();
} }
@Override @Override
@@ -350,9 +120,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@Override @Override
public void cleanup(Map<String, Object> settings) { public void cleanup(Map<String, Object> settings) {
settings.remove(libraryId); settings.remove(libraryId);
// fixme if (task != null)
// if (executor != null) task.executor().cancel();
// executor.cancel(true);
} }
private void onRefresh() { private void onRefresh() {
@@ -367,10 +136,23 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
FXUtils.openLink("https://bmclapidoc.bangbang93.com"); 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<RemoteVersion> { private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
final IconedTwoLineListItem content = new IconedTwoLineListItem(); private final IconedTwoLineListItem content = new IconedTwoLineListItem();
final RipplerContainer ripplerContainer = new RipplerContainer(content); private final RipplerContainer ripplerContainer = new RipplerContainer(content);
final StackPane pane = new StackPane(); private final StackPane pane = new StackPane();
private final Holder<RemoteVersionListCell> lastCell; private final Holder<RemoteVersionListCell> lastCell;
@@ -523,4 +305,188 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
return i18n("wiki.version.game.search", id); return i18n("wiki.version.game.search", id);
} }
} }
private static final class VersionsPageSkin extends SkinBase<VersionsPage> {
private final JFXListView<RemoteVersion> list;
private final TransitionPane transitionPane;
private final JFXSpinner spinner;
private final JFXTextField nameField;
private final JFXComboBox<VersionType> 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<RemoteVersionListCell> 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<RemoteVersion> 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()));
}
}
} }

View File

@@ -1428,11 +1428,13 @@ version.empty=No Instances
version.empty.add=Add new instance 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.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.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.old=Historical
version.game.release=Release version.game.release=Release
version.game.releases=Releases version.game.releases=Releases
version.game.snapshot=Snapshot version.game.snapshot=Snapshot
version.game.snapshots=Snapshots version.game.snapshots=Snapshots
version.game.type=Type
version.launch=Launch Game version.launch=Launch Game
version.launch.test=Test Launch version.launch.test=Test Launch
version.switch=Switch Instance version.switch=Switch Instance
@@ -1458,6 +1460,8 @@ version.manage.remove_libraries=Delete All Libraries
version.manage.rename=Rename Instance version.manage.rename=Rename Instance
version.manage.rename.message=Enter New Instance Name 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.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.settings=Settings
version.update=Update Modpack version.update=Update Modpack

View File

@@ -1221,11 +1221,13 @@ version.empty=沒有遊戲實例
version.empty.add=進入下載頁安裝遊戲 version.empty.add=進入下載頁安裝遊戲
version.empty.launch=沒有可啟動的遊戲。點擊「確定」將進入「下載」頁面。\n你也可以點擊 HMCL 主介面左側的「下載」按鈕安裝遊戲,或在「實例清單」切換遊戲目錄。 version.empty.launch=沒有可啟動的遊戲。點擊「確定」將進入「下載」頁面。\n你也可以點擊 HMCL 主介面左側的「下載」按鈕安裝遊戲,或在「實例清單」切換遊戲目錄。
version.empty.hint=沒有已安裝的遊戲。\n你可以切換其他遊戲目錄或者點擊此處進入遊戲下載頁面。 version.empty.hint=沒有已安裝的遊戲。\n你可以切換其他遊戲目錄或者點擊此處進入遊戲下載頁面。
version.game.april_fools=愚人節
version.game.old=遠古版 version.game.old=遠古版
version.game.release=正式版 version.game.release=正式版
version.game.releases=正式版 version.game.releases=正式版
version.game.snapshot=快照 version.game.snapshot=快照
version.game.snapshots=快照 version.game.snapshots=快照
version.game.type=版本類型
version.launch=啟動遊戲 version.launch=啟動遊戲
version.launch.test=測試遊戲 version.launch.test=測試遊戲
version.switch=切換實例 version.switch=切換實例
@@ -1251,6 +1253,8 @@ version.manage.remove_libraries=刪除所有支援庫檔案
version.manage.rename=重新命名該實例 version.manage.rename=重新命名該實例
version.manage.rename.message=請輸入新名稱 version.manage.rename.message=請輸入新名稱
version.manage.rename.fail=重新命名實例失敗,可能檔案被佔用或者名稱有特殊字元。 version.manage.rename.fail=重新命名實例失敗,可能檔案被佔用或者名稱有特殊字元。
version.search=名稱
version.search.prompt=輸入版本名稱進行搜尋
version.settings=遊戲設定 version.settings=遊戲設定
version.update=更新模組包 version.update=更新模組包

View File

@@ -1231,11 +1231,13 @@ version.empty=没有游戏版本
version.empty.add=进入下载页安装游戏 version.empty.add=进入下载页安装游戏
version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“版本列表”切换游戏文件夹。 version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“版本列表”切换游戏文件夹。
version.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹或者点击此处进入游戏下载页面。 version.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹或者点击此处进入游戏下载页面。
version.game.april_fools=愚人节
version.game.old=远古版 version.game.old=远古版
version.game.release=正式版 version.game.release=正式版
version.game.releases=正式版 version.game.releases=正式版
version.game.snapshot=快照 version.game.snapshot=快照
version.game.snapshots=快照 version.game.snapshots=快照
version.game.type=版本类型
version.launch=启动游戏 version.launch=启动游戏
version.launch.test=测试游戏 version.launch.test=测试游戏
version.switch=切换版本 version.switch=切换版本
@@ -1261,6 +1263,8 @@ version.manage.remove_libraries=删除所有库文件
version.manage.rename=重命名该版本 version.manage.rename=重命名该版本
version.manage.rename.message=请输入要修改的名称 version.manage.rename.message=请输入要修改的名称
version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符。 version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符。
version.search=名称
version.search.prompt=输入版本名称进行搜索
version.settings=游戏设置 version.settings=游戏设置
version.update=更新整合包 version.update=更新整合包

View File

@@ -98,6 +98,10 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
this.value = value; this.value = value;
} }
public boolean isSpecial() {
return this instanceof Special;
}
enum Type { enum Type {
PRE_CLASSIC, CLASSIC, INFDEV, ALPHA, BETA, NEW PRE_CLASSIC, CLASSIC, INFDEV, ALPHA, BETA, NEW
} }