diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java index 402fb6409..9394ddc21 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java @@ -17,15 +17,21 @@ */ package org.jackhuang.hmcl.ui.versions; +import com.jfoenix.controls.JFXTextField; +import javafx.animation.PauseTransition; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javafx.util.Duration; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; @@ -40,6 +46,8 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList; import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; @@ -54,6 +62,8 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage private final ObjectProperty selectedProfile; private ToggleGroup toggleGroup; + private final TextField searchField; + private final GameList gameList; public GameListPage() { profileListItems = MappedObservableList.create(profilesProperty(), profile -> { @@ -63,7 +73,36 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage }); selectedProfile = createSelectedItemPropertyFor(profileListItems, Profile.class); - GameList gameList = new GameList(); + gameList = new GameList(); + + HBox searchBar = new HBox(); + searchBar.setPadding(new Insets(12, 10, 0, 10)); + searchField = new JFXTextField(); + searchField.setPromptText(i18n("search.hint.regex")); + HBox.setHgrow(searchField, Priority.ALWAYS); + PauseTransition pause = new PauseTransition(Duration.millis(100)); + pause.setOnFinished(e -> gameList.filter(searchField.getText())); + searchField.textProperty().addListener((obs, oldText, newText) -> { + pause.setRate(1); + pause.playFromStart(); + }); + searchBar.getChildren().setAll(searchField); + + VBox centerBox = new VBox(); + if (gameList.getItems().isEmpty() && searchField.getText().isEmpty()) { + setCenter(gameList); + } else { + centerBox.getChildren().setAll(searchBar, gameList); + setCenter(centerBox); + } + gameList.itemsProperty().addListener((obs, oldItems, newItems) -> { + if (newItems.isEmpty() && searchField.getText().isEmpty()) { + setCenter(gameList); + } else { + centerBox.getChildren().setAll(searchBar, gameList); + setCenter(centerBox); + } + }); { ScrollPane pane = new ScrollPane(); @@ -94,8 +133,6 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 4 + 12 * 2); setLeft(pane, bottomLeftCornerList); } - - setCenter(gameList); } public ObjectProperty selectedProfileProperty() { @@ -124,12 +161,17 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage } private class GameList extends ListPageBase { + private final ObjectProperty filter = new SimpleObjectProperty<>(""); + private final ObservableList originalItems = FXCollections.observableArrayList(); + public GameList() { super(); Profiles.registerVersionsListener(this::loadVersions); setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage())); + + filter.addListener((obs, oldFilter, newFilter) -> applyFilter(newFilter)); } private void loadVersions(Profile profile) { @@ -145,10 +187,20 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage List children = repository.getDisplayVersions() .map(version -> new GameListItem(toggleGroup, profile, version.getId())) .collect(Collectors.toList()); + + originalItems.setAll(children); + itemsProperty().setAll(children); children.forEach(GameListItem::checkSelection); + if (!center.getChildren().isEmpty()) { + searchField.clear(); + } + if (children.isEmpty()) { + if (!center.getChildren().isEmpty()) { + setCenter(gameList); + } setFailedReason(i18n("version.empty.hint")); } @@ -173,6 +225,37 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start(); } + public void filter(String searchText) { + filter.set(searchText); + } + + private void applyFilter(String filterText) { + if (filterText.startsWith("regex:")) { + try { + String regex = filterText.substring("regex:".length()); + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + + List filteredItems = originalItems.stream() + .filter(item -> pattern.matcher(item.getVersion()).find()) + .collect(Collectors.toList()); + + itemsProperty().setAll(filteredItems); + } catch (PatternSyntaxException e) { + System.err.println("Invalid regex pattern: " + e.getMessage()); + } + } else { + String lowerCaseFilterText = filterText.toLowerCase(); + if (filterText.isEmpty()) { + itemsProperty().setAll(originalItems); + } else { + List filteredItems = originalItems.stream() + .filter(item -> item.getVersion().toLowerCase().contains(lowerCaseFilterText)) + .collect(Collectors.toList()); + itemsProperty().setAll(filteredItems); + } + } + } + @Override protected GameListSkin createDefaultSkin() { return new GameListSkin(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a237e15b8..20bf02816 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1202,6 +1202,7 @@ schematics.manage=Schematics schematics.sub_items=%d sub-item(s) search=Search +search.hint.regex=Support regex search (regex:+regular expression) search.hint.chinese=Search in English and Chinese search.hint.english=Search in English only search.enter=Enter text here diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 90c3b63fd..171a6d9e1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1004,6 +1004,7 @@ schematics.manage=原理圖管理 schematics.sub_items=%d 個子項 search=搜尋 +search.hint.regex=支援正規表示式搜尋(regex:+正規表示式) search.hint.chinese=支援中英文搜尋 search.hint.english=僅支援英文搜尋 search.enter=請在此處輸入 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 de1c55bcf..a2c0bdf3b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1014,6 +1014,7 @@ schematics.manage=原理图管理 schematics.sub_items=%d 个子项 search=搜索 +search.hint.regex=支持正则表达式搜索(regex:+正则表达式) search.hint.chinese=支持中英文搜索 search.hint.english=仅支持英文搜索 search.enter=可在此处输入