feat: client side search box
This commit is contained in:
parent
65a7b0291a
commit
3c45143d76
9 changed files with 209 additions and 2 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Recipe> 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());
|
||||
|
|
|
|||
150
client/src/main/java/client/scenes/SearchBarCtrl.java
Normal file
150
client/src/main/java/client/scenes/SearchBarCtrl.java
Normal file
|
|
@ -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<List<Recipe>> onSearchCallback;
|
||||
|
||||
/**
|
||||
* Debounce timer to limit search frequency.
|
||||
*
|
||||
* @see SearchBarCtrl#initializeComponents()
|
||||
*/
|
||||
private final PauseTransition searchDebounce = new PauseTransition(Duration.millis(SEARCH_DELAY_MS));
|
||||
|
||||
private Task<List<Recipe>> 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<List<Recipe>> 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<Recipe> call() throws IOException, InterruptedException {
|
||||
return serverUtils.getRecipesFiltered(filter);
|
||||
}
|
||||
};
|
||||
|
||||
currentSearchTask.setOnSucceeded(e -> {
|
||||
List<Recipe> 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,11 @@ public class ServerUtils {
|
|||
});// JSON string-> List<Recipe> (Jackson)
|
||||
}
|
||||
|
||||
public List<Recipe> 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
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@
|
|||
<Font name="System Bold" size="15.0" />
|
||||
</font></Label>
|
||||
|
||||
<fx:include source="SearchBar.fxml" />
|
||||
|
||||
<ListView fx:id="recipeList" />
|
||||
|
||||
<HBox spacing="10">
|
||||
|
|
|
|||
11
client/src/main/resources/client/scenes/SearchBar.fxml
Normal file
11
client/src/main/resources/client/scenes/SearchBar.fxml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
|
||||
|
||||
<HBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.SearchBarCtrl">
|
||||
<children>
|
||||
<TextField fx:id="searchField" promptText="Search..." />
|
||||
</children>
|
||||
</HBox>
|
||||
|
|
@ -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...
|
||||
|
|
@ -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...
|
||||
|
|
@ -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...
|
||||
Loading…
Add table
Add a link
Reference in a new issue