Merge branch 'main' into 'fix/wiring_print_button_with_functionality'

# Conflicts: fixed import merge conflict
#   client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java
This commit is contained in:
Rithvik Sriram 2026-01-14 22:15:53 +01:00
commit 6caf4eaa61
35 changed files with 1310 additions and 203 deletions

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);
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 client.exception.InvalidModificationException;
import client.scenes.recipe.IngredientsPopupCtrl;
import client.scenes.recipe.RecipeDetailCtrl;
import client.utils.Config;
@ -20,6 +21,7 @@ import client.utils.LocaleManager;
import client.utils.ServerUtils;
import client.utils.WebSocketDataService;
import client.utils.WebSocketUtils;
import commons.Ingredient;
import commons.Recipe;
import commons.ws.Topics;
@ -36,17 +38,20 @@ 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 org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
public class FoodpalApplicationCtrl implements LocaleAware {
private final ServerUtils server;
private final WebSocketUtils webSocketUtils;
private final LocaleManager localeManager;
private final WebSocketDataService<Long, Recipe> dataService;
private final Logger logger = Logger.getLogger(FoodpalApplicationCtrl.class.getName());
@FXML
private RecipeDetailCtrl recipeDetailController;
@ -60,6 +65,9 @@ public class FoodpalApplicationCtrl implements LocaleAware {
@FXML
public ListView<Recipe> recipeList;
@FXML
private ListView<Ingredient> ingredientListView;
@FXML
private Button addRecipeButton;
@ -93,6 +101,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
this.configService = configService;
this.dataService = recipeDataService;
setupDataService();
logger.info("WebSocket processor initialized.");
initializeWebSocket();
@ -268,7 +277,10 @@ public class FoodpalApplicationCtrl implements LocaleAware {
public void refresh() {
List<Recipe> recipes;
try {
recipes = server.getRecipesFiltered(searchBarController.getFilter());
recipes = server.getRecipesFiltered(
searchBarController.getFilter(),
this.configService.getConfig().getRecipeLanguages()
);
} catch (IOException | InterruptedException e) {
recipes = Collections.emptyList();
String msg = "Failed to load recipes: " + e.getMessage();
@ -400,6 +412,139 @@ 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<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;
import client.utils.ConfigService;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import client.utils.ServerUtils;
@ -28,6 +29,7 @@ public class SearchBarCtrl implements LocaleAware {
private final LocaleManager localeManager;
private final ServerUtils serverUtils;
private final ConfigService configService;
private Consumer<List<Recipe>> onSearchCallback;
@ -41,9 +43,10 @@ public class SearchBarCtrl implements LocaleAware {
private Task<List<Recipe>> currentSearchTask = null;
@Inject
public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils) {
public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils, ConfigService configService) {
this.localeManager = localeManager;
this.serverUtils = serverUtils;
this.configService = configService;
}
@FXML
@ -96,7 +99,7 @@ public class SearchBarCtrl implements LocaleAware {
currentSearchTask = new Task<>() {
@Override
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();
if (unit == null || !unit.isFormal()) {
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.);
commitEdit(new VagueIngredient(newIngredient, desc));
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

@ -24,6 +24,7 @@ import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
@ -82,6 +83,9 @@ public class RecipeDetailCtrl implements LocaleAware {
@FXML
private Button favouriteButton;
@FXML
private ComboBox<String> langSelector;
private ListView<Recipe> getParentRecipeList() {
return this.appCtrl.recipeList;
}
@ -143,6 +147,7 @@ public class RecipeDetailCtrl implements LocaleAware {
this.showName(recipe.getName());
this.ingredientListController.refetchFromRecipe(recipe);
this.stepListController.refetchFromRecipe(recipe);
this.langSelector.setValue(recipe.getLocale());
this.refreshFavouriteButton();
}
@ -331,6 +336,20 @@ public class RecipeDetailCtrl implements LocaleAware {
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
public void updateText() {
editRecipeTitleButton.setText(getLocaleString("menu.button.edit"));
@ -346,5 +365,7 @@ public class RecipeDetailCtrl implements LocaleAware {
@Override
public void initializeComponents() {
initStepsIngredientsList();
langSelector.getItems().addAll("en", "nl", "pl");
}
}

View file

@ -4,15 +4,18 @@ import java.util.ArrayList;
import java.util.List;
public class Config {
private String language = "en";
private List<String> recipeLanguages = new ArrayList<>();
private String serverUrl = "http://localhost:8080";
private List<Long> favourites = new ArrayList<>();
private List<String> shoppingList = new ArrayList<>();
public Config() {
this.language = "en";
this.serverUrl = "http://localhost:8080";
this.favourites = new ArrayList<>();
this.shoppingList = new ArrayList<>();
}
public String getLanguage() {
@ -66,5 +69,28 @@ public class Config {
public void removeFavourite(long 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(
null,
"Untitled recipe",
"en",
List.of(),
List.of());
}

View file

@ -3,6 +3,7 @@ package client.utils;
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;
@ -39,9 +40,16 @@ public class ServerUtils {
* Gets all the recipes from the backend.
* @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()
.uri(URI.create(SERVER + "/recipes"))
.uri(URI.create(uri))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
@ -56,9 +64,9 @@ public class ServerUtils {
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
return this.getRecipes();
return this.getRecipes(locales);
}
/**
@ -87,7 +95,7 @@ public class ServerUtils {
*/
public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException {
//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
// 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
@ -207,4 +215,84 @@ public class ServerUtils {
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

@ -30,7 +30,7 @@
<Font name="System Bold" size="29.0" />
</font>
</Label>
</HBox>
</HBox>
</top>
<!-- LEFT: RECIPE LIST -->
@ -44,18 +44,25 @@
<Label fx:id="recipesLabel" text="Recipes">
<font>
<Font name="System Bold" size="15.0" />
</font></Label>
</font>
</Label>
<fx:include source="SearchBar.fxml" fx:id="searchBar" />
<fx:include source="LanguageFilter.fxml" fx:id="langFilter" />
<ListView fx:id="recipeList" />
<HBox spacing="10">
<Button fx:id="addRecipeButton" onAction="#addRecipe" text="Add Recipe" />
<Button fx:id="removeRecipeButton" onAction="#removeSelectedRecipe" text="Remove Recipe" />
<Button fx:id= "cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView"/>
<Button fx:id="cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" />
</HBox>
<Button fx:id="manageIngredientsButton"
onAction="#openIngredientsPopup"
text="Ingredients..." />
</VBox>
</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

@ -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,6 +27,8 @@
<Button fx:id="favouriteButton" onAction="#toggleFavourite" text="☆" />
</HBox>
<ComboBox fx:id="langSelector" onAction="#changeLanguage" />
<!-- Ingredients -->
<fx:include source="IngredientList.fxml" fx:id="ingredientList"
VBox.vgrow="ALWAYS" maxWidth="Infinity" />

View file

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

View file

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

View file

@ -25,6 +25,8 @@ menu.button.edit=Bewerken
menu.button.clone=Dupliceren
menu.button.print=Recept afdrukken
menu.label.selected-langs=Talen
menu.search=Zoeken...
lang.en.display=English
lang.nl.display=Nederlands

View file

@ -26,6 +26,9 @@ menu.button.clone=Duplikuj
menu.button.print=Drukuj przepis
menu.search=Szukaj...
menu.label.selected-langs=J?zyki
lang.en.display=English
lang.nl.display=Nederlands
lang.pl.display=Polski

View file

@ -49,7 +49,7 @@ class ServerUtilsTest {
void tearDown() throws IOException, InterruptedException {
// Not applicable in pipeline testing
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
dv.getRecipes().stream().map(Recipe::getId).forEach(id -> {
dv.getRecipes(List.of()).stream().map(Recipe::getId).forEach(id -> {
try {
dv.deleteRecipe(id);
} catch (Exception ex) {
@ -77,7 +77,7 @@ class ServerUtilsTest {
@Test
void getAllRecipesTest() throws IOException, InterruptedException {
List<Recipe> recipes = dv.getRecipes();
List<Recipe> recipes = dv.getRecipes(List.of());
assertNotNull(recipes, "The list should not be null");
assertTrue(recipes.size() >= 0, "The list should be 0 (when no recipes), or more");

View file

@ -1,96 +1,120 @@
package client.scenes;
import client.utils.Config;
import client.utils.ConfigService;
import client.utils.ServerUtils;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ConfigServiceTest {
static ServerUtils dv = new ServerUtils();
@TempDir
Path tempDir;
private static final long TEST_ID_A = 23412L;
private static final long TEST_ID_B = 25412L;
private Path configPath;
private ConfigService configService;
@BeforeEach
public void setup(){
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
void setUp() throws IOException {
configPath = tempDir.resolve("configServiceTest.json");
configService = new ConfigService(configPath);
}
@Test
public void constructorTest(){
assertEquals(configPath, configService.getConfigPath());
assertNotNull(configService.getMapper());
assertNotNull(configService.getConfig());
}
/*
Tests if the config file loads properly with a prewritten json file
*/
@Test
public void configServiceFileLoadTest(@TempDir Path tempDir) throws IOException {
Path configPath = tempDir.resolve("config.json");
public void constructorFileDNETest(){
Path nonExistentPath = tempDir.resolve("DNE.json");
ConfigService newService = new ConfigService(nonExistentPath);
assertNotNull(newService.getConfig());
assertEquals("en", newService.getConfig().getLanguage());
assertEquals("http://localhost:8080", newService.getConfig().getServerUrl());
assertTrue(newService.getConfig().getFavourites().isEmpty());
assertTrue(newService.getConfig().getShoppingList().isEmpty());
}
@Test
public void validJsonLoadTest() throws IOException {
String json = """
{
"language": "de",
"serverUrl": "http://exmple12.com",
"favourites": [23412, 25412],
"serverUrl": "http://example.com:8080",
"favourites": [123, 456],
"shoppingList": ["milk", "butter"]
}
""";
Files.writeString(configPath, json); //writes into path
ConfigService configService = new ConfigService(configPath);//initiates configservice
Config config = configService.getConfig(); //checks
Files.writeString(configPath, json);
ConfigService loadedService = new ConfigService(configPath);
Config loadedConfig = loadedService.getConfig();
assertEquals("de", config.getLanguage());
assertEquals("http://exmple12.com", config.getServerUrl());
List<Long> x = new ArrayList<>();
x.add(TEST_ID_A);
x.add(TEST_ID_B);
List<String> y = new ArrayList<>();
y.add("milk");
y.add("butter");
assertEquals(x , config.getFavourites());
assertEquals(y , config.getShoppingList());
assertEquals("de", loadedConfig.getLanguage());
assertEquals("http://example.com:8080", loadedConfig.getServerUrl());
assertEquals(2, loadedConfig.getFavourites().size());
assertTrue(loadedConfig.getFavourites().contains(123L));
assertTrue(loadedConfig.getFavourites().contains(456L));
assertEquals(2, loadedConfig.getShoppingList().size());
assertTrue(loadedConfig.getShoppingList().contains("milk"));
assertTrue(loadedConfig.getShoppingList().contains("butter"));
}
/*
Tests if the save method saves changes to the config file.
*/
@Test
public void configSaveTest(@TempDir Path tempDir) throws IOException {
Path configPath = tempDir.resolve("config.json");
ConfigService configService = new ConfigService(configPath);
public void invalidJsonLoadTest() throws IOException {
Files.writeString(configPath, "{ invalid json text");
Config config = configService.getConfig();
assertThrows(RuntimeException.class, () -> {
ConfigService corruptedService = new ConfigService(configPath);
corruptedService.getConfig();
});
}
@Test
public void safeTest(){
Config realConfig = new Config();
realConfig.setLanguage("de");
realConfig.setServerUrl("http://example.com:8080");
realConfig.addFavourite(1L);
realConfig.addFavourite(2L);
realConfig.setShoppingList(List.of("milk", "bread"));
config.setLanguage("fr");
config.setServerUrl("www.domain1.com");
configService.setConfig(realConfig);
configService.save();
String jsonTest = Files.readString(configPath);
assertTrue(jsonTest.contains("\"language\":\"fr\""));
assertTrue(jsonTest.contains("\"serverUrl\":\"www.domain1.com\""));
ConfigService loadedService = new ConfigService(configPath);
Config loadedConfig = loadedService.getConfig();
assertEquals("de", loadedConfig.getLanguage());
assertEquals("http://example.com:8080", loadedConfig.getServerUrl());
assertEquals(2, loadedConfig.getFavourites().size());
assertTrue(loadedConfig.getFavourites().contains(1L));
assertTrue(loadedConfig.getFavourites().contains(2L));
assertEquals(2, loadedConfig.getShoppingList().size());
assertTrue(loadedConfig.getShoppingList().contains("milk"));
assertTrue(loadedConfig.getShoppingList().contains("bread"));
}
@Test
public void multipleTimesSaveTest(){
Config realConfig = new Config();
configService.setConfig(realConfig);
configService.save();
realConfig.setLanguage("de");
configService.save();
realConfig.setServerUrl("http://example.com:8080");
configService.save();
ConfigService loadedService = new ConfigService(configPath);
Config loadedConfig = loadedService.getConfig();
assertEquals("de", loadedConfig.getLanguage());
assertEquals("http://example.com:8080", loadedConfig.getServerUrl());
}
}

View file

@ -1,8 +1,6 @@
package client.scenes;
import client.utils.Config;
import client.utils.ServerUtils;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -11,42 +9,64 @@ import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
public class ConfigTest {
static ServerUtils dv = new ServerUtils();
private Config config;
private static final long FAV_LIST_ID_1 = 1234L;
private static final long FAV_LIST_ID_2 = 1235L;
@BeforeEach
public void setup(){
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
void setUp(){
config = new Config();
config.setLanguage("nl");
config.setServerUrl("http://localhost:8081");
}
@Test
public void configDefaultValueTest(){
Config config = new Config();
assertEquals("en", config.getLanguage());
assertEquals("http://localhost:8080", config.getServerUrl());
assertEquals("nl", config.getLanguage());
assertEquals("http://localhost:8081", config.getServerUrl());
assertTrue(config.getFavourites().isEmpty());
assertTrue(config.getShoppingList().isEmpty());
}
@Test
public void configGetterTest(){
Config config = new Config();
config.setLanguage("nl");
config.setServerUrl("http://localhost:8081");
ArrayList<String> x = new ArrayList<>();
x.add("Lava Cake");
x.add("Brownie");
ArrayList<Long> y = new ArrayList<>();
y.add(FAV_LIST_ID_1);
y.add(FAV_LIST_ID_2);
config.setFavourites(y);
assertEquals(config.getFavourites(), y);
config.setShoppingList(x);
assertEquals(config.getShoppingList(), x);
}
@Test
public void isFavTest(){
assertFalse(config.isFavourite(FAV_LIST_ID_1)); //not yet fav
ArrayList<Long> y = new ArrayList<>();
y.add(FAV_LIST_ID_1);
config.setFavourites(y);
assertTrue(config.isFavourite(FAV_LIST_ID_1));
}
@Test
public void removeFavTest(){
ArrayList<Long> y = new ArrayList<>();
y.add(FAV_LIST_ID_1);
config.setFavourites(y);
assertTrue(config.isFavourite(FAV_LIST_ID_1));
config.removeFavourite(FAV_LIST_ID_1);
assertFalse(config.isFavourite(FAV_LIST_ID_1));
}
}

View file

@ -2,11 +2,9 @@ package client.scenes;
import client.utils.DefaultValueFactory;
import client.utils.PrintExportService;
import client.utils.ServerUtils;
import commons.Recipe;
import commons.RecipeIngredient;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@ -19,22 +17,20 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class PrintExportTest {
static ServerUtils dv = new ServerUtils();
@BeforeEach
public void setup(){
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
}
@TempDir
Path tempDir;
@Test
public void buildRecipeTextTest(){
List<RecipeIngredient> ingredients = new ArrayList<>();
ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Banana"));
ingredients.add(DefaultValueFactory.getDefaultVagueIngredient("Bread"));
final long testRecipeId = 1234L;
List<String> preparationSteps = new ArrayList<>();
preparationSteps.add("Mix Ingredients");
preparationSteps.add("Heat in Oven");
Recipe recipe1 = new Recipe(testRecipeId, "Banana Bread", ingredients, preparationSteps);
Recipe recipe1 = new Recipe(testRecipeId, "Banana Bread", "en", ingredients, preparationSteps);
assertEquals("""
Title: Banana Bread
@ -44,11 +40,8 @@ public class PrintExportTest {
1: Mix Ingredients
2: Heat in Oven
""", PrintExportService.buildRecipeText(recipe1));
}
@TempDir
Path tempDir;
@Test
public void validateFolderWithValidFolderTest(){
assertDoesNotThrow(() -> PrintExportService.validateFolder(tempDir));
@ -168,4 +161,27 @@ public class PrintExportTest {
}
@Test
public void succesExportTest() throws IOException {
String data = "recipe data";
String fileName = "succes.txt";
Path filePath = tempDir.resolve(fileName);
PrintExportService.exportToFile(data,tempDir,fileName);
assertTrue(Files.exists(filePath));
assertEquals(data, Files.readString(filePath));
}
@Test
public void failExportTest(){
String data = "recipe data";
String fileName = "succes.txt";
Path filePath = tempDir.resolve("fail/failDir");
IllegalArgumentException i = assertThrows(IllegalArgumentException.class,
()->PrintExportService.exportToFile(data,filePath,fileName));
assertEquals("Folder does not exist", i.getMessage());
}
}