diff --git a/client/pom.xml b/client/pom.xml index b9eb186..ebbef30 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -21,7 +21,13 @@ commons 0.0.1-SNAPSHOT - + + + org.slf4j + slf4j-api + 2.0.17 + compile + com.fasterxml.jackson.core jackson-databind diff --git a/client/src/main/java/client/Ingredient/IngredientController.java b/client/src/main/java/client/Ingredient/IngredientController.java index 429fecd..e674ab6 100644 --- a/client/src/main/java/client/Ingredient/IngredientController.java +++ b/client/src/main/java/client/Ingredient/IngredientController.java @@ -23,7 +23,7 @@ public class IngredientController { private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client @FXML - private void handleDeleteIngredient(ActionEvent event) { + public void handleDeleteIngredient(ActionEvent event) { // Get selected ingredient Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem(); if (selectedIngredient == null) { diff --git a/client/src/main/java/client/MyModule.java b/client/src/main/java/client/MyModule.java index 4d027ca..14eaa3e 100644 --- a/client/src/main/java/client/MyModule.java +++ b/client/src/main/java/client/MyModule.java @@ -21,6 +21,11 @@ import client.scenes.nutrition.NutritionDetailsCtrl; import client.scenes.nutrition.NutritionViewCtrl; import client.scenes.recipe.IngredientListCtrl; import client.scenes.recipe.RecipeStepListCtrl; +import client.scenes.shopping.ShoppingListCtrl; +import client.scenes.shopping.ShoppingListNewItemPromptCtrl; +import client.service.ShoppingListService; +import client.service.ShoppingListServiceImpl; +import client.service.ShoppingListViewModel; import client.utils.ConfigService; import client.utils.LocaleManager; import client.utils.server.ServerUtils; @@ -57,6 +62,10 @@ public class MyModule implements Module { binder.bind(new TypeLiteral>() {}).toInstance( new WebSocketDataService<>() ); + binder.bind(ShoppingListNewItemPromptCtrl.class).in(Scopes.SINGLETON); + binder.bind(ShoppingListCtrl.class).in(Scopes.SINGLETON); + binder.bind(ShoppingListViewModel.class).toInstance(new ShoppingListViewModel()); + binder.bind(ShoppingListService.class).to(ShoppingListServiceImpl.class); binder.bind(new TypeLiteral>() {}).toInstance( new WebSocketDataService<>() ); diff --git a/client/src/main/java/client/UI.java b/client/src/main/java/client/UI.java index 752dbaf..da3a2b3 100644 --- a/client/src/main/java/client/UI.java +++ b/client/src/main/java/client/UI.java @@ -2,6 +2,7 @@ package client; import client.scenes.FoodpalApplicationCtrl; import client.scenes.MainCtrl; +import client.scenes.ServerConnectionDialogCtrl; import client.utils.server.ServerUtils; import com.google.inject.Injector; import javafx.application.Application; @@ -27,9 +28,14 @@ public class UI extends Application { var serverUtils = INJECTOR.getInstance(ServerUtils.class); if (!serverUtils.isServerAvailable()) { - var msg = "Server needs to be started before the client, but it does not seem to be available. Shutting down."; - System.err.println(msg); - return; + var connectionHandler = INJECTOR.getInstance(ServerConnectionDialogCtrl.class); + boolean serverConnected = connectionHandler.promptForURL(); + + if(!serverConnected){ + var msg = "User Cancelled Server connection. Shutting down"; + System.err.print(msg); + return; + } } var foodpal = FXML.load(FoodpalApplicationCtrl.class, "client", "scenes", "FoodpalApplication.fxml"); diff --git a/client/src/main/java/client/exception/DuplicateIngredientException.java b/client/src/main/java/client/exception/DuplicateIngredientException.java new file mode 100644 index 0000000..af04346 --- /dev/null +++ b/client/src/main/java/client/exception/DuplicateIngredientException.java @@ -0,0 +1,20 @@ +package client.exception; + +public class DuplicateIngredientException extends Exception{ + private final String ingredientName; + + public DuplicateIngredientException(String ingredientName){ + super("An ingredient with name " + ingredientName + " already exists, please provide a different name"); + this.ingredientName = ingredientName; + } + + public DuplicateIngredientException(String ingredientName, String message){ + super(message); + this.ingredientName = ingredientName; + } + + public String getIngredientName() { + return ingredientName; + } + +} diff --git a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java index e7cb0df..181279b 100644 --- a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java +++ b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java @@ -2,17 +2,17 @@ package client.scenes; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.logging.Logger; -import java.util.stream.Collectors; +import client.UI; import client.exception.InvalidModificationException; import client.scenes.nutrition.NutritionViewCtrl; import client.scenes.recipe.RecipeDetailCtrl; +import client.scenes.shopping.ShoppingListCtrl; import client.utils.Config; import client.utils.ConfigService; import client.utils.DefaultValueFactory; @@ -21,7 +21,6 @@ import client.utils.LocaleManager; import client.utils.server.ServerUtils; import client.utils.WebSocketDataService; import client.utils.WebSocketUtils; -import commons.Ingredient; import commons.Recipe; import commons.ws.Topics; @@ -32,14 +31,20 @@ import commons.ws.messages.Message; import commons.ws.messages.UpdateRecipeMessage; import jakarta.inject.Inject; import javafx.application.Platform; +import javafx.beans.property.ListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; import javafx.fxml.FXML; +import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; -import javafx.scene.control.TextInputDialog; import javafx.scene.control.ToggleButton; +import javafx.scene.paint.Color; +import javafx.stage.Stage; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; @@ -65,8 +70,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { @FXML public ListView recipeList; - @FXML - private ListView ingredientListView; + private final ListProperty favouriteRecipeList = new SimpleListProperty<>(); @FXML private Button addRecipeButton; @@ -84,7 +88,8 @@ public class FoodpalApplicationCtrl implements LocaleAware { @FXML private ToggleButton favouritesOnlyToggle; - private List allRecipes = new ArrayList<>(); + @FXML + private Button manageIngredientsButton; @FXML private Label updatedBadge; @@ -200,6 +205,12 @@ public class FoodpalApplicationCtrl implements LocaleAware { } //gives star in front fav items boolean fav = config.isFavourite(item.getId()); setText((fav ? "★ " : "") + item.getName()); + + if(fav){ + setTextFill(Color.BLUE); + }else{ + setTextFill(Color.BLACK); + } } }); // When your selection changes, update details in the panel @@ -277,6 +288,13 @@ public class FoodpalApplicationCtrl implements LocaleAware { openSelectedRecipe(); } }); + recipeList.getItems().addListener((ListChangeListener.Change c) -> { + favouriteRecipeList.set( + FXCollections.observableList( + recipeList.getItems().stream().filter(r -> config.isFavourite(r.getId())).toList() + )); + System.out.println(favouriteRecipeList); + }); this.initializeSearchBar(); refresh(); @@ -290,6 +308,9 @@ public class FoodpalApplicationCtrl implements LocaleAware { removeRecipeButton.setText(getLocaleString("menu.button.remove.recipe")); cloneRecipeButton.setText(getLocaleString("menu.button.clone")); recipesLabel.setText(getLocaleString("menu.label.recipes")); + + favouritesOnlyToggle.setText(getLocaleString("menu.button.favourites")); + manageIngredientsButton.setText(getLocaleString("menu.button.ingredients")); } @Override @@ -312,8 +333,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { logger.severe(msg); printError(msg); } - - allRecipes = new ArrayList<>(recipes); + recipeList.getItems().setAll(recipes); applyRecipeFilterAndKeepSelection(); showUpdatedBadge(); @@ -409,16 +429,20 @@ public class FoodpalApplicationCtrl implements LocaleAware { public void applyRecipeFilterAndKeepSelection() { Recipe selected = recipeList.getSelectionModel().getSelectedItem(); Long selectedId = selected == null ? null : selected.getId(); - - List view = allRecipes; + List view = recipeList.getItems().stream().toList(); if (favouritesOnlyToggle != null && favouritesOnlyToggle.isSelected()) { - view = allRecipes.stream() - .filter(r -> config.isFavourite(r.getId())) - .collect(Collectors.toList()); + view = favouriteRecipeList.get(); + recipeList.getItems().setAll(view); + } + try { + if (favouritesOnlyToggle != null && !favouritesOnlyToggle.isSelected()) { + recipeList.getItems().setAll(server.getRecipes(config.getRecipeLanguages())); + } + } + catch (IOException | InterruptedException e) { + logger.severe(e.getMessage()); } - - recipeList.getItems().setAll(view); // restore selection if possible if (selectedId != null) { @@ -439,114 +463,6 @@ public class FoodpalApplicationCtrl implements LocaleAware { this.recipeDetailController.refreshFavouriteButton(); this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty()); } - - //Delete Ingredient button click - @FXML - private void handleDeleteIngredient() { - // Get selected ingredient - Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem(); - - if (selectedIngredient == null) { - // Show an error message if no ingredient is selected - showError("No ingredient selected", "Please select an ingredient to delete."); - return; - } - - // Check if the ingredient is used in any recipe - checkIngredientUsage(selectedIngredient); - } - - // Check if ingredient is used in any recipe before deleting - private void checkIngredientUsage(Ingredient ingredient) { - try { - long usageCount = server.getIngredientUsage(ingredient.getId()); // Check ingredient usage via ServerUtils - - if (usageCount > 0) { - // If ingredient is used, show a warning dialog - showWarningDialog(ingredient, usageCount); - } else { - // If not used, delete - deleteIngredient(ingredient); - } - } catch (IOException | InterruptedException e) { - showError("Error", "Failed to check ingredient usage: " + e.getMessage()); - } - } - - private void deleteIngredient(Ingredient ingredient) { - try { - server.deleteIngredient(ingredient.getId()); // Call ServerUtils to delete the ingredient - showConfirmation("Success", "Ingredient '" + ingredient.getName() + "' has been deleted."); - refreshIngredientList(); // refresh the ingredient list - } catch (IOException | InterruptedException e) { - showError("Error", "Failed to delete ingredient: " + e.getMessage()); - } - } - - private void showWarningDialog(Ingredient ingredient, long usedInRecipes) { - Alert alert = new Alert(Alert.AlertType.WARNING); - } - - private void showError(String title, String message) { - Alert alert = new Alert(Alert.AlertType.ERROR); - alert.setTitle(title); - alert.setHeaderText(null); - alert.setContentText(message); - alert.showAndWait(); - } - - private void showConfirmation(String title, String message) { - Alert alert = new Alert(Alert.AlertType.INFORMATION); - alert.setTitle(title); - alert.setHeaderText(null); - alert.setContentText(message); - alert.showAndWait(); - } - - private void refreshIngredientList() { - // Refresh - ingredientListView.getItems().clear(); - - } - - - @FXML - private void handleAddIngredient() { - //ask the user for the ingredient name - TextInputDialog dialog = new TextInputDialog(); - dialog.setTitle("Add Ingredient"); - dialog.setHeaderText("Enter the ingredient name:"); - dialog.setContentText("Ingredient:"); - - // Wait for the user to enter a value - Optional result = dialog.showAndWait(); - - result.ifPresent(name -> { - // Create a new Ingredient object - Ingredient newIngredient = new Ingredient(); - newIngredient.setName(name); - - // Add the new ingredient to the ListView - ingredientListView.getItems().add(newIngredient); - }); - } - - // display ingredient name - @FXML - private void initializeIngredients() { - ingredientListView.setCellFactory(list -> new ListCell() { - @Override - protected void updateItem(Ingredient item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - setText(null); - } else { - setText(item.getName()); // Display the ingredient name in the ListView - } - } - }); - } - @FXML private void openIngredientsPopup() { try { @@ -558,7 +474,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { var root = pair.getValue(); var stage = new javafx.stage.Stage(); - stage.setTitle("Nutrition values view"); + stage.setTitle(getLocaleString("menu.ingredients.title")); stage.initModality(javafx.stage.Modality.APPLICATION_MODAL); stage.setScene(new javafx.scene.Scene(root)); stage.showAndWait(); @@ -598,6 +514,14 @@ public class FoodpalApplicationCtrl implements LocaleAware { updatedBadgeTimer.playFromStart(); } + public void openShoppingListWindow() throws IOException { + var root = UI.getFXML().load(ShoppingListCtrl.class, "client", "scenes", "shopping", "ShoppingList.fxml"); + Stage stage = new Stage(); + stage.setTitle(this.getLocaleString("menu.shopping.title")); + stage.setScene(new Scene(root.getValue())); + stage.show(); + } + } diff --git a/client/src/main/java/client/scenes/Ingredient/IngredientListCtrl.java b/client/src/main/java/client/scenes/Ingredient/IngredientListCtrl.java index dfcd7be..c06a04a 100644 --- a/client/src/main/java/client/scenes/Ingredient/IngredientListCtrl.java +++ b/client/src/main/java/client/scenes/Ingredient/IngredientListCtrl.java @@ -1,11 +1,17 @@ package client.scenes.Ingredient; +import client.exception.DuplicateIngredientException; import client.scenes.nutrition.NutritionDetailsCtrl; +import client.utils.LocaleAware; +import client.utils.LocaleManager; import client.utils.server.ServerUtils; import commons.Ingredient; import jakarta.inject.Inject; import javafx.fxml.FXML; + import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.TextInputDialog; @@ -19,10 +25,18 @@ import java.util.logging.Logger; //TODO and check for capital letter milk and MILK are seen as different -public class IngredientListCtrl { - +public class IngredientListCtrl implements LocaleAware { private final ServerUtils server; + private final LocaleManager localeManager; private final Logger logger = Logger.getLogger(IngredientListCtrl.class.getName()); + + @FXML + public Label ingredientsLabel; + public Button addButton; + public Button refreshButton; + public Button deleteButton; + public Button closeButton; + @FXML private ListView ingredientListView; @FXML @@ -31,14 +45,30 @@ public class IngredientListCtrl { @Inject public IngredientListCtrl( ServerUtils server, + LocaleManager localeManager, NutritionDetailsCtrl nutritionDetailsCtrl ) { this.server = server; + this.localeManager = localeManager; this.nutritionDetailsCtrl = nutritionDetailsCtrl; } - @FXML - public void initialize() { + @Override + public void updateText() { + ingredientsLabel.setText(getLocaleString("menu.label.ingredients")); + addButton.setText(getLocaleString("menu.button.add")); + refreshButton.setText(getLocaleString("menu.button.refresh")); + deleteButton.setText(getLocaleString("menu.button.delete")); + closeButton.setText(getLocaleString("menu.button.close")); + } + + @Override + public LocaleManager getLocaleManager() { + return this.localeManager; + } + + @Override + public void initializeComponents() { ingredientListView.setCellFactory(list -> new ListCell<>() { @Override protected void updateItem(Ingredient item, boolean empty) { @@ -60,6 +90,7 @@ public class IngredientListCtrl { refresh(); } + @FXML private void addIngredient() { TextInputDialog dialog = new TextInputDialog(); @@ -83,10 +114,11 @@ public class IngredientListCtrl { refresh(); // reload list from server } catch (IOException | InterruptedException e) { showError("Failed to create ingredient: " + e.getMessage()); + } catch (DuplicateIngredientException e) { + throw new RuntimeException(e); } } - @FXML private void refresh() { try { diff --git a/client/src/main/java/client/scenes/LangSelectMenuCtrl.java b/client/src/main/java/client/scenes/LangSelectMenuCtrl.java index cc764f0..76fdef8 100644 --- a/client/src/main/java/client/scenes/LangSelectMenuCtrl.java +++ b/client/src/main/java/client/scenes/LangSelectMenuCtrl.java @@ -1,5 +1,6 @@ package client.scenes; +import client.utils.Config; import client.utils.LocaleAware; import client.utils.LocaleManager; import com.google.inject.Inject; @@ -41,11 +42,12 @@ public class LangSelectMenuCtrl implements LocaleAware { String lang = langSelectMenu.getSelectionModel().getSelectedItem(); logger.info("Switching locale to " + lang); manager.setLocale(Locale.of(lang)); + initializeComponents(); } @Override public void initializeComponents() { - langSelectMenu.getItems().setAll("en", "pl", "nl", "zht", "zhc", "tok", "tr"); + langSelectMenu.getItems().setAll(Config.languages); langSelectMenu.setValue(manager.getLocale().getLanguage()); langSelectMenu.setConverter(new StringConverter() { @Override diff --git a/client/src/main/java/client/scenes/LanguageFilterCtrl.java b/client/src/main/java/client/scenes/LanguageFilterCtrl.java index 80d01a0..fd1de08 100644 --- a/client/src/main/java/client/scenes/LanguageFilterCtrl.java +++ b/client/src/main/java/client/scenes/LanguageFilterCtrl.java @@ -1,5 +1,6 @@ package client.scenes; +import client.utils.Config; import client.utils.ConfigService; import client.utils.LocaleAware; import client.utils.LocaleManager; @@ -54,7 +55,7 @@ public class LanguageFilterCtrl implements LocaleAware { public void initializeComponents() { var items = this.langFilterMenu.getItems(); - final List languages = List.of("en", "nl", "pl", "tok", "tr"); + final List languages = List.of(Config.languages); this.selectedLanguages = this.configService.getConfig().getRecipeLanguages(); this.updateMenuButtonDisplay(); diff --git a/client/src/main/java/client/scenes/SearchBarCtrl.java b/client/src/main/java/client/scenes/SearchBarCtrl.java index 065455c..c37f188 100644 --- a/client/src/main/java/client/scenes/SearchBarCtrl.java +++ b/client/src/main/java/client/scenes/SearchBarCtrl.java @@ -10,11 +10,15 @@ import javafx.animation.PauseTransition; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; import javafx.util.Duration; import java.io.IOException; +import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.function.Consumer; +import java.util.stream.Collectors; /** * Controller for the search bar component. @@ -99,7 +103,11 @@ public class SearchBarCtrl implements LocaleAware { currentSearchTask = new Task<>() { @Override protected List call() throws IOException, InterruptedException { - return serverUtils.getRecipesFiltered(filter, configService.getConfig().getRecipeLanguages()); + var recipes = serverUtils.getRecipesFiltered( + "", + configService.getConfig().getRecipeLanguages() + ); + return applyMultiTermAndFilter(recipes, filter); } }; @@ -146,8 +154,61 @@ public class SearchBarCtrl implements LocaleAware { }); this.searchField.setOnKeyReleased(event -> { + if (event.getCode() == KeyCode.ESCAPE) { + searchField.clear(); + this.onSearch(); + return; + } // This cancels the current debounce timer and restarts it. this.searchDebounce.playFromStart(); }); } + private List applyMultiTermAndFilter(List recipes, String query) { + if (recipes == null) { + return List.of(); + } + if (query == null || query.isBlank()) { + return recipes; + } + + var tokens = Arrays.stream(query.toLowerCase(Locale.ROOT).split("[\\s,]+")) + .map(String::trim) + .filter(s -> !s.isBlank()) + .filter(s -> !s.equals("and")) + .toList(); + + if (tokens.isEmpty()) { + return recipes; + } + + return recipes.stream() + .filter(r -> { + var sb = new StringBuilder(); + + if (r.getName() != null) { + sb.append(r.getName()).append(' '); + } + + if (r.getIngredients() != null) { + r.getIngredients().forEach(i -> { + if (i != null) { + sb.append(i).append(' '); + } + }); + } + + if (r.getPreparationSteps() != null) { + r.getPreparationSteps().forEach(s -> { + if (s != null) { + sb.append(s).append(' '); + } + }); + } + + var haystack = sb.toString().toLowerCase(Locale.ROOT); + return tokens.stream().allMatch(haystack::contains); + }) + .collect(Collectors.toList()); + } + } diff --git a/client/src/main/java/client/scenes/ServerConnectionDialogCtrl.java b/client/src/main/java/client/scenes/ServerConnectionDialogCtrl.java new file mode 100644 index 0000000..4283d01 --- /dev/null +++ b/client/src/main/java/client/scenes/ServerConnectionDialogCtrl.java @@ -0,0 +1,94 @@ +package client.scenes; + + +import client.utils.ConfigService; +import client.utils.server.ServerUtils; +import com.google.inject.Inject; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.TextInputDialog; + +import java.util.Optional; + + +public class ServerConnectionDialogCtrl { + + private final ConfigService configService; + private final ServerUtils serverUtils; + + + + @Inject + public ServerConnectionDialogCtrl(ConfigService configService, ServerUtils serverUtils) { + this.configService = configService; + this.serverUtils = serverUtils; + + } + + /** + * + * @return a boolean for if the user got connected to server or + */ + public boolean promptForURL(){ + Alert error = new Alert(Alert.AlertType.ERROR); //creates an error alert + error.setTitle("Server is Unavailable"); + error.setHeaderText("Unable to connect to Server"); + error.setContentText("The server at " + configService.getConfig().getServerUrl() + + " is not available\n\n Would you like to try a different Server URL?"); + + error.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO); + Optional userChoice = error.showAndWait(); //asks if user wants to input server URL + + if(userChoice.isEmpty() || userChoice.get() == ButtonType.NO){ + return false; + } + + while(true){ // Keeps asking the user until either a valid url is provided or the user exits + TextInputDialog dialog = + new TextInputDialog(configService.getConfig().getServerUrl()); + dialog.setTitle("Enter new server URL"); + dialog.setContentText("Server URL:"); + Optional userRes = dialog.showAndWait(); + if(userRes.isEmpty()){ + return false; //user cancelled the operation + } + String newServer = userRes.get().trim(); + + if(newServer.isEmpty()){ + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("Invalid Input"); + alert.setHeaderText("Invalid server URL"); + alert.setContentText("Please enter a valid URL"); + alert.showAndWait(); + continue; + } + configService.getConfig().setServerUrl(newServer); + if(serverUtils.isServerAvailable()){ + configService.save(); + Alert success = new Alert(Alert.AlertType.INFORMATION); + success.setTitle("Success"); + success.setHeaderText("Connected to Server"); + success.setContentText("Successfully connected to the server!"); + success.showAndWait(); + return true; + } + else{ + Alert retry = new Alert(Alert.AlertType.ERROR); + retry.setTitle("Failed"); + retry.setHeaderText("Failed to connect to Server"); + retry.setContentText("Would you like to try another URL?"); + retry.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO); + Optional result = retry.showAndWait(); + if(result.isEmpty() || result.get() == ButtonType.NO){ + return false; + } + + } + + } + } + + + + +} diff --git a/client/src/main/java/client/scenes/nutrition/NutritionDetailsCtrl.java b/client/src/main/java/client/scenes/nutrition/NutritionDetailsCtrl.java index c7c968f..d8351f3 100644 --- a/client/src/main/java/client/scenes/nutrition/NutritionDetailsCtrl.java +++ b/client/src/main/java/client/scenes/nutrition/NutritionDetailsCtrl.java @@ -66,7 +66,7 @@ public class NutritionDetailsCtrl implements LocaleAware { this.proteinInputElement.textProperty().bindBidirectional(vm.proteinProperty(), new NumberStringConverter()); this.carbInputElement.textProperty().bindBidirectional(vm.carbsProperty(), new NumberStringConverter()); this.estimatedKcalLabel.textProperty().bind(Bindings.createStringBinding( - () -> String.format("Estimated energy value: %.1f", vm.getKcal()), vm.kcalProperty() + () -> String.format("Estimated energy value: %.1f kcal/100g", vm.getKcal()), vm.kcalProperty() )); }); this.nutritionValueContainer.addEventHandler(KeyEvent.KEY_RELEASED, event -> { diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java index 7bb392f..679e05d 100644 --- a/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java +++ b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java @@ -8,6 +8,7 @@ import com.google.inject.Inject; import commons.FormalIngredient; import commons.Recipe; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.function.Consumer; @@ -78,7 +79,13 @@ public class IngredientListCtrl implements LocaleAware { if (recipe == null) { this.ingredients = FXCollections.observableArrayList(new ArrayList<>()); } else { - List ingredientList = recipe.getIngredients(); + List ingredientList = recipe + .getIngredients() + .stream() + .sorted(Comparator.comparing(ingredient -> ingredient + .getIngredient() + .getName())) + .toList(); this.ingredients = FXCollections.observableArrayList(ingredientList); } diff --git a/client/src/main/java/client/scenes/recipe/IngredientsPopupCtrl.java b/client/src/main/java/client/scenes/recipe/IngredientsPopupCtrl.java index de27245..091befd 100644 --- a/client/src/main/java/client/scenes/recipe/IngredientsPopupCtrl.java +++ b/client/src/main/java/client/scenes/recipe/IngredientsPopupCtrl.java @@ -1,6 +1,7 @@ package client.scenes.recipe; import client.utils.server.ServerUtils; +import client.exception.DuplicateIngredientException; import commons.Ingredient; import jakarta.inject.Inject; import javafx.fxml.FXML; @@ -67,8 +68,15 @@ public class IngredientsPopupCtrl { server.createIngredient(name); // calls POST /api/ingredients refresh(); // reload list from server } catch (IOException | InterruptedException e) { + showError("Failed to create ingredient: " + e.getMessage()); + } catch (DuplicateIngredientException e) { + showError("An ingredient with the name " + name + " already exists." + + " Please provide a different name."); //checks if error received has the DUPLICATE string and creates a showError instance with an appropriate error message and description + } + + } diff --git a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java index d072669..d923ffd 100644 --- a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java +++ b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java @@ -2,6 +2,7 @@ package client.scenes.recipe; import client.exception.UpdateException; import client.scenes.FoodpalApplicationCtrl; +import client.service.ShoppingListService; import client.utils.Config; import client.utils.ConfigService; import client.utils.LocaleAware; @@ -10,6 +11,7 @@ import client.utils.PrintExportService; import client.utils.server.ServerUtils; import client.utils.WebSocketDataService; import com.google.inject.Inject; +import commons.FormalIngredient; import commons.Recipe; import java.io.File; @@ -18,6 +20,9 @@ import java.nio.file.Path; import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; + +import javafx.beans.binding.Bindings; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; @@ -44,8 +49,12 @@ public class RecipeDetailCtrl implements LocaleAware { private final FoodpalApplicationCtrl appCtrl; private final ConfigService configService; private final WebSocketDataService webSocketDataService; + private final ShoppingListService shoppingListService; public Spinner scaleSpinner; + public Label inferredKcalLabel; + public Spinner servingsSpinner; + public Label inferredServeSizeLabel; @FXML private IngredientListCtrl ingredientListController; @@ -62,12 +71,14 @@ public class RecipeDetailCtrl implements LocaleAware { ServerUtils server, FoodpalApplicationCtrl appCtrl, ConfigService configService, + ShoppingListService listService, WebSocketDataService webSocketDataService) { this.localeManager = localeManager; this.server = server; this.appCtrl = appCtrl; this.configService = configService; this.webSocketDataService = webSocketDataService; + this.shoppingListService = listService; } @FXML @@ -154,12 +165,20 @@ public class RecipeDetailCtrl implements LocaleAware { // If there is a scale // Prevents issues from first startup - if (scaleSpinner.getValue() != null) { + if (scaleSpinner.getValue() != null && servingsSpinner.getValue() != null) { Double scale = scaleSpinner.getValue(); - // see impl. creates a scaled context for the recipe such that its non-scaled value is kept as a reference. this.recipeView = new ScalableRecipeView(recipe, scale); - + // TODO i18n + inferredKcalLabel.textProperty().bind(Bindings.createStringBinding(() -> + String.format("Inferred %.1f kcal/100g for this recipe", + Double.isNaN(this.recipeView.scaledKcalProperty().get()) ? + 0.0 : this.recipeView.scaledKcalProperty().get()) + , this.recipeView.scaledKcalProperty())); + recipeView.servingsProperty().set(servingsSpinner.getValue()); + inferredServeSizeLabel.textProperty().bind(Bindings.createStringBinding( + () -> String.format("Inferred size per serving: %.1f g", recipeView.servingSizeProperty().get()), + recipeView.servingSizeProperty())); // expose the scaled view to list controllers this.ingredientListController.refetchFromRecipe(this.recipeView.getScaled()); this.stepListController.refetchFromRecipe(this.recipeView.getScaled()); @@ -386,7 +405,7 @@ public class RecipeDetailCtrl implements LocaleAware { public void initializeComponents() { initStepsIngredientsList(); // creates a new scale spinner with an arbitrary max scale - scaleSpinner.setValueFactory(new SpinnerValueFactory.DoubleSpinnerValueFactory(0, Double.MAX_VALUE, 1)); + scaleSpinner.setValueFactory(new SpinnerValueFactory.DoubleSpinnerValueFactory(1, Double.MAX_VALUE, 1)); scaleSpinner.setEditable(true); scaleSpinner.valueProperty().addListener((observable, oldValue, newValue) -> { if (newValue == null) { @@ -395,6 +414,23 @@ public class RecipeDetailCtrl implements LocaleAware { // triggers a UI update each time the spinner changes to a different value. setCurrentlyViewedRecipe(recipe); }); - langSelector.getItems().addAll("en", "nl", "pl", "tok"); + servingsSpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, Integer.MAX_VALUE, 1)); + servingsSpinner.setEditable(true); + servingsSpinner.valueProperty().addListener((observable, oldValue, newValue) -> { + if (newValue == null) { + return; + } + setCurrentlyViewedRecipe(recipe); + }); + langSelector.getItems().addAll(Config.languages); + } + + public void handleAddAllToShoppingList(ActionEvent actionEvent) { + System.out.println("handleAddAllToShoppingList"); + // TODO BACKLOG Add overview screen + recipe.getIngredients().stream() + .filter(x -> x.getClass().equals(FormalIngredient.class)) + .map(FormalIngredient.class::cast) + .forEach(x -> shoppingListService.putIngredient(x, recipe)); } } diff --git a/client/src/main/java/client/scenes/recipe/ScalableRecipeView.java b/client/src/main/java/client/scenes/recipe/ScalableRecipeView.java index db5de91..e9192ab 100644 --- a/client/src/main/java/client/scenes/recipe/ScalableRecipeView.java +++ b/client/src/main/java/client/scenes/recipe/ScalableRecipeView.java @@ -4,14 +4,19 @@ import commons.Recipe; import javafx.beans.binding.Bindings; import javafx.beans.binding.ObjectBinding; import javafx.beans.property.DoubleProperty; +import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; public class ScalableRecipeView { private final ObjectProperty recipe = new SimpleObjectProperty<>(); private final ObjectProperty scaled = new SimpleObjectProperty<>(); private final DoubleProperty scale = new SimpleDoubleProperty(); + private final DoubleProperty scaledKcal = new SimpleDoubleProperty(); + private final IntegerProperty servings = new SimpleIntegerProperty(); + private final DoubleProperty servingSize = new SimpleDoubleProperty(); public ScalableRecipeView( Recipe recipe, Double scale @@ -22,10 +27,11 @@ public class ScalableRecipeView { () -> Recipe.getScaled(this.recipe.get(), this.scale.get()), this.recipe, this.scale); this.scaled.bind(binding); - } - - public double getScale() { - return scale.get(); + this.scaledKcal.bind(Bindings.createDoubleBinding(() -> this.scaled.get().kcal(), this.scaled)); + this.servingSize.bind(Bindings.createDoubleBinding( + () -> this.scaled.get().weight() * ( 1.0 / this.servings.get()), + this.servings) + ); } public Recipe getRecipe() { @@ -36,15 +42,14 @@ public class ScalableRecipeView { return scaled.get(); } - public DoubleProperty scaleProperty() { - return scale; + public DoubleProperty scaledKcalProperty() { + return scaledKcal; } - public ObjectProperty scaledProperty() { - return scaled; + public IntegerProperty servingsProperty() { + return servings; } - - public ObjectProperty recipeProperty() { - return recipe; + public DoubleProperty servingSizeProperty() { + return servingSize; } } diff --git a/client/src/main/java/client/scenes/shopping/ShoppingListCell.java b/client/src/main/java/client/scenes/shopping/ShoppingListCell.java new file mode 100644 index 0000000..d684435 --- /dev/null +++ b/client/src/main/java/client/scenes/shopping/ShoppingListCell.java @@ -0,0 +1,67 @@ +package client.scenes.shopping; + +import client.scenes.recipe.OrderedEditableListCell; +import commons.FormalIngredient; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; +import javafx.util.Pair; + +import java.util.Optional; + +public class ShoppingListCell extends OrderedEditableListCell>> { + private Node makeEditor() { + HBox editor = new HBox(); + Spinner amountInput = new Spinner<>(); + FormalIngredient ingredient = getItem().getKey(); + amountInput.setEditable(true); + amountInput.setValueFactory(new SpinnerValueFactory.DoubleSpinnerValueFactory(0, Double.MAX_VALUE, ingredient.getAmount())); + Label textLabel = new Label(getItem().getKey().getUnitSuffix() + " of " + getItem().getKey().getIngredient().getName()); + editor.getChildren().addAll(amountInput, textLabel); + editor.addEventHandler(KeyEvent.KEY_RELEASED, e -> { + if (e.getCode() != KeyCode.ENTER) { + return; + } + Pair> pair = getItem(); + pair.getKey().setAmount(amountInput.getValue()); + commitEdit(pair); + }); + return editor; + } + @Override + public void startEdit() { + super.startEdit(); + this.setText(""); + this.setGraphic(makeEditor()); + } + @Override + protected void updateItem(Pair> item, boolean empty) { + super.updateItem(item, empty); + + if (empty) { + this.setText(""); + return; + } + + String display = item.getKey().toString() + + item.getValue().map(recipe -> { + return " (" + recipe + ")"; + }).orElse(""); + + this.setText(display); + } + @Override + public void cancelEdit() { + super.cancelEdit(); + } + + @Override + public void commitEdit(Pair> newValue) { + super.commitEdit(newValue); + } + +} diff --git a/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java b/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java new file mode 100644 index 0000000..ec08c62 --- /dev/null +++ b/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java @@ -0,0 +1,81 @@ +package client.scenes.shopping; + +import client.UI; +import client.service.ShoppingListService; +import client.utils.LocaleAware; +import client.utils.LocaleManager; +import com.google.inject.Inject; +import commons.FormalIngredient; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.ListView; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.util.Pair; + +import java.util.Optional; + +public class ShoppingListCtrl implements LocaleAware { + ShoppingListService shopping; + + LocaleManager localeManager; + + @FXML + private ListView>> shoppingListView; + @Inject + public ShoppingListCtrl( + ShoppingListService shopping, + LocaleManager localeManager + ) { + this.shopping = shopping; + this.localeManager = localeManager; + } + + @Override + public void updateText() { + + } + + @Override + public LocaleManager getLocaleManager() { + return this.localeManager; + } + + public void initializeComponents() { + this.shoppingListView.setEditable(true); + this.shoppingListView.setCellFactory(l -> new ShoppingListCell()); + this.shoppingListView.getItems().setAll( + this.shopping.getItems() + ); + } + private void refreshList() { + this.shoppingListView.getItems().setAll( + this.shopping.getItems() + ); + } + + public void handleAddItem(ActionEvent actionEvent) { + Stage stage = new Stage(); + Pair root = UI.getFXML().load(ShoppingListNewItemPromptCtrl.class, + "client", "scenes", "shopping", "ShoppingListItemAddModal.fxml"); + root.getKey().setNewValueConsumer(fi -> { + this.shopping.putIngredient(fi); + refreshList(); + }); + stage.setScene(new Scene(root.getValue())); + stage.setTitle("My modal window"); + stage.initModality(Modality.WINDOW_MODAL); + stage.initOwner( + ((Node)actionEvent.getSource()).getScene().getWindow() ); + stage.show(); + } + + public void handleRemoveItem(ActionEvent actionEvent) { + var x = this.shoppingListView.getSelectionModel().getSelectedItem(); + this.shopping.getItems().remove(x); + refreshList(); + } +} diff --git a/client/src/main/java/client/scenes/shopping/ShoppingListNewItemPromptCtrl.java b/client/src/main/java/client/scenes/shopping/ShoppingListNewItemPromptCtrl.java new file mode 100644 index 0000000..09bd44f --- /dev/null +++ b/client/src/main/java/client/scenes/shopping/ShoppingListNewItemPromptCtrl.java @@ -0,0 +1,96 @@ +package client.scenes.shopping; + +import client.utils.LocaleAware; +import client.utils.LocaleManager; +import client.utils.server.ServerUtils; +import com.google.inject.Inject; +import commons.FormalIngredient; +import commons.Ingredient; +import commons.Unit; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.ActionEvent; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.Function; + +public class ShoppingListNewItemPromptCtrl implements LocaleAware { + public MenuButton ingredientSelection; + private final ObjectProperty selected = new SimpleObjectProperty<>(); + private final ObjectProperty selectedUnit = new SimpleObjectProperty<>(); + private final ServerUtils server; + private final LocaleManager localeManager; + private Consumer newValueConsumer; + public MenuButton unitSelect; + public Spinner amountSelect; + + @Inject + public ShoppingListNewItemPromptCtrl(ServerUtils server, LocaleManager localeManager) { + this.server = server; + this.localeManager = localeManager; + } + + public void setNewValueConsumer(Consumer consumer) { + this.newValueConsumer = consumer; + } + + public void confirmAdd(ActionEvent actionEvent) { + if (selected.get() == null || selectedUnit.get() == null) { + System.err.println("You must select both an ingredient and an unit"); + return; + } + FormalIngredient fi = new FormalIngredient(selected.get(), amountSelect.getValue(), selectedUnit.get().suffix); + newValueConsumer.accept(fi); + Stage stage = (Stage) ingredientSelection.getScene().getWindow(); + stage.close(); + } + + public void cancelAdd(ActionEvent actionEvent) { + } + private void makeMenuItems( + MenuButton menu, + Iterable items, + Function labelMapper, + Consumer onSelect) { + // Iterates over the list of items and applies the label and onSelect handlers. + for (T item : items) { + MenuItem mi = new MenuItem(); + mi.setText(labelMapper.apply(item)); + mi.setOnAction(_ -> { + menu.setText(labelMapper.apply(item)); + onSelect.accept(item); + }); + menu.getItems().add(mi); + } + } + @Override + public void updateText() { + + } + @Override + public void initializeComponents() { + try { + amountSelect.setValueFactory( + new SpinnerValueFactory.DoubleSpinnerValueFactory(0, Double.MAX_VALUE, 0)); + amountSelect.setEditable(true); + makeMenuItems(ingredientSelection, server.getIngredients(), Ingredient::getName, selected::set); + makeMenuItems(unitSelect, + Arrays.stream(Unit.values()).filter(u -> u.formal).toList(), + Unit::toString, selectedUnit::set); + } catch (IOException | InterruptedException e) { + System.err.println(e.getMessage()); + } + } + + @Override + public LocaleManager getLocaleManager() { + return localeManager; + } +} diff --git a/client/src/main/java/client/service/NonFunctionalShoppingListService.java b/client/src/main/java/client/service/NonFunctionalShoppingListService.java new file mode 100644 index 0000000..d85b320 --- /dev/null +++ b/client/src/main/java/client/service/NonFunctionalShoppingListService.java @@ -0,0 +1,62 @@ +package client.service; + +import com.google.inject.Inject; +import commons.FormalIngredient; +import commons.Recipe; +import javafx.util.Pair; +import org.apache.commons.lang3.NotImplementedException; + +import java.util.List; +import java.util.Optional; + +public class NonFunctionalShoppingListService extends ShoppingListService { + @Inject + public NonFunctionalShoppingListService(ShoppingListViewModel viewModel) { + super(viewModel); + } + + @Override + public void putIngredient(FormalIngredient ingredient) { + throw new NotImplementedException(); + } + + @Override + public void putIngredient(FormalIngredient ingredient, Recipe recipe) { + throw new NotImplementedException(); + } + + @Override + public void putIngredient(FormalIngredient ingredient, String recipeName) { + throw new NotImplementedException(); + } + + @Override + public void putArbitraryItem(String name) { + throw new NotImplementedException(); + } + + @Override + public FormalIngredient purgeIngredient(Long id) { + throw new NotImplementedException(); + } + + @Override + public FormalIngredient purgeIngredient(String ingredientName) { + throw new NotImplementedException(); + } + + @Override + public void reset() { + throw new NotImplementedException(); + } + + @Override + public List>> getItems() { + throw new NotImplementedException(); + } + + @Override + public String makePrintable() { + throw new NotImplementedException(); + } +} diff --git a/client/src/main/java/client/service/ShoppingListItem.java b/client/src/main/java/client/service/ShoppingListItem.java new file mode 100644 index 0000000..91dd98a --- /dev/null +++ b/client/src/main/java/client/service/ShoppingListItem.java @@ -0,0 +1,6 @@ +package client.service; + +import commons.FormalIngredient; +public class ShoppingListItem { + private FormalIngredient i; +} diff --git a/client/src/main/java/client/service/ShoppingListService.java b/client/src/main/java/client/service/ShoppingListService.java new file mode 100644 index 0000000..04ef26b --- /dev/null +++ b/client/src/main/java/client/service/ShoppingListService.java @@ -0,0 +1,38 @@ +package client.service; + +import com.google.inject.Inject; +import commons.FormalIngredient; +import commons.Recipe; +import javafx.util.Pair; + +import java.util.List; +import java.util.Optional; + +public abstract class ShoppingListService { + private ShoppingListViewModel viewModel; + @Inject + public ShoppingListService(ShoppingListViewModel viewModel) { + this.viewModel = viewModel; + } + + public ShoppingListViewModel getViewModel() { + return viewModel; + } + + public void setViewModel(ShoppingListViewModel viewModel) { + this.viewModel = viewModel; + } + + public abstract void putIngredient(FormalIngredient ingredient); + public abstract void putIngredient(FormalIngredient ingredient, Recipe recipe); + public abstract void putIngredient(FormalIngredient ingredient, String recipeName); + public abstract void putArbitraryItem(String name); + + public abstract FormalIngredient purgeIngredient(Long id); + public abstract FormalIngredient purgeIngredient(String ingredientName); + + public abstract void reset(); + + public abstract List>> getItems(); + public abstract String makePrintable(); +} diff --git a/client/src/main/java/client/service/ShoppingListServiceImpl.java b/client/src/main/java/client/service/ShoppingListServiceImpl.java new file mode 100644 index 0000000..9941ee6 --- /dev/null +++ b/client/src/main/java/client/service/ShoppingListServiceImpl.java @@ -0,0 +1,71 @@ +package client.service; + +import com.google.inject.Inject; +import commons.FormalIngredient; +import commons.Recipe; +import javafx.util.Pair; + +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +public class ShoppingListServiceImpl extends ShoppingListService { + private final Logger logger = Logger.getLogger(ShoppingListServiceImpl.class.getName()); + @Inject + public ShoppingListServiceImpl( + ShoppingListViewModel model + ) { + super(model); + } + + @Override + public void putIngredient(FormalIngredient ingredient) { + getViewModel().getListItems().add(new Pair<>(ingredient, Optional.empty())); + } + + @Override + public void putIngredient(FormalIngredient ingredient, Recipe recipe) { + Pair> val = new Pair<>(ingredient, Optional.of(recipe.getName())); + logger.info("putting ingredients into shopping list: " + val); + getViewModel().getListItems().add(val); + } + + @Override + public void putIngredient(FormalIngredient ingredient, String recipeName) { + getViewModel().getListItems().add(new Pair<>(ingredient, Optional.of(recipeName))); + } + + @Override + public void putArbitraryItem(String name) { + + } + + @Override + public FormalIngredient purgeIngredient(Long id) { + return null; + } + + @Override + public FormalIngredient purgeIngredient(String ingredientName) { + FormalIngredient fi = getViewModel().getListItems().stream() + .filter(i -> + i.getKey().getIngredient().getName().equals(ingredientName)) + .findFirst().orElseThrow(NullPointerException::new).getKey(); + return null; + } + + @Override + public void reset() { + getViewModel().getListItems().clear(); + } + + @Override + public List>> getItems() { + return getViewModel().getListItems(); + } + + @Override + public String makePrintable() { + return "TODO"; + } +} diff --git a/client/src/main/java/client/service/ShoppingListViewModel.java b/client/src/main/java/client/service/ShoppingListViewModel.java new file mode 100644 index 0000000..aaaa237 --- /dev/null +++ b/client/src/main/java/client/service/ShoppingListViewModel.java @@ -0,0 +1,26 @@ +package client.service; + +import commons.FormalIngredient; +import javafx.beans.property.ListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.util.Pair; +import org.apache.commons.lang3.NotImplementedException; + +import java.util.Optional; + +public class ShoppingListViewModel { + /** + * The formal ingredient provides the ingredient and its amount, + * and the string (optional) describes the recipe where it came from. + */ + private final ListProperty>> listItems = new SimpleListProperty<>(FXCollections.observableArrayList()); + public void addArbitrary() { + throw new NotImplementedException(); + } + + public ObservableList>> getListItems() { + return listItems.get(); + } +} diff --git a/client/src/main/java/client/utils/Config.java b/client/src/main/java/client/utils/Config.java index a70d55c..d3c3823 100644 --- a/client/src/main/java/client/utils/Config.java +++ b/client/src/main/java/client/utils/Config.java @@ -5,6 +5,7 @@ import java.util.List; public class Config { private String language = "en"; + public static String[] languages = {"en", "nl", "pl", "tok", "zhc", "zht"}; private List recipeLanguages = new ArrayList<>(); private String serverUrl = "http://localhost:8080"; diff --git a/client/src/main/java/client/utils/server/Endpoints.java b/client/src/main/java/client/utils/server/Endpoints.java index 5b31304..6c23870 100644 --- a/client/src/main/java/client/utils/server/Endpoints.java +++ b/client/src/main/java/client/utils/server/Endpoints.java @@ -4,7 +4,9 @@ import client.utils.ConfigService; import com.google.inject.Inject; import java.net.URI; +import java.net.URLEncoder; import java.net.http.HttpRequest; +import java.nio.charset.StandardCharsets; import java.util.List; public class Endpoints { @@ -81,9 +83,23 @@ public class Endpoints { } public HttpRequest.Builder getRecipesWith(String params) { + if (params != null && params.contains("search=")) { + int start = params.indexOf("search=") + "search=".length(); + int end = params.indexOf('&', start); + if (end == -1) { + end = params.length(); + } + + String rawValue = params.substring(start, end); + String encodedValue = URLEncoder.encode(rawValue, StandardCharsets.UTF_8); + + params = params.substring(0, start) + encodedValue + params.substring(end); + } + return this.http(this.createApiUrl("/recipes?" + params)).GET(); } + public HttpRequest.Builder createIngredient(HttpRequest.BodyPublisher body) { String url = this.createApiUrl("/ingredients"); diff --git a/client/src/main/java/client/utils/server/ServerUtils.java b/client/src/main/java/client/utils/server/ServerUtils.java index 5330fdd..8bd7f3b 100644 --- a/client/src/main/java/client/utils/server/ServerUtils.java +++ b/client/src/main/java/client/utils/server/ServerUtils.java @@ -1,19 +1,18 @@ package client.utils.server; import client.utils.ConfigService; +import client.exception.DuplicateIngredientException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Inject; import commons.Ingredient; import commons.Recipe; import commons.RecipeIngredient; -import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.ClientBuilder; import org.glassfish.jersey.client.ClientConfig; import java.io.IOException; -import java.net.ConnectException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -171,10 +170,9 @@ public class ServerUtils { .target(this.endpoints.baseUrl()) // .request(APPLICATION_JSON) // .get(); - } catch (ProcessingException e) { - if (e.getCause() instanceof ConnectException) { - return false; - } + } + catch(Exception e){ + return false; //any exception caught will return false, not just processing exception. } return true; } @@ -270,7 +268,7 @@ public class ServerUtils { //creates new ingredients in the ingredient list - public Ingredient createIngredient(String name) throws IOException, InterruptedException { + public Ingredient createIngredient(String name) throws IOException, InterruptedException, DuplicateIngredientException { Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0); String json = objectMapper.writeValueAsString(ingredient); @@ -278,6 +276,11 @@ public class ServerUtils { .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + final int DUPLICATE_STATUS_CODE = 409; + if (response.statusCode() == DUPLICATE_STATUS_CODE) { + throw new DuplicateIngredientException(name); + } + if (response.statusCode() != statusOK) { throw new IOException("Failed to create ingredient. Server responds with: " + response.body()); } diff --git a/client/src/main/resources/client/scenes/FoodpalApplication.fxml b/client/src/main/resources/client/scenes/FoodpalApplication.fxml index c777543..adfffba 100644 --- a/client/src/main/resources/client/scenes/FoodpalApplication.fxml +++ b/client/src/main/resources/client/scenes/FoodpalApplication.fxml @@ -70,6 +70,9 @@ + - + + + + + + + @@ -37,4 +42,6 @@ +