From 3c45143d76d400b1dcb58e84b5e2557642db8851 Mon Sep 17 00:00:00 2001 From: Natalia Cholewa Date: Fri, 19 Dec 2025 19:59:29 +0100 Subject: [PATCH 1/2] feat: client side search box --- client/src/main/java/client/MyModule.java | 2 + .../client/scenes/FoodpalApplicationCtrl.java | 35 +++- .../java/client/scenes/SearchBarCtrl.java | 150 ++++++++++++++++++ .../main/java/client/utils/ServerUtils.java | 5 + .../client/scenes/FoodpalApplication.fxml | 2 + .../resources/client/scenes/SearchBar.fxml | 11 ++ .../main/resources/locale/lang_en.properties | 2 + .../main/resources/locale/lang_nl.properties | 2 + .../main/resources/locale/lang_pl.properties | 2 + 9 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 client/src/main/java/client/scenes/SearchBarCtrl.java create mode 100644 client/src/main/resources/client/scenes/SearchBar.fxml diff --git a/client/src/main/java/client/MyModule.java b/client/src/main/java/client/MyModule.java index 4f3464e..98d878a 100644 --- a/client/src/main/java/client/MyModule.java +++ b/client/src/main/java/client/MyModule.java @@ -16,6 +16,7 @@ package client; import client.scenes.FoodpalApplicationCtrl; +import client.scenes.SearchBarCtrl; import client.scenes.recipe.IngredientListCtrl; import client.scenes.recipe.RecipeStepListCtrl; import client.utils.LocaleManager; @@ -33,6 +34,7 @@ public class MyModule implements Module { binder.bind(FoodpalApplicationCtrl.class).in(Scopes.SINGLETON); binder.bind(IngredientListCtrl.class).in(Scopes.SINGLETON); binder.bind(RecipeStepListCtrl.class).in(Scopes.SINGLETON); + binder.bind(SearchBarCtrl.class).in(Scopes.SINGLETON); binder.bind(LocaleManager.class).in(Scopes.SINGLETON); } } \ No newline at end of file diff --git a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java index bcb6f44..2c796cf 100644 --- a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java +++ b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java @@ -35,6 +35,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { private final LocaleManager localeManager; private final IngredientListCtrl ingredientListCtrl; private final RecipeStepListCtrl stepListCtrl; + private final SearchBarCtrl searchBarCtrl; public VBox detailsScreen; public HBox editableTitleArea; @@ -83,13 +84,41 @@ public class FoodpalApplicationCtrl implements LocaleAware { ServerUtils server, LocaleManager localeManager, IngredientListCtrl ingredientListCtrl, - RecipeStepListCtrl stepListCtrl + RecipeStepListCtrl stepListCtrl, + SearchBarCtrl searchBarCtrl ) { this.server = server; this.localeManager = localeManager; this.ingredientListCtrl = ingredientListCtrl; this.stepListCtrl = stepListCtrl; + this.searchBarCtrl = searchBarCtrl; } + + private void initializeSearchBar() { + // Refresh on search bar change + this.searchBarCtrl.setOnSearch(recipes -> { + // Don't lose selection on refresh + Recipe currentlySelected = recipeList.getSelectionModel().getSelectedItem(); + int newIndex = -1; + + if (recipes.contains(currentlySelected)) { + newIndex = recipes.indexOf(currentlySelected); + } + + this.recipeList.getItems().setAll(recipes); + + System.out.println("Search returned " + recipes.size() + " recipes."); + + // Restore selection, if possible + if (newIndex != -1) { + this.recipeList.getSelectionModel().select(newIndex); + } else if (!recipes.isEmpty()) { + // Otherwise select first item + this.recipeList.getSelectionModel().selectFirst(); + } + }); + } + @Override public void initializeComponents() { // TODO Reduce code duplication?? @@ -144,6 +173,8 @@ public class FoodpalApplicationCtrl implements LocaleAware { } }); + this.initializeSearchBar(); + refresh(); } private void showName(String name) { @@ -192,7 +223,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { public void refresh() { List recipes; try { - recipes = server.getRecipes(); + recipes = server.getRecipesFiltered(searchBarCtrl.getFilter()); } catch (IOException | InterruptedException e) { recipes = Collections.emptyList(); System.err.println("Failed to load recipes: " + e.getMessage()); diff --git a/client/src/main/java/client/scenes/SearchBarCtrl.java b/client/src/main/java/client/scenes/SearchBarCtrl.java new file mode 100644 index 0000000..4f6113b --- /dev/null +++ b/client/src/main/java/client/scenes/SearchBarCtrl.java @@ -0,0 +1,150 @@ +package client.scenes; + +import client.utils.LocaleAware; +import client.utils.LocaleManager; +import client.utils.ServerUtils; +import com.google.inject.Inject; +import commons.Recipe; +import javafx.animation.PauseTransition; +import javafx.concurrent.Task; +import javafx.fxml.FXML; +import javafx.scene.control.TextField; +import javafx.util.Duration; + +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; + +/** + * Controller for the search bar component. + * Manages the search input field and performs searches with debounce. + */ +public class SearchBarCtrl implements LocaleAware { + /** + * How many milliseconds to wait after the user's last keypress + * before sending an actual search request. + */ + private final static int SEARCH_DELAY_MS = 300; + + private final LocaleManager localeManager; + private final ServerUtils serverUtils; + + private Consumer> onSearchCallback; + + /** + * Debounce timer to limit search frequency. + * + * @see SearchBarCtrl#initializeComponents() + */ + private final PauseTransition searchDebounce = new PauseTransition(Duration.millis(SEARCH_DELAY_MS)); + + private Task> currentSearchTask = null; + + @Inject + public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils) { + this.localeManager = localeManager; + this.serverUtils = serverUtils; + } + + @FXML + private TextField searchField; + + /** + * Set the callback function to be called when a search is performed. + * + * @param callback The callback function. Receives the list of recipes + * matching the search filter. + */ + public void setOnSearch(Consumer> callback) { + this.onSearchCallback = callback; + } + + /** + * Get the current filter text - aka the text in the search field. + * + * @return The current filter text. "" if empty. + */ + public String getFilter() { + if (this.searchField == null || this.searchField.getText() == null) { + return ""; + } + + return this.searchField.getText(); + } + + /** + * Perform a search with the current filter text. + * If a previous search is still running, it will be cancelled. + */ + private void onSearch() { + if (this.onSearchCallback == null) { + return; + } + + // Cancel any previously running search tasks. + if (currentSearchTask != null && currentSearchTask.isRunning()) { + currentSearchTask.cancel(); + } + + String filter = this.getFilter(); + + // Spawn a new background task. This allows us to continue + // to update the UI while waiting for the server to respond. + // + // Basically this just makes it feel less laggy and doesn't halt + // the entire application while searching. + currentSearchTask = new Task<>() { + @Override + protected List call() throws IOException, InterruptedException { + return serverUtils.getRecipesFiltered(filter); + } + }; + + currentSearchTask.setOnSucceeded(e -> { + List recipes = currentSearchTask.getValue(); + this.onSearchCallback.accept(recipes); + }); + + // Don't do anything on failure - just log the error + currentSearchTask.setOnFailed(e -> { + System.err.println("Failed to fetch recipes: " + currentSearchTask.getException().getMessage()); + }); + + // This could probably be done better with some sort + // of thread pool / executor service, but for now + // this *should* work fine, especially given the debounce time. + new Thread(currentSearchTask).start(); + } + + @Override + public void updateText() { + this.searchField.setPromptText( + this.getLocaleString("menu.search") + ); + } + + @Override + public LocaleManager getLocaleManager() { + return this.localeManager; + } + + @Override + public void initializeComponents() { + // Set up the debounce for the search field. + // + // This makes it so that we only perform a search + // after the user has stopped typing for a short + // period of time, instead of on every single key press. + // + // This way, if a user is typing quickly, we don't + // spam the server with requests! + searchDebounce.setOnFinished(e -> { + this.onSearch(); + }); + + this.searchField.setOnKeyReleased(event -> { + // This cancels the current debounce timer and restarts it. + this.searchDebounce.playFromStart(); + }); + } +} diff --git a/client/src/main/java/client/utils/ServerUtils.java b/client/src/main/java/client/utils/ServerUtils.java index 1e7ee98..2d6dc46 100644 --- a/client/src/main/java/client/utils/ServerUtils.java +++ b/client/src/main/java/client/utils/ServerUtils.java @@ -50,6 +50,11 @@ public class ServerUtils { });// JSON string-> List (Jackson) } + public List getRecipesFiltered(String filter) throws IOException, InterruptedException { + // TODO: implement filtering on server side + return this.getRecipes(); + } + /** * Gets a single recipe based on its id. * @param id every recipe has it's unique id diff --git a/client/src/main/resources/client/scenes/FoodpalApplication.fxml b/client/src/main/resources/client/scenes/FoodpalApplication.fxml index 27b5bc2..0087ac4 100644 --- a/client/src/main/resources/client/scenes/FoodpalApplication.fxml +++ b/client/src/main/resources/client/scenes/FoodpalApplication.fxml @@ -89,6 +89,8 @@ + + diff --git a/client/src/main/resources/client/scenes/SearchBar.fxml b/client/src/main/resources/client/scenes/SearchBar.fxml new file mode 100644 index 0000000..10e90fe --- /dev/null +++ b/client/src/main/resources/client/scenes/SearchBar.fxml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/client/src/main/resources/locale/lang_en.properties b/client/src/main/resources/locale/lang_en.properties index d4f71f4..da55e8c 100644 --- a/client/src/main/resources/locale/lang_en.properties +++ b/client/src/main/resources/locale/lang_en.properties @@ -24,3 +24,5 @@ menu.button.remove.step=Remove Step menu.button.edit=Edit menu.button.clone=Clone menu.button.print=Print recipe + +menu.search=Search... \ No newline at end of file diff --git a/client/src/main/resources/locale/lang_nl.properties b/client/src/main/resources/locale/lang_nl.properties index 9ab41ae..9a2aec6 100644 --- a/client/src/main/resources/locale/lang_nl.properties +++ b/client/src/main/resources/locale/lang_nl.properties @@ -24,3 +24,5 @@ menu.button.remove.step=Stap verwijderen menu.button.edit=Bewerken menu.button.clone=Dupliceren menu.button.print=Recept afdrukken + +menu.search=Zoeken... \ No newline at end of file diff --git a/client/src/main/resources/locale/lang_pl.properties b/client/src/main/resources/locale/lang_pl.properties index 10076de..cb9a9b8 100644 --- a/client/src/main/resources/locale/lang_pl.properties +++ b/client/src/main/resources/locale/lang_pl.properties @@ -24,3 +24,5 @@ menu.button.remove.step=Usuń instrukcję menu.button.edit=Edytuj menu.button.clone=Duplikuj menu.button.print=Drukuj przepis + +menu.search=Szukaj... \ No newline at end of file From d802d8627e408ff5ec9f4f90de251ce10a2d6bde Mon Sep 17 00:00:00 2001 From: Natalia Cholewa Date: Fri, 19 Dec 2025 20:16:21 +0100 Subject: [PATCH 2/2] fix: checkstyle violations --- .../java/client/scenes/FoodpalApplicationCtrl.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java index 4413e5c..b80404a 100644 --- a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java +++ b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java @@ -42,7 +42,8 @@ public class FoodpalApplicationCtrl implements LocaleAware { private final LocaleManager localeManager; private final IngredientListCtrl ingredientListCtrl; private final RecipeStepListCtrl stepListCtrl; - private final SearchBarCtrl searchBarCtrl; + + private SearchBarCtrl searchBarCtrl; public VBox detailsScreen; public HBox editableTitleArea; @@ -100,7 +101,6 @@ public class FoodpalApplicationCtrl implements LocaleAware { LocaleManager localeManager, IngredientListCtrl ingredientListCtrl, RecipeStepListCtrl stepListCtrl, - SearchBarCtrl searchBarCtrl, ConfigService configService ) { this.server = server; @@ -108,13 +108,17 @@ public class FoodpalApplicationCtrl implements LocaleAware { this.localeManager = localeManager; this.ingredientListCtrl = ingredientListCtrl; this.stepListCtrl = stepListCtrl; - this.searchBarCtrl = searchBarCtrl; this.configService = configService; initializeWebSocket(); } + @Inject + void setSearchBarCtrl(SearchBarCtrl searchBarCtrl) { + this.searchBarCtrl = searchBarCtrl; + } + private void initializeSearchBar() { // Refresh on search bar change this.searchBarCtrl.setOnSearch(recipes -> { @@ -220,7 +224,6 @@ public class FoodpalApplicationCtrl implements LocaleAware { }); this.initializeSearchBar(); - refresh(); updateFavouriteButton(recipeList.getSelectionModel().getSelectedItem()); }