Merge conflict fix

This commit is contained in:
Mei Chang van der Werff 2026-01-13 16:07:36 +01:00
commit 92c1c0eb15
34 changed files with 1161 additions and 164 deletions

View file

@ -1,18 +1,62 @@
# CSEP Template Project # CSEP FoodPal Application (Team 76)
This repository contains the template for the CSE project. Please extend this README.md with sufficient instructions that will illustrate for your TA and the course staff how they can run your project. ## Usage
To run the template project from the command line, you either need to have [Maven](https://maven.apache.org/install.html) installed on your local system (`mvn`) or you need to use the Maven wrapper (`mvnw`). You can then execute The project uses Java 25. Make sure you have the correct Java version installed.
mvn -pl server -am spring-boot:run ### Client
to run the server and The client needs to be launched **after** a server is already running, see Usage.Server section:
```
mvn -pl client -am javafx:run mvn -pl client -am javafx:run
```
to run the client. Please note that the server needs to be running, before you can start the client. ### Server
Get the template project running from the command line first to ensure you have the required tools on your sytem. By default, the server listens to the port `8080`.
[TODO(1)]:: Configurable port.
Once it is working, you can try importing the project into your favorite IDE. Especially the client is a bit more tricky to set up there due to the dependency on a JavaFX SDK. ```
To help you get started, you can find additional instructions in the corresponding README of the client project. mvn -pl server -am spring-boot:run
```
## Features
- Recipe tracking in an intuitive GUI.
- Ability to input arbitrary amounts of an ingredient in a recipe. The design is very human. See Manual.Ingredients for more usage details.
- Native localization in more than 2, and less than 4 languages.
- Configurable via JSON, See Manual.Configuration.
## Manual
### Configuration
The configuration is with JSON, read from `config.json` in the working directory. We illustrate an example configuration below.
```json
{
"language": "en",
"serverUrl": "http://localhost:8080",
"favourites": [
1,
],
"shoppingList": [
"Ingredient A",
],
}
```
#### Options
- `language: string`
- One of \[en, nl, pl\] (as of Jan 11 2026)
- `serverUrl: string`
- The host that the FoodPal server runs on, see configuration example.
- `favourites: [number]`
- The list of recipe IDs that the user has marked as favourite.
- `shoppingList: [string]`
- The list of ingredients that the user has in their shopping list.
### Ingredients
- To input a **formal** ingredient, you write the numeric amount in the first input box, then the Unit in the selection dropdown, and then write the name of the ingredient, e.g. salt, apples, etc. should it be not visible already on the platform.
- To input an **informal** ingredient, describe the amount in the first input box, like "some of", or "a sprinkle of", then select "<NONE>" in the unit selection box, and write the name of your ingredient or pick from one of the availables from the dropdown.

View file

@ -0,0 +1,131 @@
package client.Ingredient;
import commons.Ingredient;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Alert.AlertType;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.control.Button;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
public class IngredientController {
@FXML
private ListView<Ingredient> ingredientListView;
@FXML
private Button deleteButton;
private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client
@FXML
private void handleDeleteIngredient(ActionEvent event) {
// Get selected ingredient
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
if (selectedIngredient == null) {
showError("No ingredient selected", "Please select an ingredient to delete.");
return;
}
// Check if the ingredient is used in any recipe
checkIngredientUsage(selectedIngredient);
}
private void checkIngredientUsage(Ingredient ingredient) {
String url = "http://localhost:8080/api/ingredients/" + ingredient.getId() + "/usage";
ResponseEntity<IngredientUsageResponse> response = restTemplate.getForEntity(url, IngredientUsageResponse.class);
if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
showError("Ingredient not found", "The ingredient does not exist.");
return;
}
IngredientUsageResponse usageResponse = response.getBody();
long usedInRecipes = usageResponse != null ? usageResponse.getUsedInRecipes() : 0;
if (usedInRecipes > 0) {
// If ingredient is in use, show warning to the user
showWarningDialog(ingredient, usedInRecipes);
} else {
// delete if not in use
deleteIngredient(ingredient);
}
}
private void showWarningDialog(Ingredient ingredient, long usedInRecipes) {
Alert alert = new Alert(AlertType.WARNING);
alert.setTitle("Warning");
alert.setHeaderText("Ingredient in Use");
alert.setContentText("The ingredient '" + ingredient.getName() + "' is used in " + usedInRecipes + " recipe(s). Are you sure you want to delete it?");
ButtonType deleteButton = new ButtonType("Delete Anyway");
ButtonType cancelButton = new ButtonType("Cancel");
alert.getButtonTypes().setAll(deleteButton, cancelButton);
alert.showAndWait().ifPresent(response -> {
if (response == deleteButton) {
// delete if the user confirms
deleteIngredient(ingredient);
}
});
}
private void deleteIngredient(Ingredient ingredient) {
String url = "http://localhost:8080/api/ingredients/" + ingredient.getId();
restTemplate.delete(url);
showConfirmation("Deletion Successful", "The ingredient '" + ingredient.getName() + "' has been successfully deleted.");
// refresh
refreshIngredientList();
}
private void showError(String title, String message) {
Alert alert = new 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(AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
private void refreshIngredientList() {
// Refresh
ingredientListView.getItems().clear();
}
// Inner class for usage response
public static class IngredientUsageResponse {
private long ingredientId;
private long usedInRecipes;
public long getIngredientId() {
return ingredientId;
}
public void setIngredientId(long ingredientId) {
this.ingredientId = ingredientId;
}
public long getUsedInRecipes() {
return usedInRecipes;
}
public void setUsedInRecipes(long usedInRecipes) {
this.usedInRecipes = usedInRecipes;
}
}
}

View file

@ -26,4 +26,8 @@ public class UI extends Application {
var mainCtrl = INJECTOR.getInstance(MainCtrl.class); var mainCtrl = INJECTOR.getInstance(MainCtrl.class);
mainCtrl.setup(primaryStage, foodpal); mainCtrl.setup(primaryStage, foodpal);
} }
public static MyFXML getFXML() {
return FXML;
}
} }

View file

@ -10,6 +10,7 @@ import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import client.exception.InvalidModificationException; import client.exception.InvalidModificationException;
import client.scenes.recipe.IngredientsPopupCtrl;
import client.scenes.recipe.RecipeDetailCtrl; import client.scenes.recipe.RecipeDetailCtrl;
import client.utils.Config; import client.utils.Config;
@ -20,6 +21,7 @@ import client.utils.LocaleManager;
import client.utils.ServerUtils; import client.utils.ServerUtils;
import client.utils.WebSocketDataService; import client.utils.WebSocketDataService;
import client.utils.WebSocketUtils; import client.utils.WebSocketUtils;
import commons.Ingredient;
import commons.Recipe; import commons.Recipe;
import commons.ws.Topics; import commons.ws.Topics;
@ -36,17 +38,20 @@ import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListCell; import javafx.scene.control.ListCell;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleButton;
import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
public class FoodpalApplicationCtrl implements LocaleAware { public class FoodpalApplicationCtrl implements LocaleAware {
private final ServerUtils server; private final ServerUtils server;
private final WebSocketUtils webSocketUtils; private final WebSocketUtils webSocketUtils;
private final LocaleManager localeManager; private final LocaleManager localeManager;
private final WebSocketDataService<Long, Recipe> dataService; private final WebSocketDataService<Long, Recipe> dataService;
private final Logger logger = Logger.getLogger(FoodpalApplicationCtrl.class.getName()); private final Logger logger = Logger.getLogger(FoodpalApplicationCtrl.class.getName());
@FXML @FXML
private RecipeDetailCtrl recipeDetailController; private RecipeDetailCtrl recipeDetailController;
@ -60,6 +65,9 @@ public class FoodpalApplicationCtrl implements LocaleAware {
@FXML @FXML
public ListView<Recipe> recipeList; public ListView<Recipe> recipeList;
@FXML
private ListView<Ingredient> ingredientListView;
@FXML @FXML
private Button addRecipeButton; private Button addRecipeButton;
@ -93,6 +101,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
this.configService = configService; this.configService = configService;
this.dataService = recipeDataService; this.dataService = recipeDataService;
setupDataService(); setupDataService();
logger.info("WebSocket processor initialized."); logger.info("WebSocket processor initialized.");
initializeWebSocket(); initializeWebSocket();
@ -268,7 +277,10 @@ public class FoodpalApplicationCtrl implements LocaleAware {
public void refresh() { public void refresh() {
List<Recipe> recipes; List<Recipe> recipes;
try { try {
recipes = server.getRecipesFiltered(searchBarController.getFilter()); recipes = server.getRecipesFiltered(
searchBarController.getFilter(),
this.configService.getConfig().getRecipeLanguages()
);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
recipes = Collections.emptyList(); recipes = Collections.emptyList();
String msg = "Failed to load recipes: " + e.getMessage(); String msg = "Failed to load recipes: " + e.getMessage();
@ -400,6 +412,139 @@ public class FoodpalApplicationCtrl implements LocaleAware {
this.recipeDetailController.refreshFavouriteButton(); this.recipeDetailController.refreshFavouriteButton();
this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty()); 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<String> 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<Ingredient>() {
@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 {
var pair = client.UI.getFXML().load(
IngredientsPopupCtrl.class,
"client", "scenes", "recipe", "IngredientsPopup.fxml"
);
var root = pair.getValue();
var stage = new javafx.stage.Stage();
stage.setTitle("Ingredients");
stage.initModality(javafx.stage.Modality.APPLICATION_MODAL);
stage.setScene(new javafx.scene.Scene(root));
stage.showAndWait();
} catch (Exception e) {
var alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Failed to open ingredients popup");
alert.setContentText(e.getMessage());
alert.showAndWait();
e.printStackTrace();
}
}
} }
@ -410,3 +555,8 @@ public class FoodpalApplicationCtrl implements LocaleAware {

View file

@ -0,0 +1,87 @@
package client.scenes;
import client.utils.ConfigService;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import com.google.inject.Inject;
import javafx.fxml.FXML;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.MenuButton;
import java.util.ArrayList;
import java.util.List;
public class LanguageFilterCtrl implements LocaleAware {
private final LocaleManager manager;
private final ConfigService configService;
private final FoodpalApplicationCtrl appCtrl;
@Inject
public LanguageFilterCtrl(LocaleManager manager, ConfigService configService, FoodpalApplicationCtrl appCtrl) {
this.manager = manager;
this.configService = configService;
this.appCtrl = appCtrl;
}
@FXML
MenuButton langFilterMenu;
List<String> selectedLanguages = new ArrayList<>();
private String getSelectedLanguagesDisplay() {
String joined = String.join(", ", selectedLanguages);
if (joined.isEmpty()) {
return "none";
} else {
return joined;
}
}
private void updateMenuButtonDisplay() {
langFilterMenu.setText(getLocaleString("menu.label.selected-langs") + ": " + this.getSelectedLanguagesDisplay());
}
@Override
public void updateText() {
this.updateMenuButtonDisplay();
}
@Override
public LocaleManager getLocaleManager() {
return this.manager;
}
public void initializeComponents() {
var items = this.langFilterMenu.getItems();
final List<String> languages = List.of("en", "nl", "pl");
this.selectedLanguages = this.configService.getConfig().getRecipeLanguages();
this.updateMenuButtonDisplay();
items.clear();
languages.forEach(lang -> {
CheckMenuItem it = new CheckMenuItem(lang);
if (selectedLanguages.contains(it.getText())) {
it.setSelected(true);
}
it.selectedProperty().addListener((observable, _, value) -> {
if (value) {
selectedLanguages.add(it.getText());
} else {
selectedLanguages.remove(it.getText());
}
configService.save();
selectedLanguages.sort(String::compareTo);
appCtrl.refresh();
this.updateMenuButtonDisplay();
});
items.add(it);
});
}
}

View file

@ -1,5 +1,6 @@
package client.scenes; package client.scenes;
import client.utils.ConfigService;
import client.utils.LocaleAware; import client.utils.LocaleAware;
import client.utils.LocaleManager; import client.utils.LocaleManager;
import client.utils.ServerUtils; import client.utils.ServerUtils;
@ -28,6 +29,7 @@ public class SearchBarCtrl implements LocaleAware {
private final LocaleManager localeManager; private final LocaleManager localeManager;
private final ServerUtils serverUtils; private final ServerUtils serverUtils;
private final ConfigService configService;
private Consumer<List<Recipe>> onSearchCallback; private Consumer<List<Recipe>> onSearchCallback;
@ -41,9 +43,10 @@ public class SearchBarCtrl implements LocaleAware {
private Task<List<Recipe>> currentSearchTask = null; private Task<List<Recipe>> currentSearchTask = null;
@Inject @Inject
public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils) { public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils, ConfigService configService) {
this.localeManager = localeManager; this.localeManager = localeManager;
this.serverUtils = serverUtils; this.serverUtils = serverUtils;
this.configService = configService;
} }
@FXML @FXML
@ -96,7 +99,7 @@ public class SearchBarCtrl implements LocaleAware {
currentSearchTask = new Task<>() { currentSearchTask = new Task<>() {
@Override @Override
protected List<Recipe> call() throws IOException, InterruptedException { protected List<Recipe> call() throws IOException, InterruptedException {
return serverUtils.getRecipesFiltered(filter); return serverUtils.getRecipesFiltered(filter, configService.getConfig().getRecipeLanguages());
} }
}; };

View file

@ -75,6 +75,10 @@ public class IngredientListCell extends OrderedEditableListCell<RecipeIngredient
String name = nameInput.getText(); String name = nameInput.getText();
if (unit == null || !unit.isFormal()) { if (unit == null || !unit.isFormal()) {
String desc = amountInput.getText(); String desc = amountInput.getText();
if (desc.isEmpty() || name.isEmpty()) {
// TODO printError() integration
return; // The user is forced to kindly try again until something valid comes up.
}
Ingredient newIngredient = new Ingredient(name, 0., 0., 0.); Ingredient newIngredient = new Ingredient(name, 0., 0., 0.);
commitEdit(new VagueIngredient(newIngredient, desc)); commitEdit(new VagueIngredient(newIngredient, desc));
return; return;

View file

@ -0,0 +1,136 @@
package client.scenes.recipe;
import client.utils.ServerUtils;
import commons.Ingredient;
import jakarta.inject.Inject;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextInputDialog;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
//TODO and check for capital letter milk and MILK are seen as different
public class IngredientsPopupCtrl {
private final ServerUtils server;
@FXML
private ListView<Ingredient> ingredientListView;
@Inject
public IngredientsPopupCtrl(ServerUtils server) {
this.server = server;
}
@FXML
public void initialize() {
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());
}
}
});
refresh();
}
@FXML
private void addIngredient() {
TextInputDialog dialog = new TextInputDialog();
dialog.setTitle("Add Ingredient");
dialog.setHeaderText("Create a new ingredient");
dialog.setContentText("Name:");
Optional<String> result = dialog.showAndWait();
if (result.isEmpty()) {
return;
}
String name = result.get().trim();
if (name.isEmpty()) {
showError("Ingredient name cannot be empty.");
return;
}
try {
server.createIngredient(name); // calls POST /api/ingredients
refresh(); // reload list from server
} catch (IOException | InterruptedException e) {
showError("Failed to create ingredient: " + e.getMessage());
}
}
@FXML
private void refresh() {
try {
List<Ingredient> ingredients = server.getIngredients();
ingredientListView.getItems().setAll(ingredients);
} catch (IOException | InterruptedException e) {
showError("Failed to load ingredients: " + e.getMessage());
}
}
@FXML
private void deleteSelected() {
Ingredient selected = ingredientListView.getSelectionModel().getSelectedItem();
if (selected == null) {
showError("No ingredient selected.");
return;
}
try {
long usageCount = server.getIngredientUsage(selected.getId());
if (usageCount > 0) {
boolean proceed = confirmDeleteUsed(selected.getName(), usageCount);
if (!proceed) {
return;
}
}
server.deleteIngredient(selected.getId());
refresh();
} catch (IOException | InterruptedException e) {
showError("Failed to delete ingredient: " + e.getMessage());
}
}
@FXML
private void close() {
Stage stage = (Stage) ingredientListView.getScene().getWindow();
stage.close();
}
private boolean confirmDeleteUsed(String name, long usedInRecipes) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Warning");
alert.setHeaderText("Ingredient in use");
alert.setContentText("Ingredient '" + name + "' is used in " + usedInRecipes
+ " recipe(s). Delete anyway?");
var delete = new javafx.scene.control.ButtonType("Delete Anyway");
var cancel = new javafx.scene.control.ButtonType("Cancel");
alert.getButtonTypes().setAll(delete, cancel);
return alert.showAndWait().orElse(cancel) == delete;
}
private void showError(String msg) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText(null);
alert.setContentText(msg);
alert.showAndWait();
}
}

View file

@ -19,6 +19,7 @@ import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@ -77,6 +78,9 @@ public class RecipeDetailCtrl implements LocaleAware {
@FXML @FXML
private Button favouriteButton; private Button favouriteButton;
@FXML
private ComboBox<String> langSelector;
private ListView<Recipe> getParentRecipeList() { private ListView<Recipe> getParentRecipeList() {
return this.appCtrl.recipeList; return this.appCtrl.recipeList;
} }
@ -138,6 +142,7 @@ public class RecipeDetailCtrl implements LocaleAware {
this.showName(recipe.getName()); this.showName(recipe.getName());
this.ingredientListController.refetchFromRecipe(recipe); this.ingredientListController.refetchFromRecipe(recipe);
this.stepListController.refetchFromRecipe(recipe); this.stepListController.refetchFromRecipe(recipe);
this.langSelector.setValue(recipe.getLocale());
this.refreshFavouriteButton(); this.refreshFavouriteButton();
} }
@ -294,6 +299,20 @@ public class RecipeDetailCtrl implements LocaleAware {
editableTitleArea.getChildren().add(nameLabel); editableTitleArea.getChildren().add(nameLabel);
} }
/**
* Switch the recipe's language.
*/
@FXML
void changeLanguage() {
recipe.setLocale(this.langSelector.getValue());
try {
server.updateRecipe(this.recipe);
} catch (IOException | InterruptedException e) {
throw new UpdateException("Error occurred when updating recipe locale!");
}
}
@Override @Override
public void updateText() { public void updateText() {
editRecipeTitleButton.setText(getLocaleString("menu.button.edit")); editRecipeTitleButton.setText(getLocaleString("menu.button.edit"));
@ -309,5 +328,7 @@ public class RecipeDetailCtrl implements LocaleAware {
@Override @Override
public void initializeComponents() { public void initializeComponents() {
initStepsIngredientsList(); initStepsIngredientsList();
langSelector.getItems().addAll("en", "nl", "pl");
} }
} }

View file

@ -6,7 +6,6 @@ import java.util.List;
public class Config { public class Config {
private String language; private String language;
private String serverUrl; private String serverUrl;
private List<Long> favourites; private List<Long> favourites;
private List<String> shoppingList; private List<String> shoppingList;
@ -68,5 +67,28 @@ public class Config {
public void removeFavourite(long recipeId) { public void removeFavourite(long recipeId) {
getFavourites().remove(recipeId); getFavourites().remove(recipeId);
} }
/**
* Get a list of languages that should filter the displayed recipes.
*
* @return The desired languages the user would like to see.
*/
public List<String> getRecipeLanguages() {
return this.recipeLanguages;
}
/**
* Add a language to the list of filtering languages.
*/
public void addRecipeLanguage(String lang) {
this.recipeLanguages.add(lang);
}
/**
* Remove a language from the list of filtering languages.
*/
public void removeRecipeLanguage(String lang) {
this.recipeLanguages.remove(lang);
}
} }

View file

@ -26,6 +26,7 @@ public class DefaultValueFactory {
return new Recipe( return new Recipe(
null, null,
"Untitled recipe", "Untitled recipe",
"en",
List.of(), List.of(),
List.of()); List.of());
} }

View file

@ -3,6 +3,7 @@ package client.utils;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject; import com.google.inject.Inject;
import commons.Ingredient;
import commons.Recipe; import commons.Recipe;
import commons.RecipeIngredient; import commons.RecipeIngredient;
import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.ProcessingException;
@ -39,9 +40,16 @@ public class ServerUtils {
* Gets all the recipes from the backend. * Gets all the recipes from the backend.
* @return a JSON string with all the recipes * @return a JSON string with all the recipes
*/ */
public List<Recipe> getRecipes() throws IOException, InterruptedException { public List<Recipe> getRecipes(List<String> locales) throws IOException, InterruptedException {
String uri =
SERVER +
"/recipes" +
"?locales=" +
String.join(",", locales);
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/recipes")) .uri(URI.create(uri))
.GET() .GET()
.build(); .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
@ -56,9 +64,9 @@ public class ServerUtils {
return list; // JSON string-> List<Recipe> (Jackson) return list; // JSON string-> List<Recipe> (Jackson)
} }
public List<Recipe> getRecipesFiltered(String filter) throws IOException, InterruptedException { public List<Recipe> getRecipesFiltered(String filter, List<String> locales) throws IOException, InterruptedException {
// TODO: implement filtering on server side // TODO: implement filtering on server side
return this.getRecipes(); return this.getRecipes(locales);
} }
/** /**
@ -87,7 +95,7 @@ public class ServerUtils {
*/ */
public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException { public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException {
//Make sure the name of the newRecipe is unique //Make sure the name of the newRecipe is unique
List<Recipe> allRecipes = getRecipes(); List<Recipe> allRecipes = getRecipes(List.of());
newRecipe.setId(null); // otherwise the id is the same as the original, and that's wrong newRecipe.setId(null); // otherwise the id is the same as the original, and that's wrong
// now that each recipeIngredient has its own ID in the database, // now that each recipeIngredient has its own ID in the database,
// we set that to null too to force a new persist value on the server // we set that to null too to force a new persist value on the server
@ -207,4 +215,84 @@ public class ServerUtils {
updateRecipe(recipe); updateRecipe(recipe);
} }
// how many ingredients are getting used in recipes
public long getIngredientUsage(long ingredientId) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/ingredients/" + ingredientId + "/usage"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("Failed to get usage for ingredient with id: " + ingredientId
+ " body: " + response.body());
}
record IngredientUsageResponse(long ingredientId, long usedInRecipes) {}
IngredientUsageResponse usage =
objectMapper.readValue(response.body(), IngredientUsageResponse.class);
return usage.usedInRecipes();
}
public void deleteIngredient(long ingredientId) throws IOException, InterruptedException {
// Send delete request to remove the ingredient
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/ingredients/" + ingredientId))
.DELETE()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("Failed to delete ingredient with id: " + ingredientId + " body: " + response.body());
}
logger.info("Successfully deleted ingredient with id: " + ingredientId);
}
//retrieves the list of ingredients saved to backend
public List<Ingredient> getIngredients() throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/ingredients"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("Failed to fetch ingredients. Server responds with: " + response.body());
}
return objectMapper.readValue(response.body(), new com.fasterxml.jackson.core.type.TypeReference<List<Ingredient>>() {});
}
//creates new ingredients in the ingredient list
public Ingredient createIngredient(String name) throws IOException, InterruptedException {
Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0);
String json = objectMapper.writeValueAsString(ingredient);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/ingredients"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != statusOK) {
throw new IOException("Failed to create ingredient. Server responds with: " + response.body());
}
return objectMapper.readValue(response.body(), Ingredient.class);
}
} }

View file

@ -44,9 +44,11 @@
<Label fx:id="recipesLabel" text="Recipes"> <Label fx:id="recipesLabel" text="Recipes">
<font> <font>
<Font name="System Bold" size="15.0" /> <Font name="System Bold" size="15.0" />
</font></Label> </font>
</Label>
<fx:include source="SearchBar.fxml" fx:id="searchBar" /> <fx:include source="SearchBar.fxml" fx:id="searchBar" />
<fx:include source="LanguageFilter.fxml" fx:id="langFilter" />
<ListView fx:id="recipeList" /> <ListView fx:id="recipeList" />
@ -56,6 +58,11 @@
<Button fx:id="cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" /> <Button fx:id="cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" /> <ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" />
</HBox> </HBox>
<Button fx:id="manageIngredientsButton"
onAction="#openIngredientsPopup"
text="Ingredients..." />
</VBox> </VBox>
</left> </left>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.MenuButton?>
<MenuButton
mnemonicParsing="false"
text="Languages"
xmlns:fx="http://javafx.com/fxml/1"
xmlns="http://javafx.com/javafx/25"
fx:controller="client.scenes.LanguageFilterCtrl"
fx:id="langFilterMenu"
/>

View file

@ -9,9 +9,10 @@
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?> <?import javafx.scene.text.Font?>
<AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.IngredientListCtrl">
<children> <VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
<VBox minHeight="200.0" minWidth="200.0"> fx:controller="client.scenes.recipe.IngredientListCtrl"
minHeight="200.0" minWidth="200.0" maxWidth="Infinity">
<children> <children>
<HBox alignment="CENTER_LEFT" spacing="20.0"> <HBox alignment="CENTER_LEFT" spacing="20.0">
<children> <children>
@ -37,5 +38,3 @@
<ListView fx:id="ingredientListView" prefHeight="200.0" prefWidth="200.0" /> <ListView fx:id="ingredientListView" prefHeight="200.0" prefWidth="200.0" />
</children> </children>
</VBox> </VBox>
</children>
</AnchorPane>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="client.scenes.recipe.IngredientsPopupCtrl"
spacing="10" prefWidth="420" prefHeight="520">
<padding>
<Insets top="12" right="12" bottom="12" left="12"/>
</padding>
<Label text="Ingredients" style="-fx-font-size: 18px; -fx-font-weight: bold;"/>
<ListView fx:id="ingredientListView" VBox.vgrow="ALWAYS"/>
<ButtonBar>
<buttons>
<Button text="Add" onAction="#addIngredient"/>
<Button text="Refresh" onAction="#refresh"/>
<Button text="Delete" onAction="#deleteSelected"/>
<Button text="Close" onAction="#close"/>
</buttons>
</ButtonBar>
</VBox>

View file

@ -27,9 +27,13 @@
<Button fx:id="favouriteButton" onAction="#toggleFavourite" text="☆" /> <Button fx:id="favouriteButton" onAction="#toggleFavourite" text="☆" />
</HBox> </HBox>
<ComboBox fx:id="langSelector" onAction="#changeLanguage" />
<!-- Ingredients --> <!-- Ingredients -->
<fx:include source="IngredientList.fxml" fx:id="ingredientList" /> <fx:include source="IngredientList.fxml" fx:id="ingredientList"
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
<!-- Preparation --> <!-- Preparation -->
<fx:include source="RecipeStepList.fxml" fx:id="stepList" /> <fx:include source="RecipeStepList.fxml" fx:id="stepList"
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
</VBox> </VBox>

View file

@ -9,9 +9,9 @@
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?> <?import javafx.scene.text.Font?>
<AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.RecipeStepListCtrl"> <VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
<children> fx:controller="client.scenes.recipe.RecipeStepListCtrl"
<VBox minHeight="200.0" minWidth="200.0"> minHeight="200.0" minWidth="200.0" maxWidth="Infinity">
<children> <children>
<HBox alignment="CENTER_LEFT" spacing="20.0"> <HBox alignment="CENTER_LEFT" spacing="20.0">
<children> <children>
@ -37,5 +37,3 @@
<ListView fx:id="recipeStepListView" prefHeight="200.0" prefWidth="200.0" /> <ListView fx:id="recipeStepListView" prefHeight="200.0" prefWidth="200.0" />
</children> </children>
</VBox> </VBox>
</children>
</AnchorPane>

View file

@ -25,6 +25,8 @@ menu.button.edit=Edit
menu.button.clone=Clone menu.button.clone=Clone
menu.button.print=Print recipe menu.button.print=Print recipe
menu.label.selected-langs=Languages
lang.en.display=English lang.en.display=English
lang.nl.display=Dutch lang.nl.display=Dutch
lang.pl.display=Polish lang.pl.display=Polish

View file

@ -26,6 +26,9 @@ menu.button.clone=Clone
menu.button.print=Print recipe menu.button.print=Print recipe
menu.search=Search... menu.search=Search...
menu.label.selected-langs=Languages
lang.en.display=English lang.en.display=English
lang.nl.display=Nederlands lang.nl.display=Nederlands
lang.pl.display=Polski lang.pl.display=Polski

Some files were not shown because too many files have changed in this diff Show more