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:
commit
6caf4eaa61
35 changed files with 1312 additions and 205 deletions
64
README.md
64
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
131
client/src/main/java/client/Ingredient/IngredientController.java
Normal file
131
client/src/main/java/client/Ingredient/IngredientController.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
87
client/src/main/java/client/scenes/LanguageFilterCtrl.java
Normal file
87
client/src/main/java/client/scenes/LanguageFilterCtrl.java
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ 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.TextInputDialog;
|
import javafx.scene.control.TextInputDialog;
|
||||||
|
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;
|
||||||
|
|
@ -82,6 +83,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;
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +147,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,6 +336,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"));
|
||||||
|
|
@ -346,5 +365,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
@Override
|
@Override
|
||||||
public void initializeComponents() {
|
public void initializeComponents() {
|
||||||
initStepsIngredientsList();
|
initStepsIngredientsList();
|
||||||
|
|
||||||
|
langSelector.getItems().addAll("en", "nl", "pl");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,18 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Config {
|
public class Config {
|
||||||
|
|
||||||
|
|
||||||
private String language = "en";
|
private String language = "en";
|
||||||
|
private List<String> recipeLanguages = new ArrayList<>();
|
||||||
private String serverUrl = "http://localhost:8080";
|
private String serverUrl = "http://localhost:8080";
|
||||||
|
|
||||||
private List<Long> favourites = new ArrayList<>();
|
private List<Long> favourites = new ArrayList<>();
|
||||||
private List<String> shoppingList = new ArrayList<>();
|
private List<String> shoppingList = new ArrayList<>();
|
||||||
|
|
||||||
public Config() {
|
public Config() {
|
||||||
|
this.language = "en";
|
||||||
|
this.serverUrl = "http://localhost:8080";
|
||||||
|
this.favourites = new ArrayList<>();
|
||||||
|
this.shoppingList = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLanguage() {
|
public String getLanguage() {
|
||||||
|
|
@ -66,5 +69,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
<Font name="System Bold" size="29.0" />
|
<Font name="System Bold" size="29.0" />
|
||||||
</font>
|
</font>
|
||||||
</Label>
|
</Label>
|
||||||
</HBox>
|
</HBox>
|
||||||
</top>
|
</top>
|
||||||
|
|
||||||
<!-- LEFT: RECIPE LIST -->
|
<!-- LEFT: RECIPE LIST -->
|
||||||
|
|
@ -44,18 +44,25 @@
|
||||||
<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" />
|
||||||
|
|
||||||
<HBox spacing="10">
|
<HBox spacing="10">
|
||||||
<Button fx:id="addRecipeButton" onAction="#addRecipe" text="Add Recipe" />
|
<Button fx:id="addRecipeButton" onAction="#addRecipe" text="Add Recipe" />
|
||||||
<Button fx:id="removeRecipeButton" onAction="#removeSelectedRecipe" text="Remove Recipe" />
|
<Button fx:id="removeRecipeButton" onAction="#removeSelectedRecipe" text="Remove Recipe" />
|
||||||
<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>
|
||||||
|
|
||||||
|
|
|
||||||
12
client/src/main/resources/client/scenes/LanguageFilter.fxml
Normal file
12
client/src/main/resources/client/scenes/LanguageFilter.fxml
Normal 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"
|
||||||
|
/>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -27,6 +27,8 @@
|
||||||
<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" />
|
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ menu.button.edit=Bewerken
|
||||||
menu.button.clone=Dupliceren
|
menu.button.clone=Dupliceren
|
||||||
menu.button.print=Recept afdrukken
|
menu.button.print=Recept afdrukken
|
||||||
|
|
||||||
|
menu.label.selected-langs=Talen
|
||||||
|
|
||||||
menu.search=Zoeken...
|
menu.search=Zoeken...
|
||||||
lang.en.display=English
|
lang.en.display=English
|
||||||
lang.nl.display=Nederlands
|
lang.nl.display=Nederlands
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ menu.button.clone=Duplikuj
|
||||||
menu.button.print=Drukuj przepis
|
menu.button.print=Drukuj przepis
|
||||||
|
|
||||||
menu.search=Szukaj...
|
menu.search=Szukaj...
|
||||||
|
|
||||||
|
menu.label.selected-langs=J?zyki
|
||||||
|
|
||||||
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
Loading…
Reference in a new issue