Compare commits
73 commits
46ba82bc0d
...
b4c5652070
| Author | SHA1 | Date | |
|---|---|---|---|
|
b4c5652070 |
|||
|
|
b942862dc9 | ||
|
|
bc852ce3d6 | ||
|
|
7946cd3df2 | ||
|
|
c1301dad28 | ||
| 167a80c84a | |||
| 3e8442e517 | |||
|
91a8a13369 |
|||
|
|
33e7c7386e | ||
|
|
a5cdcaac14 | ||
|
|
2c54833c6b | ||
|
|
b5348a4c24 | ||
|
|
6d4c33fa10 | ||
| e17a75fcd5 | |||
|
827fe195a9 |
|||
|
dda01f3bfe |
|||
|
a3c08170d3 |
|||
|
|
b82a8f7d33 | ||
| 1fd3b26faa | |||
| 186e74ddd3 | |||
|
|
f1fc55f674 | ||
| bfe6505c54 | |||
|
f281ce1333 |
|||
|
1542117ac6 |
|||
|
|
4d57f84ccb | ||
|
|
be1b4b9ee3 | ||
| ad199ea244 | |||
|
7cddc0303c |
|||
| 4c341aadec | |||
|
d23eb34a00 |
|||
|
|
e3e176939c | ||
|
|
91997ae251 | ||
|
234c8c3d64 |
|||
|
670de432c5 |
|||
|
795926298e |
|||
|
f03c12cc0f |
|||
|
|
3ca526e0f1 | ||
| 143b73c23f | |||
| 76852fcd4f | |||
|
483617c5bf |
|||
|
21b2465b91 |
|||
|
c0107d752a |
|||
|
40cfc0b98e |
|||
|
|
d732aee6de | ||
|
|
6cfd6ab82c | ||
| 70dfc7e983 | |||
| 5315fe3df4 | |||
|
|
9077df5164 | ||
|
|
22dd603746 | ||
|
|
cb4a93aeab | ||
|
|
a1df0f5b24 | ||
|
|
e5f7df7318 | ||
|
|
b985aa283f | ||
|
|
274a48c5c2 | ||
| 23d6d6bbf6 | |||
|
|
14a4e157ea | ||
|
|
37fd3b18be | ||
|
4e901f7847 |
|||
|
d83494c2a1 |
|||
|
6931367145 |
|||
|
8615187628 |
|||
|
23447a96d6 |
|||
|
b7bbebe222 |
|||
|
|
2dd8627f58 | ||
|
|
8238e9b462 | ||
|
|
abe9750b9f | ||
|
|
9603742220 | ||
|
|
f7a1deb9b1 | ||
|
|
0acbb13a49 | ||
|
|
9222999505 | ||
|
|
4d17f10323 | ||
|
|
55b3811deb | ||
|
|
110e5e8163 |
54 changed files with 2294 additions and 233 deletions
22
README.md
22
README.md
|
|
@ -6,27 +6,27 @@ The project uses Java 25. Make sure you have the correct Java version installed.
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
By default, the server listens to the port `8080`.
|
By default, the server listens to the port `8080`. If the server is not available, the next immediate free port is opened. You can run the server with:
|
||||||
[TODO(1)]:: Configurable port.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
mvn -pl server -am spring-boot:run
|
mvn -pl server -am spring-boot:run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Implemented features
|
||||||
|
|
||||||
- Recipe tracking in an intuitive GUI.
|
- Full 4.1 basic requirements criteria. Print/Export functionality exports a plain-text file of the recipe to a file in a location of the user's choosing.
|
||||||
- Ability to input arbitrary amounts of an ingredient in a recipe. The design is very human. See Manual.Ingredients for more usage details.
|
- Full 4.2 WebSocket modelling. We have implemented all points in the backlog and created a compliant solution using distinct STOMP messages that distribute handling to multiple distinct callbacks. We also implemented a meaningful addition as an "Updated at {Time}" utility prompt such that the application hints to the user when it may be time to perform a manual refresh to stay up-to-date.
|
||||||
- Native localization in more than 2, and less than 4 languages.
|
- Full 4.3 Nutrition view and list of ingredients. We have implemented each criteria in the product backlog pertaining to this section to a satisfactory level.
|
||||||
- Configurable via JSON, See Manual.Configuration.
|
When in confusion, the user should consult Manual.Ingredients section of this file to see how to add an informal or formal ingredient.
|
||||||
|
We also include a meaningful addition into the application as a pie chart describing the nutritional composition of each recipe between proteins, carbohydrates, and fats, so that the end user gets a much more straightforward presentation for their dietary choices.
|
||||||
|
- Full 4.4 search functionality implemented. Each criteria is met to satisfactory standards and the client submits a search query to the backend with a list of parameters, to which the client proceeds to respond.
|
||||||
|
- Full 4.5 Shopping list functionality. We implemented a functional shopping list to which the user can add/delete/edit ingredients to, as well as an Add Overview when the user decides to add ingredients of a recipe into their shopping list. Printing the list to a file is also supported.
|
||||||
|
- Full 4.6 functionality. All parts of the UI buttons have been linked to their respective resource items. The application has support for 7 languages, including English, Dutch, Chinese (Simplified/Traditional), Polish, Turkish, and toki pona. We include a national flag for each language in a language selection menu for easier interaction with the end user.
|
||||||
|
|
||||||
## Manual
|
## Manual
|
||||||
|
|
||||||
|
|
@ -58,5 +58,5 @@ The configuration is with JSON, read from `config.json` in the working directory
|
||||||
|
|
||||||
### Ingredients
|
### 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 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 from the dropdown.
|
||||||
- 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.
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,13 @@
|
||||||
<artifactId>commons</artifactId>
|
<artifactId>commons</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Source: https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>2.0.17</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ public class IngredientController {
|
||||||
private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client
|
private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void handleDeleteIngredient(ActionEvent event) {
|
public void handleDeleteIngredient(ActionEvent event) {
|
||||||
// Get selected ingredient
|
// Get selected ingredient
|
||||||
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
||||||
if (selectedIngredient == null) {
|
if (selectedIngredient == null) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ import client.scenes.nutrition.NutritionDetailsCtrl;
|
||||||
import client.scenes.nutrition.NutritionViewCtrl;
|
import client.scenes.nutrition.NutritionViewCtrl;
|
||||||
import client.scenes.recipe.IngredientListCtrl;
|
import client.scenes.recipe.IngredientListCtrl;
|
||||||
import client.scenes.recipe.RecipeStepListCtrl;
|
import client.scenes.recipe.RecipeStepListCtrl;
|
||||||
|
import client.scenes.shopping.ShoppingListCtrl;
|
||||||
|
import client.scenes.shopping.ShoppingListNewItemPromptCtrl;
|
||||||
|
import client.service.ShoppingListService;
|
||||||
|
import client.service.ShoppingListServiceImpl;
|
||||||
|
import client.service.ShoppingListViewModel;
|
||||||
import client.utils.ConfigService;
|
import client.utils.ConfigService;
|
||||||
import client.utils.LocaleManager;
|
import client.utils.LocaleManager;
|
||||||
import client.utils.server.ServerUtils;
|
import client.utils.server.ServerUtils;
|
||||||
|
|
@ -57,6 +62,10 @@ public class MyModule implements Module {
|
||||||
binder.bind(new TypeLiteral<WebSocketDataService<Long, Recipe>>() {}).toInstance(
|
binder.bind(new TypeLiteral<WebSocketDataService<Long, Recipe>>() {}).toInstance(
|
||||||
new WebSocketDataService<>()
|
new WebSocketDataService<>()
|
||||||
);
|
);
|
||||||
|
binder.bind(ShoppingListNewItemPromptCtrl.class).in(Scopes.SINGLETON);
|
||||||
|
binder.bind(ShoppingListCtrl.class).in(Scopes.SINGLETON);
|
||||||
|
binder.bind(ShoppingListViewModel.class).toInstance(new ShoppingListViewModel());
|
||||||
|
binder.bind(ShoppingListService.class).to(ShoppingListServiceImpl.class);
|
||||||
binder.bind(new TypeLiteral<WebSocketDataService<Long, Ingredient>>() {}).toInstance(
|
binder.bind(new TypeLiteral<WebSocketDataService<Long, Ingredient>>() {}).toInstance(
|
||||||
new WebSocketDataService<>()
|
new WebSocketDataService<>()
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package client;
|
||||||
|
|
||||||
import client.scenes.FoodpalApplicationCtrl;
|
import client.scenes.FoodpalApplicationCtrl;
|
||||||
import client.scenes.MainCtrl;
|
import client.scenes.MainCtrl;
|
||||||
|
import client.scenes.ServerConnectionDialogCtrl;
|
||||||
import client.utils.server.ServerUtils;
|
import client.utils.server.ServerUtils;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
|
|
@ -27,10 +28,15 @@ public class UI extends Application {
|
||||||
|
|
||||||
var serverUtils = INJECTOR.getInstance(ServerUtils.class);
|
var serverUtils = INJECTOR.getInstance(ServerUtils.class);
|
||||||
if (!serverUtils.isServerAvailable()) {
|
if (!serverUtils.isServerAvailable()) {
|
||||||
var msg = "Server needs to be started before the client, but it does not seem to be available. Shutting down.";
|
var connectionHandler = INJECTOR.getInstance(ServerConnectionDialogCtrl.class);
|
||||||
System.err.println(msg);
|
boolean serverConnected = connectionHandler.promptForURL();
|
||||||
|
|
||||||
|
if(!serverConnected){
|
||||||
|
var msg = "User Cancelled Server connection. Shutting down";
|
||||||
|
System.err.print(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
var foodpal = FXML.load(FoodpalApplicationCtrl.class, "client", "scenes", "FoodpalApplication.fxml");
|
var foodpal = FXML.load(FoodpalApplicationCtrl.class, "client", "scenes", "FoodpalApplication.fxml");
|
||||||
|
|
||||||
var mainCtrl = INJECTOR.getInstance(MainCtrl.class);
|
var mainCtrl = INJECTOR.getInstance(MainCtrl.class);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package client.exception;
|
||||||
|
|
||||||
|
public class DuplicateIngredientException extends Exception{
|
||||||
|
private final String ingredientName;
|
||||||
|
|
||||||
|
public DuplicateIngredientException(String ingredientName){
|
||||||
|
super("An ingredient with name " + ingredientName + " already exists, please provide a different name");
|
||||||
|
this.ingredientName = ingredientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DuplicateIngredientException(String ingredientName, String message){
|
||||||
|
super(message);
|
||||||
|
this.ingredientName = ingredientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIngredientName() {
|
||||||
|
return ingredientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -2,17 +2,17 @@ package client.scenes;
|
||||||
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
|
import client.UI;
|
||||||
import client.exception.InvalidModificationException;
|
import client.exception.InvalidModificationException;
|
||||||
import client.scenes.nutrition.NutritionViewCtrl;
|
import client.scenes.nutrition.NutritionViewCtrl;
|
||||||
import client.scenes.recipe.RecipeDetailCtrl;
|
import client.scenes.recipe.RecipeDetailCtrl;
|
||||||
|
|
||||||
|
import client.scenes.shopping.ShoppingListCtrl;
|
||||||
import client.utils.Config;
|
import client.utils.Config;
|
||||||
import client.utils.ConfigService;
|
import client.utils.ConfigService;
|
||||||
import client.utils.DefaultValueFactory;
|
import client.utils.DefaultValueFactory;
|
||||||
|
|
@ -21,7 +21,6 @@ import client.utils.LocaleManager;
|
||||||
import client.utils.server.ServerUtils;
|
import client.utils.server.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;
|
||||||
|
|
@ -32,15 +31,20 @@ import commons.ws.messages.Message;
|
||||||
import commons.ws.messages.UpdateRecipeMessage;
|
import commons.ws.messages.UpdateRecipeMessage;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.ListProperty;
|
||||||
|
import javafx.beans.property.SimpleListProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
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 javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.stage.Stage;
|
||||||
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;
|
||||||
|
|
@ -66,8 +70,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
@FXML
|
@FXML
|
||||||
public ListView<Recipe> recipeList;
|
public ListView<Recipe> recipeList;
|
||||||
|
|
||||||
@FXML
|
private final ListProperty<Recipe> favouriteRecipeList = new SimpleListProperty<>();
|
||||||
private ListView<Ingredient> ingredientListView;
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button addRecipeButton;
|
private Button addRecipeButton;
|
||||||
|
|
@ -88,8 +91,6 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
@FXML
|
@FXML
|
||||||
private Button manageIngredientsButton;
|
private Button manageIngredientsButton;
|
||||||
|
|
||||||
private List<Recipe> allRecipes = new ArrayList<>();
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label updatedBadge;
|
private Label updatedBadge;
|
||||||
|
|
||||||
|
|
@ -287,6 +288,13 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
openSelectedRecipe();
|
openSelectedRecipe();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
recipeList.getItems().addListener((ListChangeListener.Change<? extends Recipe> c) -> {
|
||||||
|
favouriteRecipeList.set(
|
||||||
|
FXCollections.observableList(
|
||||||
|
recipeList.getItems().stream().filter(r -> config.isFavourite(r.getId())).toList()
|
||||||
|
));
|
||||||
|
System.out.println(favouriteRecipeList);
|
||||||
|
});
|
||||||
|
|
||||||
this.initializeSearchBar();
|
this.initializeSearchBar();
|
||||||
refresh();
|
refresh();
|
||||||
|
|
@ -325,8 +333,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
logger.severe(msg);
|
logger.severe(msg);
|
||||||
printError(msg);
|
printError(msg);
|
||||||
}
|
}
|
||||||
|
recipeList.getItems().setAll(recipes);
|
||||||
allRecipes = new ArrayList<>(recipes);
|
|
||||||
applyRecipeFilterAndKeepSelection();
|
applyRecipeFilterAndKeepSelection();
|
||||||
|
|
||||||
showUpdatedBadge();
|
showUpdatedBadge();
|
||||||
|
|
@ -422,16 +429,20 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
public void applyRecipeFilterAndKeepSelection() {
|
public void applyRecipeFilterAndKeepSelection() {
|
||||||
Recipe selected = recipeList.getSelectionModel().getSelectedItem();
|
Recipe selected = recipeList.getSelectionModel().getSelectedItem();
|
||||||
Long selectedId = selected == null ? null : selected.getId();
|
Long selectedId = selected == null ? null : selected.getId();
|
||||||
|
List<Recipe> view = recipeList.getItems().stream().toList();
|
||||||
List<Recipe> view = allRecipes;
|
|
||||||
|
|
||||||
if (favouritesOnlyToggle != null && favouritesOnlyToggle.isSelected()) {
|
if (favouritesOnlyToggle != null && favouritesOnlyToggle.isSelected()) {
|
||||||
view = allRecipes.stream()
|
view = favouriteRecipeList.get();
|
||||||
.filter(r -> config.isFavourite(r.getId()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
recipeList.getItems().setAll(view);
|
recipeList.getItems().setAll(view);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (favouritesOnlyToggle != null && !favouritesOnlyToggle.isSelected()) {
|
||||||
|
recipeList.getItems().setAll(server.getRecipes(config.getRecipeLanguages()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException | InterruptedException e) {
|
||||||
|
logger.severe(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// restore selection if possible
|
// restore selection if possible
|
||||||
if (selectedId != null) {
|
if (selectedId != null) {
|
||||||
|
|
@ -452,114 +463,6 @@ 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
|
@FXML
|
||||||
private void openIngredientsPopup() {
|
private void openIngredientsPopup() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -611,6 +514,14 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
updatedBadgeTimer.playFromStart();
|
updatedBadgeTimer.playFromStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void openShoppingListWindow() throws IOException {
|
||||||
|
var root = UI.getFXML().load(ShoppingListCtrl.class, "client", "scenes", "shopping", "ShoppingList.fxml");
|
||||||
|
Stage stage = new Stage();
|
||||||
|
stage.setTitle(this.getLocaleString("menu.shopping.title"));
|
||||||
|
stage.setScene(new Scene(root.getValue()));
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package client.scenes.Ingredient;
|
package client.scenes.Ingredient;
|
||||||
|
|
||||||
|
import client.exception.DuplicateIngredientException;
|
||||||
import client.scenes.nutrition.NutritionDetailsCtrl;
|
import client.scenes.nutrition.NutritionDetailsCtrl;
|
||||||
import client.utils.LocaleAware;
|
import client.utils.LocaleAware;
|
||||||
import client.utils.LocaleManager;
|
import client.utils.LocaleManager;
|
||||||
|
|
@ -66,8 +67,8 @@ public class IngredientListCtrl implements LocaleAware {
|
||||||
return this.localeManager;
|
return this.localeManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@Override
|
||||||
public void initialize() {
|
public void initializeComponents() {
|
||||||
ingredientListView.setCellFactory(list -> new ListCell<>() {
|
ingredientListView.setCellFactory(list -> new ListCell<>() {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(Ingredient item, boolean empty) {
|
protected void updateItem(Ingredient item, boolean empty) {
|
||||||
|
|
@ -113,6 +114,8 @@ public class IngredientListCtrl implements LocaleAware {
|
||||||
refresh(); // reload list from server
|
refresh(); // reload list from server
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
showError("Failed to create ingredient: " + e.getMessage());
|
showError("Failed to create ingredient: " + e.getMessage());
|
||||||
|
} catch (DuplicateIngredientException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package client.scenes;
|
package client.scenes;
|
||||||
|
|
||||||
|
import client.utils.Config;
|
||||||
import client.utils.LocaleAware;
|
import client.utils.LocaleAware;
|
||||||
import client.utils.LocaleManager;
|
import client.utils.LocaleManager;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
@ -46,7 +47,7 @@ public class LangSelectMenuCtrl implements LocaleAware {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initializeComponents() {
|
public void initializeComponents() {
|
||||||
langSelectMenu.getItems().setAll("en", "pl", "nl", "tok", "tr");
|
langSelectMenu.getItems().setAll(Config.languages);
|
||||||
langSelectMenu.setValue(manager.getLocale().getLanguage());
|
langSelectMenu.setValue(manager.getLocale().getLanguage());
|
||||||
langSelectMenu.setConverter(new StringConverter<String>() {
|
langSelectMenu.setConverter(new StringConverter<String>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package client.scenes;
|
package client.scenes;
|
||||||
|
|
||||||
|
import client.utils.Config;
|
||||||
import client.utils.ConfigService;
|
import client.utils.ConfigService;
|
||||||
import client.utils.LocaleAware;
|
import client.utils.LocaleAware;
|
||||||
import client.utils.LocaleManager;
|
import client.utils.LocaleManager;
|
||||||
|
|
@ -54,7 +55,7 @@ public class LanguageFilterCtrl implements LocaleAware {
|
||||||
public void initializeComponents() {
|
public void initializeComponents() {
|
||||||
var items = this.langFilterMenu.getItems();
|
var items = this.langFilterMenu.getItems();
|
||||||
|
|
||||||
final List<String> languages = List.of("en", "nl", "pl", "tok", "tr");
|
final List<String> languages = List.of(Config.languages);
|
||||||
this.selectedLanguages = this.configService.getConfig().getRecipeLanguages();
|
this.selectedLanguages = this.configService.getConfig().getRecipeLanguages();
|
||||||
this.updateMenuButtonDisplay();
|
this.updateMenuButtonDisplay();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,15 @@ import javafx.animation.PauseTransition;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller for the search bar component.
|
* Controller for the search bar component.
|
||||||
|
|
@ -99,7 +103,11 @@ 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, configService.getConfig().getRecipeLanguages());
|
var recipes = serverUtils.getRecipesFiltered(
|
||||||
|
"",
|
||||||
|
configService.getConfig().getRecipeLanguages()
|
||||||
|
);
|
||||||
|
return applyMultiTermAndFilter(recipes, filter);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -146,8 +154,61 @@ public class SearchBarCtrl implements LocaleAware {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.searchField.setOnKeyReleased(event -> {
|
this.searchField.setOnKeyReleased(event -> {
|
||||||
|
if (event.getCode() == KeyCode.ESCAPE) {
|
||||||
|
searchField.clear();
|
||||||
|
this.onSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// This cancels the current debounce timer and restarts it.
|
// This cancels the current debounce timer and restarts it.
|
||||||
this.searchDebounce.playFromStart();
|
this.searchDebounce.playFromStart();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
private List<Recipe> applyMultiTermAndFilter(List<Recipe> recipes, String query) {
|
||||||
|
if (recipes == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (query == null || query.isBlank()) {
|
||||||
|
return recipes;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokens = Arrays.stream(query.toLowerCase(Locale.ROOT).split("[\\s,]+"))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isBlank())
|
||||||
|
.filter(s -> !s.equals("and"))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (tokens.isEmpty()) {
|
||||||
|
return recipes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipes.stream()
|
||||||
|
.filter(r -> {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
if (r.getName() != null) {
|
||||||
|
sb.append(r.getName()).append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.getIngredients() != null) {
|
||||||
|
r.getIngredients().forEach(i -> {
|
||||||
|
if (i != null) {
|
||||||
|
sb.append(i).append(' ');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.getPreparationSteps() != null) {
|
||||||
|
r.getPreparationSteps().forEach(s -> {
|
||||||
|
if (s != null) {
|
||||||
|
sb.append(s).append(' ');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var haystack = sb.toString().toLowerCase(Locale.ROOT);
|
||||||
|
return tokens.stream().allMatch(haystack::contains);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package client.scenes;
|
||||||
|
|
||||||
|
|
||||||
|
import client.utils.ConfigService;
|
||||||
|
import client.utils.server.ServerUtils;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.TextInputDialog;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
|
public class ServerConnectionDialogCtrl {
|
||||||
|
|
||||||
|
private final ConfigService configService;
|
||||||
|
private final ServerUtils serverUtils;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ServerConnectionDialogCtrl(ConfigService configService, ServerUtils serverUtils) {
|
||||||
|
this.configService = configService;
|
||||||
|
this.serverUtils = serverUtils;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return a boolean for if the user got connected to server or
|
||||||
|
*/
|
||||||
|
public boolean promptForURL(){
|
||||||
|
Alert error = new Alert(Alert.AlertType.ERROR); //creates an error alert
|
||||||
|
error.setTitle("Server is Unavailable");
|
||||||
|
error.setHeaderText("Unable to connect to Server");
|
||||||
|
error.setContentText("The server at " + configService.getConfig().getServerUrl()
|
||||||
|
+ " is not available\n\n Would you like to try a different Server URL?");
|
||||||
|
|
||||||
|
error.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
|
||||||
|
Optional<ButtonType> userChoice = error.showAndWait(); //asks if user wants to input server URL
|
||||||
|
|
||||||
|
if(userChoice.isEmpty() || userChoice.get() == ButtonType.NO){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(true){ // Keeps asking the user until either a valid url is provided or the user exits
|
||||||
|
TextInputDialog dialog =
|
||||||
|
new TextInputDialog(configService.getConfig().getServerUrl());
|
||||||
|
dialog.setTitle("Enter new server URL");
|
||||||
|
dialog.setContentText("Server URL:");
|
||||||
|
Optional<String> userRes = dialog.showAndWait();
|
||||||
|
if(userRes.isEmpty()){
|
||||||
|
return false; //user cancelled the operation
|
||||||
|
}
|
||||||
|
String newServer = userRes.get().trim();
|
||||||
|
|
||||||
|
if(newServer.isEmpty()){
|
||||||
|
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||||
|
alert.setTitle("Invalid Input");
|
||||||
|
alert.setHeaderText("Invalid server URL");
|
||||||
|
alert.setContentText("Please enter a valid URL");
|
||||||
|
alert.showAndWait();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
configService.getConfig().setServerUrl(newServer);
|
||||||
|
if(serverUtils.isServerAvailable()){
|
||||||
|
configService.save();
|
||||||
|
Alert success = new Alert(Alert.AlertType.INFORMATION);
|
||||||
|
success.setTitle("Success");
|
||||||
|
success.setHeaderText("Connected to Server");
|
||||||
|
success.setContentText("Successfully connected to the server!");
|
||||||
|
success.showAndWait();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
Alert retry = new Alert(Alert.AlertType.ERROR);
|
||||||
|
retry.setTitle("Failed");
|
||||||
|
retry.setHeaderText("Failed to connect to Server");
|
||||||
|
retry.setContentText("Would you like to try another URL?");
|
||||||
|
retry.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
|
||||||
|
Optional<ButtonType> result = retry.showAndWait();
|
||||||
|
if(result.isEmpty() || result.get() == ButtonType.NO){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
package client.scenes.nutrition;
|
||||||
|
|
||||||
|
import client.utils.LocaleAware;
|
||||||
|
import client.utils.LocaleManager;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import commons.Ingredient;
|
||||||
|
import commons.Recipe;
|
||||||
|
import commons.VagueIngredient;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.chart.PieChart;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class NutritionPieChartCtrl implements LocaleAware {
|
||||||
|
/**
|
||||||
|
* Nutrition info for a recipe or an ingredient.
|
||||||
|
*
|
||||||
|
* @param protein The protein this recipe/ingredient has
|
||||||
|
* @param fat The fat this recipe/ingredient has
|
||||||
|
* @param carbs The carbs this recipe/ingredient has
|
||||||
|
*/
|
||||||
|
private record CompleteNutritionInfo(
|
||||||
|
double protein,
|
||||||
|
double fat,
|
||||||
|
double carbs
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Create a new {@link CompleteNutritionInfo} with zeroed values.
|
||||||
|
*
|
||||||
|
* @return A {@link CompleteNutritionInfo} with zeroed values.
|
||||||
|
*/
|
||||||
|
static CompleteNutritionInfo zero() {
|
||||||
|
return new CompleteNutritionInfo(0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether this instance has all values set to zero.
|
||||||
|
*
|
||||||
|
* @return Whether all values (protein, carbs, fat) are zero.
|
||||||
|
*/
|
||||||
|
public boolean isZero() {
|
||||||
|
return this.protein() == 0.0
|
||||||
|
&& this.carbs() == 0.0
|
||||||
|
&& this.fat() == 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale this object by an amount. Multiplies all values by amount.
|
||||||
|
*
|
||||||
|
* @param amount The amount to scale it by.
|
||||||
|
* @return The newly scaled nutrition info.
|
||||||
|
*/
|
||||||
|
public CompleteNutritionInfo scaled(double amount) {
|
||||||
|
return new CompleteNutritionInfo(
|
||||||
|
this.protein() * amount,
|
||||||
|
this.fat() * amount,
|
||||||
|
this.carbs() * amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add another nutrition info object to this object.
|
||||||
|
*
|
||||||
|
* @param rhs The nutrition info object to add.
|
||||||
|
* @return A new nutrition info object with the sum of both objects' nutrients.
|
||||||
|
*/
|
||||||
|
public CompleteNutritionInfo add(CompleteNutritionInfo rhs) {
|
||||||
|
return new CompleteNutritionInfo(
|
||||||
|
this.protein() + rhs.protein(),
|
||||||
|
this.fat() + rhs.fat(),
|
||||||
|
this.carbs() + rhs.carbs()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final LocaleManager localeManager;
|
||||||
|
private final Logger logger = Logger.getLogger(NutritionPieChartCtrl.class.getName());
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
PieChart pieChart;
|
||||||
|
|
||||||
|
private Recipe recipe;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
NutritionPieChartCtrl(
|
||||||
|
LocaleManager manager
|
||||||
|
) {
|
||||||
|
this.localeManager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data for this pie chart based on the current recipe.
|
||||||
|
* <p>
|
||||||
|
* This accumulates all the nutrients (properly scaled) from all ingredients in this recipe
|
||||||
|
* and returns a summary with data points.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* The list will be of length 3 if any nutrients are added, or 0 if all the nutrients
|
||||||
|
* sum up to be 0.
|
||||||
|
* </p>
|
||||||
|
* @return The data for this pie chart, as a labeled list of data.
|
||||||
|
*/
|
||||||
|
private List<PieChart.Data> getPieChartData() {
|
||||||
|
CompleteNutritionInfo info = this.recipe
|
||||||
|
.getIngredients()
|
||||||
|
.stream()
|
||||||
|
.map(ri -> {
|
||||||
|
Ingredient i = ri.getIngredient();
|
||||||
|
logger.info("Mapping ingredient " + i.toString());
|
||||||
|
|
||||||
|
switch (ri) {
|
||||||
|
case FormalIngredient fi -> {
|
||||||
|
return new CompleteNutritionInfo(
|
||||||
|
i.getProteinPer100g(),
|
||||||
|
i.getFatPer100g(),
|
||||||
|
i.getCarbsPer100g()
|
||||||
|
)
|
||||||
|
.scaled(fi.getBaseAmount());
|
||||||
|
}
|
||||||
|
case VagueIngredient vi -> {
|
||||||
|
return new CompleteNutritionInfo(
|
||||||
|
i.getProteinPer100g(),
|
||||||
|
i.getFatPer100g(),
|
||||||
|
i.getCarbsPer100g()
|
||||||
|
)
|
||||||
|
.scaled(vi.getBaseAmount());
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.reduce(CompleteNutritionInfo::add)
|
||||||
|
.orElseGet(CompleteNutritionInfo::zero);
|
||||||
|
|
||||||
|
this.logger.info( "Updated data: " + info.toString() );
|
||||||
|
|
||||||
|
if (info.isZero()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(
|
||||||
|
new PieChart.Data(
|
||||||
|
this.getLocaleString("menu.nutrition.protein"),
|
||||||
|
info.protein()
|
||||||
|
),
|
||||||
|
new PieChart.Data(
|
||||||
|
this.getLocaleString("menu.nutrition.fat"),
|
||||||
|
info.fat()
|
||||||
|
),
|
||||||
|
new PieChart.Data(
|
||||||
|
this.getLocaleString("menu.nutrition.carbs"),
|
||||||
|
info.carbs()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current recipe to be displayed in the pie chart.
|
||||||
|
* The pie chart will be refreshed.
|
||||||
|
* <p>
|
||||||
|
* The pie chart will disappear if all the nutrients in the recipe
|
||||||
|
* are zero.
|
||||||
|
* </p>
|
||||||
|
* @param recipe The recipe to display.
|
||||||
|
*/
|
||||||
|
public void setRecipe(Recipe recipe) {
|
||||||
|
this.recipe = recipe;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateText() {
|
||||||
|
List<PieChart.Data> data = this.pieChart.getData();
|
||||||
|
if (data.isEmpty()) return;
|
||||||
|
final int EXPECTED_DATA_SIZE = 3;
|
||||||
|
if (data.size() != EXPECTED_DATA_SIZE) return;
|
||||||
|
|
||||||
|
data.get(0).setName(this.getLocaleString("menu.nutrition.protein"));
|
||||||
|
data.get(1).setName(this.getLocaleString("menu.nutrition.fat"));
|
||||||
|
final int TWO = 2;
|
||||||
|
data.get(TWO).setName(this.getLocaleString("menu.nutrition.carbs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocaleManager getLocaleManager() {
|
||||||
|
return this.localeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data in this pie chart.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("checkstyle:MagicNumber")
|
||||||
|
public void refresh() {
|
||||||
|
if (this.recipe == null) {
|
||||||
|
this.pieChart.setVisible(false);
|
||||||
|
logger.info("Refreshing pie chart with no recipe");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Refreshing pie chart with recipe");
|
||||||
|
this.pieChart.setVisible(true);
|
||||||
|
if (this.pieChart.getData().isEmpty()) {
|
||||||
|
this.pieChart.getData().setAll(
|
||||||
|
this.getPieChartData()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
List<PieChart.Data> fresh = this.getPieChartData();
|
||||||
|
List<PieChart.Data> data = this.pieChart.getData();
|
||||||
|
|
||||||
|
if (fresh.isEmpty()) {
|
||||||
|
data.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.get(0).setPieValue(fresh.get(0).getPieValue());
|
||||||
|
data.get(1).setPieValue(fresh.get(1).getPieValue());
|
||||||
|
final int TWO = 2;
|
||||||
|
data.get(TWO).setPieValue(fresh.get(TWO).getPieValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeComponents() {
|
||||||
|
final double START_ANGLE = 60.0;
|
||||||
|
|
||||||
|
this.pieChart.setClockwise(true);
|
||||||
|
this.pieChart.setStartAngle(START_ANGLE);
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import com.google.inject.Inject;
|
||||||
import commons.FormalIngredient;
|
import commons.FormalIngredient;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
|
@ -78,7 +79,13 @@ public class IngredientListCtrl implements LocaleAware {
|
||||||
if (recipe == null) {
|
if (recipe == null) {
|
||||||
this.ingredients = FXCollections.observableArrayList(new ArrayList<>());
|
this.ingredients = FXCollections.observableArrayList(new ArrayList<>());
|
||||||
} else {
|
} else {
|
||||||
List<RecipeIngredient> ingredientList = recipe.getIngredients();
|
List<RecipeIngredient> ingredientList = recipe
|
||||||
|
.getIngredients()
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(ingredient -> ingredient
|
||||||
|
.getIngredient()
|
||||||
|
.getName()))
|
||||||
|
.toList();
|
||||||
this.ingredients = FXCollections.observableArrayList(ingredientList);
|
this.ingredients = FXCollections.observableArrayList(ingredientList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package client.scenes.recipe;
|
package client.scenes.recipe;
|
||||||
|
|
||||||
import client.utils.server.ServerUtils;
|
import client.utils.server.ServerUtils;
|
||||||
|
import client.exception.DuplicateIngredientException;
|
||||||
import commons.Ingredient;
|
import commons.Ingredient;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
|
@ -67,8 +68,15 @@ public class IngredientsPopupCtrl {
|
||||||
server.createIngredient(name); // calls POST /api/ingredients
|
server.createIngredient(name); // calls POST /api/ingredients
|
||||||
refresh(); // reload list from server
|
refresh(); // reload list from server
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
|
|
||||||
showError("Failed to create ingredient: " + e.getMessage());
|
showError("Failed to create ingredient: " + e.getMessage());
|
||||||
|
} catch (DuplicateIngredientException e) {
|
||||||
|
showError("An ingredient with the name " + name + " already exists." +
|
||||||
|
" Please provide a different name."); //checks if error received has the DUPLICATE string and creates a showError instance with an appropriate error message and description
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package client.scenes.recipe;
|
||||||
|
|
||||||
import client.exception.UpdateException;
|
import client.exception.UpdateException;
|
||||||
import client.scenes.FoodpalApplicationCtrl;
|
import client.scenes.FoodpalApplicationCtrl;
|
||||||
|
import client.scenes.nutrition.NutritionPieChartCtrl;
|
||||||
|
import client.service.ShoppingListService;
|
||||||
import client.utils.Config;
|
import client.utils.Config;
|
||||||
import client.utils.ConfigService;
|
import client.utils.ConfigService;
|
||||||
import client.utils.LocaleAware;
|
import client.utils.LocaleAware;
|
||||||
|
|
@ -10,6 +12,7 @@ import client.utils.PrintExportService;
|
||||||
import client.utils.server.ServerUtils;
|
import client.utils.server.ServerUtils;
|
||||||
import client.utils.WebSocketDataService;
|
import client.utils.WebSocketDataService;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import commons.FormalIngredient;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
@ -20,7 +23,10 @@ import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
|
@ -34,6 +40,8 @@ import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.stage.DirectoryChooser;
|
import javafx.stage.DirectoryChooser;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller for the recipe detail view.
|
* Controller for the recipe detail view.
|
||||||
|
|
@ -46,9 +54,12 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
private final FoodpalApplicationCtrl appCtrl;
|
private final FoodpalApplicationCtrl appCtrl;
|
||||||
private final ConfigService configService;
|
private final ConfigService configService;
|
||||||
private final WebSocketDataService<Long, Recipe> webSocketDataService;
|
private final WebSocketDataService<Long, Recipe> webSocketDataService;
|
||||||
|
private final ShoppingListService shoppingListService;
|
||||||
|
|
||||||
public Spinner<Double> scaleSpinner;
|
public Spinner<Double> scaleSpinner;
|
||||||
public Label inferredKcalLabel;
|
public Label inferredKcalLabel;
|
||||||
|
public Spinner<Integer> servingsSpinner;
|
||||||
|
public Label inferredServeSizeLabel;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private IngredientListCtrl ingredientListController;
|
private IngredientListCtrl ingredientListController;
|
||||||
|
|
@ -65,12 +76,14 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
ServerUtils server,
|
ServerUtils server,
|
||||||
FoodpalApplicationCtrl appCtrl,
|
FoodpalApplicationCtrl appCtrl,
|
||||||
ConfigService configService,
|
ConfigService configService,
|
||||||
|
ShoppingListService listService,
|
||||||
WebSocketDataService<Long, Recipe> webSocketDataService) {
|
WebSocketDataService<Long, Recipe> webSocketDataService) {
|
||||||
this.localeManager = localeManager;
|
this.localeManager = localeManager;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.appCtrl = appCtrl;
|
this.appCtrl = appCtrl;
|
||||||
this.configService = configService;
|
this.configService = configService;
|
||||||
this.webSocketDataService = webSocketDataService;
|
this.webSocketDataService = webSocketDataService;
|
||||||
|
this.shoppingListService = listService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
|
@ -94,6 +107,9 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
@FXML
|
@FXML
|
||||||
private ComboBox<String> langSelector;
|
private ComboBox<String> langSelector;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private NutritionPieChartCtrl pieChartController;
|
||||||
|
|
||||||
private ListView<Recipe> getParentRecipeList() {
|
private ListView<Recipe> getParentRecipeList() {
|
||||||
return this.appCtrl.recipeList;
|
return this.appCtrl.recipeList;
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +140,6 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
*
|
*
|
||||||
* @throws IOException Upon invalid recipe response.
|
* @throws IOException Upon invalid recipe response.
|
||||||
* @throws InterruptedException Upon request interruption.
|
* @throws InterruptedException Upon request interruption.
|
||||||
*
|
|
||||||
* @see FoodpalApplicationCtrl#refresh()
|
* @see FoodpalApplicationCtrl#refresh()
|
||||||
*/
|
*/
|
||||||
private void refresh() throws IOException, InterruptedException {
|
private void refresh() throws IOException, InterruptedException {
|
||||||
|
|
@ -157,7 +172,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
|
|
||||||
// If there is a scale
|
// If there is a scale
|
||||||
// Prevents issues from first startup
|
// Prevents issues from first startup
|
||||||
if (scaleSpinner.getValue() != null) {
|
if (scaleSpinner.getValue() != null && servingsSpinner.getValue() != null) {
|
||||||
Double scale = scaleSpinner.getValue();
|
Double scale = scaleSpinner.getValue();
|
||||||
// see impl. creates a scaled context for the recipe such that its non-scaled value is kept as a reference.
|
// see impl. creates a scaled context for the recipe such that its non-scaled value is kept as a reference.
|
||||||
this.recipeView = new ScalableRecipeView(recipe, scale);
|
this.recipeView = new ScalableRecipeView(recipe, scale);
|
||||||
|
|
@ -167,14 +182,21 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
Double.isNaN(this.recipeView.scaledKcalProperty().get()) ?
|
Double.isNaN(this.recipeView.scaledKcalProperty().get()) ?
|
||||||
0.0 : this.recipeView.scaledKcalProperty().get())
|
0.0 : this.recipeView.scaledKcalProperty().get())
|
||||||
, this.recipeView.scaledKcalProperty()));
|
, this.recipeView.scaledKcalProperty()));
|
||||||
|
recipeView.servingsProperty().set(servingsSpinner.getValue());
|
||||||
|
inferredServeSizeLabel.textProperty().bind(Bindings.createStringBinding(
|
||||||
|
() -> String.format("Inferred size per serving: %.1f g", recipeView.servingSizeProperty().get()),
|
||||||
|
recipeView.servingSizeProperty()));
|
||||||
// expose the scaled view to list controllers
|
// expose the scaled view to list controllers
|
||||||
this.ingredientListController.refetchFromRecipe(this.recipeView.getScaled());
|
this.ingredientListController.refetchFromRecipe(this.recipeView.getScaled());
|
||||||
this.stepListController.refetchFromRecipe(this.recipeView.getScaled());
|
this.stepListController.refetchFromRecipe(this.recipeView.getScaled());
|
||||||
|
this.pieChartController.setRecipe(recipe);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ingredientListController.refetchFromRecipe(recipe);
|
this.ingredientListController.refetchFromRecipe(recipe);
|
||||||
this.stepListController.refetchFromRecipe(recipe);
|
this.stepListController.refetchFromRecipe(recipe);
|
||||||
|
|
||||||
|
this.pieChartController.setRecipe(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -325,6 +347,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
PrintExportService.exportToFile(recipeText, dirPath, filename);
|
PrintExportService.exportToFile(recipeText, dirPath, filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the favourite status of the currently viewed recipe in the
|
* Toggles the favourite status of the currently viewed recipe in the
|
||||||
* application configuration and writes the changes to disk.
|
* application configuration and writes the changes to disk.
|
||||||
|
|
@ -402,6 +425,47 @@ public class RecipeDetailCtrl implements LocaleAware {
|
||||||
// triggers a UI update each time the spinner changes to a different value.
|
// triggers a UI update each time the spinner changes to a different value.
|
||||||
setCurrentlyViewedRecipe(recipe);
|
setCurrentlyViewedRecipe(recipe);
|
||||||
});
|
});
|
||||||
langSelector.getItems().addAll("en", "nl", "pl", "tok");
|
servingsSpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, Integer.MAX_VALUE, 1));
|
||||||
|
servingsSpinner.setEditable(true);
|
||||||
|
servingsSpinner.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (newValue == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCurrentlyViewedRecipe(recipe);
|
||||||
|
});
|
||||||
|
langSelector.getItems().addAll(Config.languages);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void handleAddAllToShoppingList(ActionEvent actionEvent) {
|
||||||
|
Recipe ingredientSource = (recipeView != null) ? recipeView.getScaled() : recipe;
|
||||||
|
|
||||||
|
var ingredients = ingredientSource.getIngredients().stream()
|
||||||
|
.map(ri -> {
|
||||||
|
if (ri instanceof FormalIngredient fi) {
|
||||||
|
return fi;
|
||||||
|
}
|
||||||
|
return new FormalIngredient(
|
||||||
|
ri.getIngredient(),
|
||||||
|
1,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
var pair = client.UI.getFXML().load(
|
||||||
|
client.scenes.shopping.AddOverviewCtrl.class,
|
||||||
|
"client", "scenes", "shopping", "AddOverview.fxml"
|
||||||
|
);
|
||||||
|
|
||||||
|
var ctrl = pair.getKey();
|
||||||
|
ctrl.setContext(recipe, ingredients);
|
||||||
|
|
||||||
|
Stage stage = new Stage();
|
||||||
|
stage.setTitle("Add to shopping list");
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.initOwner(((Node) actionEvent.getSource()).getScene().getWindow());
|
||||||
|
stage.setScene(new Scene(pair.getValue()));
|
||||||
|
stage.showAndWait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,19 @@ import commons.Recipe;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.ObjectBinding;
|
import javafx.beans.binding.ObjectBinding;
|
||||||
import javafx.beans.property.DoubleProperty;
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleDoubleProperty;
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
|
||||||
public class ScalableRecipeView {
|
public class ScalableRecipeView {
|
||||||
private final ObjectProperty<Recipe> recipe = new SimpleObjectProperty<>();
|
private final ObjectProperty<Recipe> recipe = new SimpleObjectProperty<>();
|
||||||
private final ObjectProperty<Recipe> scaled = new SimpleObjectProperty<>();
|
private final ObjectProperty<Recipe> scaled = new SimpleObjectProperty<>();
|
||||||
private final DoubleProperty scale = new SimpleDoubleProperty();
|
private final DoubleProperty scale = new SimpleDoubleProperty();
|
||||||
private final SimpleDoubleProperty scaledKcal = new SimpleDoubleProperty();
|
private final DoubleProperty scaledKcal = new SimpleDoubleProperty();
|
||||||
|
private final IntegerProperty servings = new SimpleIntegerProperty();
|
||||||
|
private final DoubleProperty servingSize = new SimpleDoubleProperty();
|
||||||
public ScalableRecipeView(
|
public ScalableRecipeView(
|
||||||
Recipe recipe,
|
Recipe recipe,
|
||||||
Double scale
|
Double scale
|
||||||
|
|
@ -24,10 +28,10 @@ public class ScalableRecipeView {
|
||||||
this.recipe, this.scale);
|
this.recipe, this.scale);
|
||||||
this.scaled.bind(binding);
|
this.scaled.bind(binding);
|
||||||
this.scaledKcal.bind(Bindings.createDoubleBinding(() -> this.scaled.get().kcal(), this.scaled));
|
this.scaledKcal.bind(Bindings.createDoubleBinding(() -> this.scaled.get().kcal(), this.scaled));
|
||||||
}
|
this.servingSize.bind(Bindings.createDoubleBinding(
|
||||||
|
() -> this.scaled.get().weight() * ( 1.0 / this.servings.get()),
|
||||||
public double getScale() {
|
this.servings)
|
||||||
return scale.get();
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Recipe getRecipe() {
|
public Recipe getRecipe() {
|
||||||
|
|
@ -38,23 +42,14 @@ public class ScalableRecipeView {
|
||||||
return scaled.get();
|
return scaled.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getScaledKcal() {
|
public DoubleProperty scaledKcalProperty() {
|
||||||
return scaledKcal.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public DoubleProperty scaleProperty() {
|
|
||||||
return scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<Recipe> scaledProperty() {
|
|
||||||
return scaled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObjectProperty<Recipe> recipeProperty() {
|
|
||||||
return recipe;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SimpleDoubleProperty scaledKcalProperty() {
|
|
||||||
return scaledKcal;
|
return scaledKcal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IntegerProperty servingsProperty() {
|
||||||
|
return servings;
|
||||||
|
}
|
||||||
|
public DoubleProperty servingSizeProperty() {
|
||||||
|
return servingSize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
244
client/src/main/java/client/scenes/shopping/AddOverviewCtrl.java
Normal file
244
client/src/main/java/client/scenes/shopping/AddOverviewCtrl.java
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
package client.scenes.shopping;
|
||||||
|
|
||||||
|
import client.service.ShoppingListService;
|
||||||
|
import client.utils.LocaleAware;
|
||||||
|
import client.utils.LocaleManager;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import commons.Ingredient;
|
||||||
|
import commons.Recipe;
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.TableColumn;
|
||||||
|
import javafx.scene.control.TableView;
|
||||||
|
import javafx.scene.control.TextInputDialog;
|
||||||
|
import javafx.scene.control.cell.ComboBoxTableCell;
|
||||||
|
import javafx.scene.control.cell.TextFieldTableCell;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.util.converter.DoubleStringConverter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class AddOverviewCtrl implements LocaleAware {
|
||||||
|
|
||||||
|
private final ShoppingListService shoppingListService;
|
||||||
|
private final LocaleManager localeManager;
|
||||||
|
|
||||||
|
|
||||||
|
private String sourceRecipeName;
|
||||||
|
|
||||||
|
private final ObservableList<AddOverviewRow> rows = FXCollections.observableArrayList();
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TableView<AddOverviewRow> overviewTable;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TableColumn<AddOverviewRow, String> nameColumn;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TableColumn<AddOverviewRow, Double> amountColumn;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TableColumn<AddOverviewRow, String> unitColumn;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
initializeComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final List<String> DEFAULT_UNITS =
|
||||||
|
List.of("", "g", "kg", "ml", "l", "tbsp");
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public AddOverviewCtrl(ShoppingListService shoppingListService, LocaleManager localeManager) {
|
||||||
|
this.shoppingListService = shoppingListService;
|
||||||
|
this.localeManager = localeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeComponents() {
|
||||||
|
overviewTable.setEditable(true);
|
||||||
|
|
||||||
|
nameColumn.setCellValueFactory(c -> c.getValue().nameProperty());
|
||||||
|
amountColumn.setCellValueFactory(c -> c.getValue().amountProperty().asObject());
|
||||||
|
unitColumn.setCellValueFactory(c -> c.getValue().unitProperty());
|
||||||
|
|
||||||
|
nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||||
|
nameColumn.setOnEditCommit(e -> e.getRowValue().setName(e.getNewValue()));
|
||||||
|
|
||||||
|
amountColumn.setCellFactory(TextFieldTableCell.forTableColumn(new DoubleStringConverter()));
|
||||||
|
amountColumn.setOnEditCommit(e -> e.getRowValue().setAmount(
|
||||||
|
e.getNewValue() == null ? 0.0 : e.getNewValue()
|
||||||
|
));
|
||||||
|
|
||||||
|
var unitOptions = FXCollections.observableArrayList(DEFAULT_UNITS);
|
||||||
|
unitColumn.setCellFactory(ComboBoxTableCell.forTableColumn(unitOptions));
|
||||||
|
unitColumn.setOnEditCommit(e -> e.getRowValue().setUnit(e.getNewValue()));
|
||||||
|
|
||||||
|
overviewTable.setItems(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateText() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocaleManager getLocaleManager() {
|
||||||
|
return localeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContext(Recipe recipe, List<FormalIngredient> ingredients) {
|
||||||
|
this.sourceRecipeName = recipe == null ? null : recipe.getName();
|
||||||
|
|
||||||
|
rows.clear();
|
||||||
|
for (FormalIngredient fi : ingredients) {
|
||||||
|
rows.add(AddOverviewRow.fromFormalIngredient(fi));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void handleAddRow() {
|
||||||
|
TextInputDialog nameDialog = new TextInputDialog();
|
||||||
|
nameDialog.setTitle("Add item");
|
||||||
|
nameDialog.setHeaderText("Add an ingredient");
|
||||||
|
nameDialog.setContentText("Name:");
|
||||||
|
|
||||||
|
Optional<String> nameOpt = nameDialog.showAndWait();
|
||||||
|
if (nameOpt.isEmpty() || nameOpt.get().isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInputDialog amountDialog = new TextInputDialog("0");
|
||||||
|
amountDialog.setTitle("Add item");
|
||||||
|
amountDialog.setHeaderText("Amount");
|
||||||
|
amountDialog.setContentText("Amount (number):");
|
||||||
|
|
||||||
|
double amount = 0.0;
|
||||||
|
Optional<String> amountOpt = amountDialog.showAndWait();
|
||||||
|
if (amountOpt.isPresent()) {
|
||||||
|
try {
|
||||||
|
amount = Double.parseDouble(amountOpt.get().trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
amount = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.add(AddOverviewRow.arbitrary(nameOpt.get().trim(), amount, ""));
|
||||||
|
overviewTable.getSelectionModel().selectLast();
|
||||||
|
overviewTable.scrollTo(rows.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void handleRemoveSelected() {
|
||||||
|
AddOverviewRow selected = overviewTable.getSelectionModel().getSelectedItem();
|
||||||
|
if (selected == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.remove(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void handleConfirm() {
|
||||||
|
for (AddOverviewRow row : rows) {
|
||||||
|
FormalIngredient fi = row.toFormalIngredient();
|
||||||
|
if (sourceRecipeName == null || sourceRecipeName.isBlank()) {
|
||||||
|
shoppingListService.putIngredient(fi);
|
||||||
|
} else {
|
||||||
|
shoppingListService.putIngredient(fi, sourceRecipeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void handleCancel() {
|
||||||
|
closeWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeWindow() {
|
||||||
|
Stage stage = (Stage) overviewTable.getScene().getWindow();
|
||||||
|
stage.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AddOverviewRow {
|
||||||
|
private Ingredient backingIngredient;
|
||||||
|
|
||||||
|
private final StringProperty name = new SimpleStringProperty("");
|
||||||
|
private final DoubleProperty amount = new SimpleDoubleProperty(0.0);
|
||||||
|
private final StringProperty unit = new SimpleStringProperty("");
|
||||||
|
|
||||||
|
public static AddOverviewRow fromFormalIngredient(FormalIngredient fi) {
|
||||||
|
AddOverviewRow r = new AddOverviewRow();
|
||||||
|
r.backingIngredient = fi.getIngredient();
|
||||||
|
r.name.set(fi.getIngredient().getName());
|
||||||
|
r.amount.set(fi.getAmount());
|
||||||
|
r.unit.set(fi.getUnitSuffix() == null ? "" : fi.getUnitSuffix());
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AddOverviewRow arbitrary(String name, double amount, String unit) {
|
||||||
|
AddOverviewRow r = new AddOverviewRow();
|
||||||
|
r.backingIngredient = null;
|
||||||
|
r.name.set(name == null ? "" : name);
|
||||||
|
r.amount.set(amount);
|
||||||
|
r.unit.set(unit == null ? "" : unit);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FormalIngredient toFormalIngredient() {
|
||||||
|
Ingredient ing = backingIngredient;
|
||||||
|
|
||||||
|
if (ing == null) {
|
||||||
|
ing = new Ingredient(getName(), 0.0, 0.0, 0.0);
|
||||||
|
} else {
|
||||||
|
ing.setName(getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FormalIngredient(ing, getAmount(), getUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty nameProperty() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DoubleProperty amountProperty() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty unitProperty() {
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name.set(name == null ? "" : name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAmount() {
|
||||||
|
return amount.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(double amount) {
|
||||||
|
this.amount.set(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUnit() {
|
||||||
|
return unit.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnit(String unit) {
|
||||||
|
this.unit.set(unit == null ? "" : unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package client.scenes.shopping;
|
||||||
|
|
||||||
|
import client.scenes.recipe.OrderedEditableListCell;
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Spinner;
|
||||||
|
import javafx.scene.control.SpinnerValueFactory;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.util.Pair;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class ShoppingListCell extends OrderedEditableListCell<Pair<FormalIngredient, Optional<String>>> {
|
||||||
|
private Node makeEditor() {
|
||||||
|
HBox editor = new HBox();
|
||||||
|
Spinner<Double> amountInput = new Spinner<>();
|
||||||
|
FormalIngredient ingredient = getItem().getKey();
|
||||||
|
amountInput.setEditable(true);
|
||||||
|
amountInput.setValueFactory(new SpinnerValueFactory.DoubleSpinnerValueFactory(0, Double.MAX_VALUE, ingredient.getAmount()));
|
||||||
|
Label textLabel = new Label(getItem().getKey().getUnitSuffix() + " of " + getItem().getKey().getIngredient().getName());
|
||||||
|
editor.getChildren().addAll(amountInput, textLabel);
|
||||||
|
editor.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
|
||||||
|
if (e.getCode() != KeyCode.ENTER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Pair<FormalIngredient, Optional<String>> pair = getItem();
|
||||||
|
pair.getKey().setAmount(amountInput.getValue());
|
||||||
|
commitEdit(pair);
|
||||||
|
});
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void startEdit() {
|
||||||
|
super.startEdit();
|
||||||
|
this.setText("");
|
||||||
|
this.setGraphic(makeEditor());
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
protected void updateItem(Pair<FormalIngredient, Optional<String>> item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
|
if (empty) {
|
||||||
|
this.setText("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String display = item.getKey().toString() +
|
||||||
|
item.getValue().map(recipe -> {
|
||||||
|
return " (" + recipe + ")";
|
||||||
|
}).orElse("");
|
||||||
|
|
||||||
|
this.setText(display);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void cancelEdit() {
|
||||||
|
super.cancelEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commitEdit(Pair<FormalIngredient, Optional<String>> newValue) {
|
||||||
|
super.commitEdit(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
package client.scenes.shopping;
|
||||||
|
|
||||||
|
import client.UI;
|
||||||
|
import client.service.ShoppingListService;
|
||||||
|
import client.utils.LocaleAware;
|
||||||
|
import client.utils.LocaleManager;
|
||||||
|
import client.utils.PrintExportService;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.util.Pair;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class ShoppingListCtrl implements LocaleAware {
|
||||||
|
ShoppingListService shopping;
|
||||||
|
|
||||||
|
LocaleManager localeManager;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<Pair<FormalIngredient, Optional<String>>> shoppingListView;
|
||||||
|
@Inject
|
||||||
|
public ShoppingListCtrl(
|
||||||
|
ShoppingListService shopping,
|
||||||
|
LocaleManager localeManager
|
||||||
|
) {
|
||||||
|
this.shopping = shopping;
|
||||||
|
this.localeManager = localeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateText() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocaleManager getLocaleManager() {
|
||||||
|
return this.localeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeComponents() {
|
||||||
|
this.shoppingListView.setEditable(true);
|
||||||
|
this.shoppingListView.setCellFactory(l -> new ShoppingListCell());
|
||||||
|
this.shoppingListView.setItems(this.shopping.getViewModel().getListItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshList() {
|
||||||
|
this.shoppingListView.getItems().setAll(
|
||||||
|
this.shopping.getItems()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleAddItem(ActionEvent actionEvent) {
|
||||||
|
Stage stage = new Stage();
|
||||||
|
Pair<ShoppingListNewItemPromptCtrl, Parent> root = UI.getFXML().load(ShoppingListNewItemPromptCtrl.class,
|
||||||
|
"client", "scenes", "shopping", "ShoppingListItemAddModal.fxml");
|
||||||
|
root.getKey().setNewValueConsumer(fi -> {
|
||||||
|
this.shopping.putIngredient(fi);
|
||||||
|
|
||||||
|
});
|
||||||
|
stage.setScene(new Scene(root.getValue()));
|
||||||
|
stage.setTitle("My modal window");
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.initOwner(
|
||||||
|
((Node)actionEvent.getSource()).getScene().getWindow() );
|
||||||
|
stage.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleRemoveItem(ActionEvent actionEvent) {
|
||||||
|
var x = this.shoppingListView.getSelectionModel().getSelectedItem();
|
||||||
|
this.shopping.getItems().remove(x);
|
||||||
|
refreshList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleReset(ActionEvent actionEvent) {
|
||||||
|
shopping.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handlePrint(ActionEvent actionEvent) {
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle("Save Shopping List");
|
||||||
|
fileChooser.getExtensionFilters().add(
|
||||||
|
new FileChooser.ExtensionFilter("Text Files", "*.txt")
|
||||||
|
);
|
||||||
|
fileChooser.setInitialFileName("shopping-list.txt");
|
||||||
|
|
||||||
|
File file = fileChooser.showSaveDialog(
|
||||||
|
((Node) actionEvent.getSource()).getScene().getWindow()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PrintExportService.exportToFile(
|
||||||
|
shopping.makePrintable(),
|
||||||
|
file.getParentFile().toPath(),
|
||||||
|
file.getName()
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||||
|
alert.setTitle("Error");
|
||||||
|
alert.setHeaderText("Failed to save shopping list");
|
||||||
|
alert.setContentText(e.getMessage());
|
||||||
|
alert.showAndWait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
package client.scenes.shopping;
|
||||||
|
|
||||||
|
import client.utils.LocaleAware;
|
||||||
|
import client.utils.LocaleManager;
|
||||||
|
import client.utils.server.ServerUtils;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import commons.Ingredient;
|
||||||
|
import commons.Unit;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.scene.control.MenuButton;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.Spinner;
|
||||||
|
import javafx.scene.control.SpinnerValueFactory;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public class ShoppingListNewItemPromptCtrl implements LocaleAware {
|
||||||
|
public MenuButton ingredientSelection;
|
||||||
|
private final ObjectProperty<Ingredient> selected = new SimpleObjectProperty<>();
|
||||||
|
private final ObjectProperty<Unit> selectedUnit = new SimpleObjectProperty<>();
|
||||||
|
private final ServerUtils server;
|
||||||
|
private final LocaleManager localeManager;
|
||||||
|
private Consumer<FormalIngredient> newValueConsumer;
|
||||||
|
public MenuButton unitSelect;
|
||||||
|
public Spinner<Double> amountSelect;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ShoppingListNewItemPromptCtrl(ServerUtils server, LocaleManager localeManager) {
|
||||||
|
this.server = server;
|
||||||
|
this.localeManager = localeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNewValueConsumer(Consumer<FormalIngredient> consumer) {
|
||||||
|
this.newValueConsumer = consumer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void confirmAdd(ActionEvent actionEvent) {
|
||||||
|
if (selected.get() == null || selectedUnit.get() == null) {
|
||||||
|
System.err.println("You must select both an ingredient and an unit");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FormalIngredient fi = new FormalIngredient(selected.get(), amountSelect.getValue(), selectedUnit.get().suffix);
|
||||||
|
newValueConsumer.accept(fi);
|
||||||
|
Stage stage = (Stage) ingredientSelection.getScene().getWindow();
|
||||||
|
stage.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelAdd(ActionEvent actionEvent) {
|
||||||
|
}
|
||||||
|
private <T> void makeMenuItems(
|
||||||
|
MenuButton menu,
|
||||||
|
Iterable<T> items,
|
||||||
|
Function<T, String> labelMapper,
|
||||||
|
Consumer<T> onSelect) {
|
||||||
|
// Iterates over the list of items and applies the label and onSelect handlers.
|
||||||
|
for (T item : items) {
|
||||||
|
MenuItem mi = new MenuItem();
|
||||||
|
mi.setText(labelMapper.apply(item));
|
||||||
|
mi.setOnAction(_ -> {
|
||||||
|
menu.setText(labelMapper.apply(item));
|
||||||
|
onSelect.accept(item);
|
||||||
|
});
|
||||||
|
menu.getItems().add(mi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void updateText() {
|
||||||
|
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void initializeComponents() {
|
||||||
|
try {
|
||||||
|
amountSelect.setValueFactory(
|
||||||
|
new SpinnerValueFactory.DoubleSpinnerValueFactory(0, Double.MAX_VALUE, 0));
|
||||||
|
amountSelect.setEditable(true);
|
||||||
|
makeMenuItems(ingredientSelection, server.getIngredients(), Ingredient::getName, selected::set);
|
||||||
|
makeMenuItems(unitSelect,
|
||||||
|
Arrays.stream(Unit.values()).filter(u -> u.formal).toList(),
|
||||||
|
Unit::toString, selectedUnit::set);
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
System.err.println(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocaleManager getLocaleManager() {
|
||||||
|
return localeManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package client.service;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import commons.Recipe;
|
||||||
|
import javafx.util.Pair;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class NonFunctionalShoppingListService extends ShoppingListService {
|
||||||
|
@Inject
|
||||||
|
public NonFunctionalShoppingListService(ShoppingListViewModel viewModel) {
|
||||||
|
super(viewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putIngredient(FormalIngredient ingredient) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putIngredient(FormalIngredient ingredient, Recipe recipe) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putIngredient(FormalIngredient ingredient, String recipeName) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putArbitraryItem(String name) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FormalIngredient purgeIngredient(Long id) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FormalIngredient purgeIngredient(String ingredientName) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Pair<FormalIngredient, Optional<String>>> getItems() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String makePrintable() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package client.service;
|
||||||
|
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
public class ShoppingListItem {
|
||||||
|
private FormalIngredient i;
|
||||||
|
}
|
||||||
38
client/src/main/java/client/service/ShoppingListService.java
Normal file
38
client/src/main/java/client/service/ShoppingListService.java
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package client.service;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import commons.Recipe;
|
||||||
|
import javafx.util.Pair;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public abstract class ShoppingListService {
|
||||||
|
private ShoppingListViewModel viewModel;
|
||||||
|
@Inject
|
||||||
|
public ShoppingListService(ShoppingListViewModel viewModel) {
|
||||||
|
this.viewModel = viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShoppingListViewModel getViewModel() {
|
||||||
|
return viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setViewModel(ShoppingListViewModel viewModel) {
|
||||||
|
this.viewModel = viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void putIngredient(FormalIngredient ingredient);
|
||||||
|
public abstract void putIngredient(FormalIngredient ingredient, Recipe recipe);
|
||||||
|
public abstract void putIngredient(FormalIngredient ingredient, String recipeName);
|
||||||
|
public abstract void putArbitraryItem(String name);
|
||||||
|
|
||||||
|
public abstract FormalIngredient purgeIngredient(Long id);
|
||||||
|
public abstract FormalIngredient purgeIngredient(String ingredientName);
|
||||||
|
|
||||||
|
public abstract void reset();
|
||||||
|
|
||||||
|
public abstract List<Pair<FormalIngredient, Optional<String>>> getItems();
|
||||||
|
public abstract String makePrintable();
|
||||||
|
}
|
||||||
124
client/src/main/java/client/service/ShoppingListServiceImpl.java
Normal file
124
client/src/main/java/client/service/ShoppingListServiceImpl.java
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
package client.service;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import commons.Recipe;
|
||||||
|
import javafx.util.Pair;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class ShoppingListServiceImpl extends ShoppingListService {
|
||||||
|
private final Logger logger = Logger.getLogger(ShoppingListServiceImpl.class.getName());
|
||||||
|
@Inject
|
||||||
|
public ShoppingListServiceImpl(
|
||||||
|
ShoppingListViewModel model
|
||||||
|
) {
|
||||||
|
super(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putIngredient(FormalIngredient ingredient) {
|
||||||
|
getViewModel().getListItems().add(new Pair<>(ingredient, Optional.empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putIngredient(FormalIngredient ingredient, Recipe recipe) {
|
||||||
|
Pair<FormalIngredient, Optional<String>> val = new Pair<>(ingredient, Optional.of(recipe.getName()));
|
||||||
|
logger.info("putting ingredients into shopping list: " + val);
|
||||||
|
getViewModel().getListItems().add(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putIngredient(FormalIngredient ingredient, String recipeName) {
|
||||||
|
getViewModel().getListItems().add(new Pair<>(ingredient, Optional.of(recipeName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putArbitraryItem(String name) {
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ingredient = new commons.Ingredient(name.trim(), 0.0, 0.0, 0.0);
|
||||||
|
var fi = new commons.FormalIngredient(ingredient, 0.0, "");
|
||||||
|
getViewModel().getListItems().add(new Pair<>(fi, Optional.empty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FormalIngredient purgeIngredient(Long id) {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var item : getViewModel().getListItems()) {
|
||||||
|
FormalIngredient fi = item.getKey();
|
||||||
|
if (fi != null && fi.getId() != null && fi.getId().equals(id)) {
|
||||||
|
getViewModel().getListItems().remove(item);
|
||||||
|
return fi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FormalIngredient purgeIngredient(String ingredientName) {
|
||||||
|
if (ingredientName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var item : getViewModel().getListItems()) {
|
||||||
|
FormalIngredient fi = item.getKey();
|
||||||
|
if (fi != null
|
||||||
|
&& fi.getIngredient() != null
|
||||||
|
&& ingredientName.equals(fi.getIngredient().getName())) {
|
||||||
|
getViewModel().getListItems().remove(item);
|
||||||
|
return fi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
getViewModel().getListItems().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Pair<FormalIngredient, Optional<String>>> getItems() {
|
||||||
|
return getViewModel().getListItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String makePrintable() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (var item : getViewModel().getListItems()) {
|
||||||
|
FormalIngredient ingredient = item.getKey();
|
||||||
|
Optional<String> source = item.getValue();
|
||||||
|
|
||||||
|
if (ingredient == null || ingredient.getIngredient() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(ingredient.getIngredient().getName());
|
||||||
|
|
||||||
|
if (ingredient.getAmount() > 0) {
|
||||||
|
sb.append(" - ")
|
||||||
|
.append(ingredient.getAmount());
|
||||||
|
|
||||||
|
if (ingredient.getUnitSuffix() != null && !ingredient.getUnitSuffix().isBlank()) {
|
||||||
|
sb.append(ingredient.getUnitSuffix());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source.ifPresent(recipe ->
|
||||||
|
sb.append(" (").append(recipe).append(")")
|
||||||
|
);
|
||||||
|
|
||||||
|
sb.append(System.lineSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package client.service;
|
||||||
|
|
||||||
|
import commons.FormalIngredient;
|
||||||
|
import javafx.beans.property.ListProperty;
|
||||||
|
import javafx.beans.property.SimpleListProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.util.Pair;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class ShoppingListViewModel {
|
||||||
|
/**
|
||||||
|
* The formal ingredient provides the ingredient and its amount,
|
||||||
|
* and the string (optional) describes the recipe where it came from.
|
||||||
|
*/
|
||||||
|
private final ListProperty<Pair<FormalIngredient, Optional<String>>> listItems = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||||
|
public void addArbitrary() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableList<Pair<FormalIngredient, Optional<String>>> getListItems() {
|
||||||
|
return listItems.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import java.util.List;
|
||||||
|
|
||||||
public class Config {
|
public class Config {
|
||||||
private String language = "en";
|
private String language = "en";
|
||||||
|
public static String[] languages = {"en", "nl", "pl", "tok", "zhc", "zht"};
|
||||||
private List<String> recipeLanguages = new ArrayList<>();
|
private List<String> recipeLanguages = new ArrayList<>();
|
||||||
private String serverUrl = "http://localhost:8080";
|
private String serverUrl = "http://localhost:8080";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import client.utils.ConfigService;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Endpoints {
|
public class Endpoints {
|
||||||
|
|
@ -81,9 +83,23 @@ public class Endpoints {
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpRequest.Builder getRecipesWith(String params) {
|
public HttpRequest.Builder getRecipesWith(String params) {
|
||||||
|
if (params != null && params.contains("search=")) {
|
||||||
|
int start = params.indexOf("search=") + "search=".length();
|
||||||
|
int end = params.indexOf('&', start);
|
||||||
|
if (end == -1) {
|
||||||
|
end = params.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
String rawValue = params.substring(start, end);
|
||||||
|
String encodedValue = URLEncoder.encode(rawValue, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
params = params.substring(0, start) + encodedValue + params.substring(end);
|
||||||
|
}
|
||||||
|
|
||||||
return this.http(this.createApiUrl("/recipes?" + params)).GET();
|
return this.http(this.createApiUrl("/recipes?" + params)).GET();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public HttpRequest.Builder createIngredient(HttpRequest.BodyPublisher body) {
|
public HttpRequest.Builder createIngredient(HttpRequest.BodyPublisher body) {
|
||||||
String url = this.createApiUrl("/ingredients");
|
String url = this.createApiUrl("/ingredients");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
package client.utils.server;
|
package client.utils.server;
|
||||||
|
|
||||||
import client.utils.ConfigService;
|
import client.utils.ConfigService;
|
||||||
|
import client.exception.DuplicateIngredientException;
|
||||||
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.Ingredient;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
import commons.RecipeIngredient;
|
import commons.RecipeIngredient;
|
||||||
import jakarta.ws.rs.ProcessingException;
|
|
||||||
import jakarta.ws.rs.client.ClientBuilder;
|
import jakarta.ws.rs.client.ClientBuilder;
|
||||||
import org.glassfish.jersey.client.ClientConfig;
|
import org.glassfish.jersey.client.ClientConfig;
|
||||||
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ConnectException;
|
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
|
@ -171,10 +170,9 @@ public class ServerUtils {
|
||||||
.target(this.endpoints.baseUrl()) //
|
.target(this.endpoints.baseUrl()) //
|
||||||
.request(APPLICATION_JSON) //
|
.request(APPLICATION_JSON) //
|
||||||
.get();
|
.get();
|
||||||
} catch (ProcessingException e) {
|
|
||||||
if (e.getCause() instanceof ConnectException) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
catch(Exception e){
|
||||||
|
return false; //any exception caught will return false, not just processing exception.
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -270,7 +268,7 @@ public class ServerUtils {
|
||||||
|
|
||||||
//creates new ingredients in the ingredient list
|
//creates new ingredients in the ingredient list
|
||||||
|
|
||||||
public Ingredient createIngredient(String name) throws IOException, InterruptedException {
|
public Ingredient createIngredient(String name) throws IOException, InterruptedException, DuplicateIngredientException {
|
||||||
Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0);
|
Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0);
|
||||||
String json = objectMapper.writeValueAsString(ingredient);
|
String json = objectMapper.writeValueAsString(ingredient);
|
||||||
|
|
||||||
|
|
@ -278,6 +276,11 @@ public class ServerUtils {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
final int DUPLICATE_STATUS_CODE = 409;
|
||||||
|
if (response.statusCode() == DUPLICATE_STATUS_CODE) {
|
||||||
|
throw new DuplicateIngredientException(name);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.statusCode() != statusOK) {
|
if (response.statusCode() != statusOK) {
|
||||||
throw new IOException("Failed to create ingredient. Server responds with: " + response.body());
|
throw new IOException("Failed to create ingredient. Server responds with: " + response.body());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@
|
||||||
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" />
|
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" />
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
||||||
|
<Button fx:id="shoppingListButton"
|
||||||
|
onAction="#openShoppingListWindow"
|
||||||
|
text="Shopping List" />
|
||||||
<Button fx:id="manageIngredientsButton"
|
<Button fx:id="manageIngredientsButton"
|
||||||
onAction="#openIngredientsPopup"
|
onAction="#openIngredientsPopup"
|
||||||
text="Ingredients..." />
|
text="Ingredients..." />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.chart.PieChart?>
|
||||||
|
|
||||||
|
|
||||||
|
<PieChart fx:controller="client.scenes.nutrition.NutritionPieChartCtrl" fx:id="pieChart" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" />
|
||||||
|
|
@ -25,11 +25,16 @@
|
||||||
<Button fx:id="removeRecipeButton" mnemonicParsing="false" onAction="#removeSelectedRecipe" text="Remove Recipe" />
|
<Button fx:id="removeRecipeButton" mnemonicParsing="false" onAction="#removeSelectedRecipe" text="Remove Recipe" />
|
||||||
<Button fx:id="printRecipeButton" mnemonicParsing="false" onAction="#printRecipe" text="Print Recipe" />
|
<Button fx:id="printRecipeButton" mnemonicParsing="false" onAction="#printRecipe" text="Print Recipe" />
|
||||||
<Button fx:id="favouriteButton" onAction="#toggleFavourite" text="☆" />
|
<Button fx:id="favouriteButton" onAction="#toggleFavourite" text="☆" />
|
||||||
<Label>Scale: </Label>
|
<Button onAction="#handleAddAllToShoppingList">Shop</Button>
|
||||||
<Spinner fx:id="scaleSpinner" />
|
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
||||||
|
<HBox>
|
||||||
<ComboBox fx:id="langSelector" onAction="#changeLanguage" />
|
<ComboBox fx:id="langSelector" onAction="#changeLanguage" />
|
||||||
|
<Label>Scale: </Label>
|
||||||
|
<Spinner fx:id="scaleSpinner" />
|
||||||
|
<Label>Servings: </Label>
|
||||||
|
<Spinner fx:id="servingsSpinner" />
|
||||||
|
</HBox>
|
||||||
|
|
||||||
<!-- Ingredients -->
|
<!-- Ingredients -->
|
||||||
<fx:include source="RecipeIngredientList.fxml" fx:id="ingredientList" />
|
<fx:include source="RecipeIngredientList.fxml" fx:id="ingredientList" />
|
||||||
|
|
@ -37,5 +42,11 @@
|
||||||
<!-- Preparation -->
|
<!-- Preparation -->
|
||||||
<fx:include source="RecipeStepList.fxml" fx:id="stepList"
|
<fx:include source="RecipeStepList.fxml" fx:id="stepList"
|
||||||
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
|
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
|
||||||
|
<HBox spacing="20">
|
||||||
|
<VBox spacing="0">
|
||||||
|
<Label fx:id="inferredServeSizeLabel" />
|
||||||
<Label fx:id="inferredKcalLabel" />
|
<Label fx:id="inferredKcalLabel" />
|
||||||
|
</VBox>
|
||||||
|
<fx:include fx:id="pieChart" source="../nutrition/NutritionPieChart.fxml" />
|
||||||
|
</HBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
|
<AnchorPane xmlns="http://javafx.com/javafx/25"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:controller="client.scenes.shopping.AddOverviewCtrl"
|
||||||
|
prefHeight="420.0" prefWidth="720.0">
|
||||||
|
|
||||||
|
<VBox spacing="10.0" AnchorPane.topAnchor="10.0" AnchorPane.leftAnchor="10.0"
|
||||||
|
AnchorPane.rightAnchor="10.0" AnchorPane.bottomAnchor="10.0">
|
||||||
|
|
||||||
|
<Label text="Review ingredients before adding" />
|
||||||
|
|
||||||
|
<TableView fx:id="overviewTable" editable="true" VBox.vgrow="ALWAYS">
|
||||||
|
<columns>
|
||||||
|
<TableColumn fx:id="nameColumn" text="Ingredient" prefWidth="360.0"/>
|
||||||
|
<TableColumn fx:id="amountColumn" text="Amount" prefWidth="140.0"/>
|
||||||
|
<TableColumn fx:id="unitColumn" text="Unit" prefWidth="140.0"/>
|
||||||
|
</columns>
|
||||||
|
</TableView>
|
||||||
|
|
||||||
|
<HBox spacing="10.0">
|
||||||
|
<Button text="Add item" onAction="#handleAddRow"/>
|
||||||
|
<Button text="Remove selected" onAction="#handleRemoveSelected"/>
|
||||||
|
<Pane HBox.hgrow="ALWAYS"/>
|
||||||
|
<Button text="Cancel" onAction="#handleCancel"/>
|
||||||
|
<Button text="Confirm & Add" onAction="#handleConfirm"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</AnchorPane>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.ListView?>
|
||||||
|
<?import javafx.scene.control.TitledPane?>
|
||||||
|
<?import javafx.scene.layout.AnchorPane?>
|
||||||
|
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<TitledPane animated="false" collapsible="false" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity"
|
||||||
|
minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" text="Shopping List"
|
||||||
|
fx:controller="client.scenes.shopping.ShoppingListCtrl"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/25">
|
||||||
|
<VBox>
|
||||||
|
<ListView fx:id="shoppingListView" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308"
|
||||||
|
AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0"
|
||||||
|
AnchorPane.topAnchor="0.0"/>
|
||||||
|
<HBox>
|
||||||
|
<Button onAction="#handleAddItem">Add</Button>
|
||||||
|
<Button onAction="#handleRemoveItem">Delete</Button>
|
||||||
|
<Button onAction="#handlePrint">Print</Button>
|
||||||
|
<Button onAction="#handleReset">Reset</Button>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</TitledPane>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.*?>
|
||||||
|
<?import java.util.*?>
|
||||||
|
<?import javafx.scene.*?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
|
<AnchorPane xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="client.scenes.shopping.ShoppingListNewItemPromptCtrl"
|
||||||
|
prefHeight="400.0" prefWidth="600.0">
|
||||||
|
<VBox>
|
||||||
|
<HBox>
|
||||||
|
<Spinner fx:id="amountSelect" />
|
||||||
|
<MenuButton fx:id="unitSelect">Unit...</MenuButton>
|
||||||
|
<MenuButton fx:id="ingredientSelection">Your ingredient...</MenuButton>
|
||||||
|
</HBox>
|
||||||
|
<HBox>
|
||||||
|
<Button onAction="#confirmAdd">Confirm</Button>
|
||||||
|
<Button onAction="#cancelAdd">Cancel</Button>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</AnchorPane>
|
||||||
BIN
client/src/main/resources/flag_zhc.png
Normal file
BIN
client/src/main/resources/flag_zhc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
client/src/main/resources/flag_zht.png
Normal file
BIN
client/src/main/resources/flag_zht.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -38,8 +38,14 @@ menu.search=Search...
|
||||||
|
|
||||||
menu.label.selected-langs=Languages
|
menu.label.selected-langs=Languages
|
||||||
|
|
||||||
|
menu.nutrition.carbs=Carbohydrates
|
||||||
|
menu.nutrition.protein=Protein
|
||||||
|
menu.nutrition.fat=Fat
|
||||||
|
|
||||||
lang.en.display=English
|
lang.en.display=English
|
||||||
lang.nl.display=Dutch
|
lang.nl.display=Dutch
|
||||||
lang.pl.display=Polish
|
lang.pl.display=Polish
|
||||||
lang.tok.display=toki pona
|
lang.tok.display=toki pona
|
||||||
lang.tr.display=Turkish
|
lang.tr.display=T\u00FCrk\u00E7e
|
||||||
|
lang.zht.display=中文(台灣)
|
||||||
|
lang.zhc.display=中文(中国大陆)
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,16 @@ menu.search=Search...
|
||||||
|
|
||||||
menu.label.selected-langs=Languages
|
menu.label.selected-langs=Languages
|
||||||
|
|
||||||
|
menu.shopping.title=Shopping list
|
||||||
|
|
||||||
|
menu.nutrition.carbs=Carbohydrates
|
||||||
|
menu.nutrition.protein=Protein
|
||||||
|
menu.nutrition.fat=Fat
|
||||||
|
|
||||||
lang.en.display=English
|
lang.en.display=English
|
||||||
lang.nl.display=Dutch
|
lang.nl.display=Dutch
|
||||||
lang.pl.display=Polish
|
lang.pl.display=Polish
|
||||||
lang.tok.display=toki pona
|
lang.tok.display=toki pona
|
||||||
lang.tr.display=Turkish
|
lang.tr.display=T\u00FCrk\u00E7e
|
||||||
|
lang.zht.display=中文(台灣)
|
||||||
|
lang.zhc.display=中文(中国大陆)
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,17 @@ menu.button.close=Sluiten
|
||||||
|
|
||||||
menu.label.selected-langs=Talen
|
menu.label.selected-langs=Talen
|
||||||
|
|
||||||
|
menu.shopping.title=Boodschappenlijst
|
||||||
|
|
||||||
|
menu.nutrition.carbs=Koolhydraten
|
||||||
|
menu.nutrition.protein=Eiwitten
|
||||||
|
menu.nutrition.fat=Vetten
|
||||||
|
|
||||||
menu.search=Zoeken...
|
menu.search=Zoeken...
|
||||||
lang.en.display=Engels
|
lang.en.display=Engels
|
||||||
lang.nl.display=Nederlands
|
lang.nl.display=Nederlands
|
||||||
lang.pl.display=Pools
|
lang.pl.display=Pools
|
||||||
lang.tok.display=toki pona
|
lang.tok.display=toki pona
|
||||||
lang.tr.display=Turks
|
lang.tr.display=T\u00FCrk\u00E7e
|
||||||
|
lang.zht.display=中文(台灣)
|
||||||
|
lang.zhc.display=中文(中国大陆)
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,16 @@ menu.search=Szukaj...
|
||||||
|
|
||||||
menu.label.selected-langs=Języki
|
menu.label.selected-langs=Języki
|
||||||
|
|
||||||
|
menu.shopping.title=Lista zakup闚
|
||||||
|
|
||||||
|
menu.nutrition.carbs=W?glowodany
|
||||||
|
menu.nutrition.protein=Bia?ka
|
||||||
|
menu.nutrition.fat=T?uszcze
|
||||||
|
|
||||||
lang.en.display=Inglisz
|
lang.en.display=Inglisz
|
||||||
lang.nl.display=Holenderski
|
lang.nl.display=Holenderski
|
||||||
lang.pl.display=Polski
|
lang.pl.display=Polski
|
||||||
lang.tok.display=toki pona
|
lang.tok.display=toki pona
|
||||||
lang.tr.display=Turecki
|
lang.tr.display=T\u00FCrk\u00E7e
|
||||||
|
lang.zht.display=銝剜<EFBFBD>嚗<EFBFBD>蝱<EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
lang.zhc.display=銝剜<EFBFBD>嚗<EFBFBD>葉<EFBFBD>賢之<EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,15 @@ menu.search=o alasa
|
||||||
|
|
||||||
menu.label.selected-langs=toki wile
|
menu.label.selected-langs=toki wile
|
||||||
|
|
||||||
|
menu.shopping.title=ijo wile mani mute
|
||||||
|
|
||||||
|
menu.nutrition.carbs=kipisi moku suwi
|
||||||
|
menu.nutrition.protein=kipisi moku walo
|
||||||
|
menu.nutrition.fat=kipisi moku suli
|
||||||
|
|
||||||
lang.en.display=toki Inli
|
lang.en.display=toki Inli
|
||||||
lang.nl.display=toki Netelan
|
lang.nl.display=toki Netelan
|
||||||
lang.pl.display=toki Posuka
|
lang.pl.display=toki Posuka
|
||||||
lang.tok.display=toki pona
|
lang.tok.display=toki pona
|
||||||
lang.tr.display=toki Tuki
|
lang.tr.display=toki Tuki
|
||||||
|
lang.zht.display=toki Sonko (tan pi tenpo pini)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,16 @@ menu.search=Arama...
|
||||||
|
|
||||||
menu.label.selected-langs=Diller
|
menu.label.selected-langs=Diller
|
||||||
|
|
||||||
lang.en.display=\u0130ngilizce
|
menu.shopping.title=Al??veri? listesi
|
||||||
lang.nl.display=Hollandaca
|
|
||||||
lang.pl.display=Leh\u00E7e
|
menu.nutrition.carbs=Karbonhidratlar
|
||||||
|
menu.nutrition.protein=Protein
|
||||||
|
menu.nutrition.fat=Ya?
|
||||||
|
|
||||||
|
lang.en.display=English
|
||||||
|
lang.nl.display=Nederlands
|
||||||
|
lang.pl.display=Polski
|
||||||
|
lang.tok.display=toki pona
|
||||||
lang.tr.display=T\u00FCrk\u00E7e
|
lang.tr.display=T\u00FCrk\u00E7e
|
||||||
|
lang.zht.display=中文(台灣)
|
||||||
|
lang.zhc.display=中文(中国大陆)
|
||||||
|
|
|
||||||
42
client/src/main/resources/locale/lang_zhc.properties
Normal file
42
client/src/main/resources/locale/lang_zhc.properties
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
add.ingredient.title=添加配料
|
||||||
|
add.recipe.title=创建食谱
|
||||||
|
add.step.title=添加步骤
|
||||||
|
|
||||||
|
add.ingredient.label=配料
|
||||||
|
add.recipe.label=食谱名称
|
||||||
|
add.step.label=步骤
|
||||||
|
|
||||||
|
button.ok=确认
|
||||||
|
button.cancel=取消
|
||||||
|
|
||||||
|
menu.label.recipes=食谱
|
||||||
|
menu.label.ingredients=配料
|
||||||
|
menu.label.preparation=准备步骤
|
||||||
|
|
||||||
|
menu.button.add.recipe=创建食谱
|
||||||
|
menu.button.add.ingredient=添加配料
|
||||||
|
menu.button.add.step=添加步骤
|
||||||
|
|
||||||
|
menu.button.remove.recipe=清除食谱
|
||||||
|
menu.button.remove.ingredient=清除配料
|
||||||
|
menu.button.remove.step=清除步骤
|
||||||
|
|
||||||
|
menu.button.edit=编辑
|
||||||
|
menu.button.clone=复制
|
||||||
|
menu.button.print=打印食谱
|
||||||
|
|
||||||
|
menu.search=搜索
|
||||||
|
|
||||||
|
menu.label.selected-langs=语言
|
||||||
|
|
||||||
|
menu.nutrition.carbs=?????
|
||||||
|
menu.nutrition.protein=???
|
||||||
|
menu.nutrition.fat=??
|
||||||
|
|
||||||
|
lang.en.display=English
|
||||||
|
lang.nl.display=Nederlands
|
||||||
|
lang.pl.display=Polski
|
||||||
|
lang.tok.display=toki pona
|
||||||
|
lang.tr.display=T\u00FCrk\u00E7e
|
||||||
|
lang.zht.display=中文(台灣)
|
||||||
|
lang.zhc.display=中文(中国大陆)
|
||||||
42
client/src/main/resources/locale/lang_zht.properties
Normal file
42
client/src/main/resources/locale/lang_zht.properties
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
add.ingredient.title=添加配料
|
||||||
|
add.recipe.title=創建食譜
|
||||||
|
add.step.title=添加步驟
|
||||||
|
|
||||||
|
add.ingredient.label=配料
|
||||||
|
add.recipe.label=食譜名稱
|
||||||
|
add.step.label=步驟
|
||||||
|
|
||||||
|
button.ok=確認
|
||||||
|
button.cancel=取消
|
||||||
|
|
||||||
|
menu.label.recipes=食譜
|
||||||
|
menu.label.ingredients=配料
|
||||||
|
menu.label.preparation=制備步驟
|
||||||
|
|
||||||
|
menu.button.add.recipe=創建食譜
|
||||||
|
menu.button.add.ingredient=添加配料
|
||||||
|
menu.button.add.step=添加步驟
|
||||||
|
|
||||||
|
menu.button.remove.recipe=清除食譜
|
||||||
|
menu.button.remove.ingredient=清除配料
|
||||||
|
menu.button.remove.step=清除步驟
|
||||||
|
|
||||||
|
menu.button.edit=編輯
|
||||||
|
menu.button.clone=複製
|
||||||
|
menu.button.print=列印食譜
|
||||||
|
|
||||||
|
menu.search=搜索
|
||||||
|
|
||||||
|
menu.label.selected-langs=語言
|
||||||
|
|
||||||
|
menu.nutrition.carbs=?????
|
||||||
|
menu.nutrition.protein=???
|
||||||
|
menu.nutrition.fat=??
|
||||||
|
|
||||||
|
lang.en.display=English
|
||||||
|
lang.nl.display=Nederlands
|
||||||
|
lang.pl.display=Polski
|
||||||
|
lang.tok.display=toki pona
|
||||||
|
lang.tr.display=T\u00FCrk\u00E7e
|
||||||
|
lang.zht.display=中文(台灣)
|
||||||
|
lang.zhc.display=中文(中国大陆)
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
package client.Ingredient;
|
||||||
|
|
||||||
|
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
|
||||||
|
import commons.Ingredient;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||||
|
|
||||||
|
@EnabledIfEnvironmentVariable(named = "DISPLAY", matches = ".+")
|
||||||
|
@WireMockTest(httpPort = 8080)
|
||||||
|
class IngredientControllerMockTest {
|
||||||
|
|
||||||
|
private IngredientController controller;
|
||||||
|
private ListView<Ingredient> ingredientListView;
|
||||||
|
|
||||||
|
// starting javaFX and allow use of listview and alert usage
|
||||||
|
@BeforeAll
|
||||||
|
static void initJavaFx() throws Exception {
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
try {
|
||||||
|
Platform.startup(latch::countDown);
|
||||||
|
} catch (IllegalStateException alreadyStarted) {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
assertTrue(latch.await(3, TimeUnit.SECONDS), "JavaFX Platform failed to start");
|
||||||
|
Platform.setImplicitExit(false);
|
||||||
|
}
|
||||||
|
//inject fxml fields and create controller + mock UI
|
||||||
|
@BeforeEach
|
||||||
|
void setup() throws Exception {
|
||||||
|
controller = new IngredientController();
|
||||||
|
|
||||||
|
ingredientListView = new ListView<>();
|
||||||
|
ingredientListView.setItems(FXCollections.observableArrayList(
|
||||||
|
new Ingredient("Bread", 1, 2, 3),
|
||||||
|
new Ingredient("Cheese", 2, 2, 2),
|
||||||
|
new Ingredient("Ham", 3, 3, 3)
|
||||||
|
));
|
||||||
|
|
||||||
|
setPrivateField(controller, "ingredientListView", ingredientListView);
|
||||||
|
setPrivateField(controller, "deleteButton", new Button("Delete"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick ingredient -> backend says not in use -> fake delete ingredient
|
||||||
|
@Test
|
||||||
|
void deleteIngredientWhenNotUsedCallsUsageThenDeleteAndClearsList() throws Exception {
|
||||||
|
Ingredient selected = ingredientListView.getItems().get(0);
|
||||||
|
ingredientListView.getSelectionModel().select(selected);
|
||||||
|
|
||||||
|
stubFor(get(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage"))
|
||||||
|
.willReturn(okJson("{\"ingredientId\":" + selected.getId() + ",\"usedInRecipes\":0}")));
|
||||||
|
|
||||||
|
stubFor(delete(urlEqualTo("/api/ingredients/" + selected.getId()))
|
||||||
|
.willReturn(ok()));
|
||||||
|
|
||||||
|
// safe close for show and wait, run controller on JavaFX
|
||||||
|
try (DialogCloser closer = startDialogCloser()) {
|
||||||
|
runOnFxThreadAndWait(() -> controller.handleDeleteIngredient(new ActionEvent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(getRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage")));
|
||||||
|
verify(deleteRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId())));
|
||||||
|
|
||||||
|
assertEquals(0, ingredientListView.getItems().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
//select ingredient -> if used backend says it and show warning -> safety delete but shouldn't happen
|
||||||
|
@Test
|
||||||
|
void deleteIngredientWhenUsedShowsWarningAndDoesNotDeleteIfDialogClosed() throws Exception {
|
||||||
|
Ingredient selected = ingredientListView.getItems().get(1);
|
||||||
|
ingredientListView.getSelectionModel().select(selected);
|
||||||
|
|
||||||
|
stubFor(get(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage"))
|
||||||
|
.willReturn(okJson("{\"ingredientId\":" + selected.getId() + ",\"usedInRecipes\":2}")));
|
||||||
|
|
||||||
|
stubFor(delete(urlEqualTo("/api/ingredients/" + selected.getId()))
|
||||||
|
.willReturn(ok()));
|
||||||
|
|
||||||
|
//safe close as if user selected cancel
|
||||||
|
try (DialogCloser closer = startDialogCloser()) {
|
||||||
|
runOnFxThreadAndWait(() -> controller.handleDeleteIngredient(new ActionEvent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// check usage but not delete
|
||||||
|
verify(getRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage")));
|
||||||
|
verify(0, deleteRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId())));
|
||||||
|
|
||||||
|
assertEquals(3, ingredientListView.getItems().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// fxml helper
|
||||||
|
private static void setPrivateField(Object target, String fieldName, Object value) throws Exception {
|
||||||
|
Field f = target.getClass().getDeclaredField(fieldName);
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(target, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
//controller on JavaFX
|
||||||
|
private static void runOnFxThreadAndWait(Runnable action) throws Exception {
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
try {
|
||||||
|
action.run();
|
||||||
|
} finally {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertTrue(latch.await(8, TimeUnit.SECONDS), "FX action timed out");
|
||||||
|
}
|
||||||
|
|
||||||
|
// safe close so that show and wait doesn't wait forever
|
||||||
|
private static DialogCloser startDialogCloser() {
|
||||||
|
AtomicBoolean running = new AtomicBoolean(true);
|
||||||
|
|
||||||
|
Thread t = new Thread(() -> {
|
||||||
|
while (running.get()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(50);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
for (Window w : Window.getWindows()) {
|
||||||
|
if (w instanceof Stage stage && stage.isShowing()) {
|
||||||
|
stage.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, "javafx-dialog-closer");
|
||||||
|
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
|
|
||||||
|
return new DialogCloser(running);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dialog closer
|
||||||
|
private static final class DialogCloser implements AutoCloseable {
|
||||||
|
private final AtomicBoolean running;
|
||||||
|
|
||||||
|
private DialogCloser(AtomicBoolean running) {
|
||||||
|
this.running = running;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
running.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package commons;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
@ -15,6 +16,13 @@ public class FormalIngredient extends RecipeIngredient implements Scalable<Forma
|
||||||
private double amount;
|
private double amount;
|
||||||
private String unitSuffix;
|
private String unitSuffix;
|
||||||
|
|
||||||
|
private static final int tbspToPoundConvert = 32;
|
||||||
|
private static final int tbspToCupConvert = 16;
|
||||||
|
private static final int tbspToOunceConvert = 2;
|
||||||
|
private static final int OunceToPoundConvert = 16;
|
||||||
|
private static final DecimalFormat numberFormat = new DecimalFormat("#.00");
|
||||||
|
|
||||||
|
|
||||||
public double getAmount() {
|
public double getAmount() {
|
||||||
return amount;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +39,8 @@ public class FormalIngredient extends RecipeIngredient implements Scalable<Forma
|
||||||
this.unitSuffix = unitSuffix;
|
this.unitSuffix = unitSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public FormalIngredient(Long id, Ingredient ingredient, double amount, String unitSuffix) {
|
public FormalIngredient(Long id, Ingredient ingredient, double amount, String unitSuffix) {
|
||||||
// For testing
|
// For testing
|
||||||
super(id, ingredient);
|
super(id, ingredient);
|
||||||
|
|
@ -55,8 +65,54 @@ public class FormalIngredient extends RecipeIngredient implements Scalable<Forma
|
||||||
}
|
}
|
||||||
return amount * unit.get().conversionFactor;
|
return amount * unit.get().conversionFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String normalisedUnit(){
|
||||||
|
Optional<Unit> unit = Unit.fromString(unitSuffix);
|
||||||
|
if (unit.isEmpty() || !unit.get().isFormal() || unit.get().conversionFactor <= 0) {
|
||||||
|
return numberFormat.format(amount) + unitSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
Unit currentUnit = unit.get();
|
||||||
|
double baseAmount = amount * currentUnit.conversionFactor;
|
||||||
|
|
||||||
|
switch (currentUnit){
|
||||||
|
case GRAMME -> {
|
||||||
|
if(baseAmount >= Unit.TONNE.conversionFactor){
|
||||||
|
return numberFormat.format(baseAmount /Unit.TONNE.conversionFactor) + Unit.TONNE.suffix;
|
||||||
|
}if(baseAmount >=Unit.KILOGRAMME.conversionFactor) {
|
||||||
|
return numberFormat.format(baseAmount / Unit.KILOGRAMME.conversionFactor) + Unit.KILOGRAMME.suffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case MILLILITRE -> {
|
||||||
|
if (baseAmount >= Unit.LITRE.conversionFactor) {
|
||||||
|
return numberFormat.format(baseAmount /Unit.LITRE.conversionFactor) + Unit.LITRE.suffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case TABLESPOON -> {
|
||||||
|
if(amount>=tbspToPoundConvert){
|
||||||
|
return numberFormat.format(amount/tbspToPoundConvert) + Unit.POUND.suffix;
|
||||||
|
}
|
||||||
|
if(amount>=tbspToCupConvert){
|
||||||
|
return numberFormat.format(amount /tbspToCupConvert) + Unit.CUP.suffix;
|
||||||
|
}
|
||||||
|
if(amount>=tbspToOunceConvert){
|
||||||
|
return numberFormat.format(amount /tbspToOunceConvert) + Unit.OUNCE.suffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case OUNCE -> {
|
||||||
|
if (baseAmount >= OunceToPoundConvert) {
|
||||||
|
return numberFormat.format(amount / OunceToPoundConvert) + Unit.POUND.suffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return numberFormat.format(amount) + currentUnit.suffix;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return amount + unitSuffix + " of " + ingredient.name;
|
return normalisedUnit()+ " of " + ingredient.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,10 @@ public class Recipe {
|
||||||
final double PER = 100; // Gram
|
final double PER = 100; // Gram
|
||||||
return
|
return
|
||||||
this.ingredients.stream().mapToDouble(RecipeIngredient::getKcal).sum() /
|
this.ingredients.stream().mapToDouble(RecipeIngredient::getKcal).sum() /
|
||||||
this.ingredients.stream().mapToDouble(RecipeIngredient::getBaseAmount).sum() * PER;
|
weight() * PER;
|
||||||
|
}
|
||||||
|
public double weight() {
|
||||||
|
return this.ingredients.stream().mapToDouble(RecipeIngredient::getBaseAmount).sum();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
58
commons/src/test/java/commons/FormalIngredientTest.java
Normal file
58
commons/src/test/java/commons/FormalIngredientTest.java
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package commons;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class FormalIngredientTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void normaliseGramsToTest(){
|
||||||
|
Ingredient ingredient = new Ingredient("Bread", 1, 2, 3);
|
||||||
|
FormalIngredient toKg = new FormalIngredient(ingredient,1_000,"g");
|
||||||
|
FormalIngredient toTonne = new FormalIngredient(ingredient,1_000_000,"g");
|
||||||
|
|
||||||
|
|
||||||
|
assertEquals(toKg.normalisedUnit(),"1.00kg");
|
||||||
|
assertEquals(toTonne.normalisedUnit(),"1.00t");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void normaliseMillilitresToLitresTest(){
|
||||||
|
Ingredient ingredient = new Ingredient("Bread", 1, 2, 3);
|
||||||
|
FormalIngredient toKg = new FormalIngredient(ingredient,1_000,"ml");
|
||||||
|
|
||||||
|
assertEquals(toKg.normalisedUnit(),"1.00l");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void normaliseOunceToPoundTest(){
|
||||||
|
Ingredient ingredient = new Ingredient("Bread", 1, 2, 3);
|
||||||
|
FormalIngredient toPound = new FormalIngredient(ingredient,16,"oz");
|
||||||
|
|
||||||
|
assertEquals(toPound.normalisedUnit(),"1.00lb");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void normaliseTablespoonToTest(){
|
||||||
|
Ingredient ingredient = new Ingredient("Bread", 1, 2, 3);
|
||||||
|
FormalIngredient toCup = new FormalIngredient(ingredient,16,"tbsp");
|
||||||
|
FormalIngredient toPound = new FormalIngredient(ingredient,32,"tbsp");
|
||||||
|
FormalIngredient toOunce = new FormalIngredient(ingredient,2,"tbsp");
|
||||||
|
|
||||||
|
assertEquals(toCup.normalisedUnit(),"1.00cup(s)");
|
||||||
|
assertEquals(toPound.normalisedUnit(),"1.00lb");
|
||||||
|
assertEquals(toOunce.normalisedUnit(),"1.00oz");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noNormaliseTest(){
|
||||||
|
Ingredient ingredient = new Ingredient("Bread", 1, 2, 3);
|
||||||
|
FormalIngredient informal = new FormalIngredient(ingredient,10,"<NONE>");
|
||||||
|
FormalIngredient toSmall = new FormalIngredient(ingredient,10,"g");
|
||||||
|
|
||||||
|
assertEquals(informal.normalisedUnit(),"10.00<NONE>");
|
||||||
|
assertEquals(toSmall.normalisedUnit(),"10.00g");
|
||||||
|
}
|
||||||
|
}
|
||||||
67
docs/agenda/agenda-09.md
Normal file
67
docs/agenda/agenda-09.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Week 9 meeting agenda
|
||||||
|
|
||||||
|
| Key | Value |
|
||||||
|
| ------------ | --------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Date | Jan 23rd 2026 |
|
||||||
|
| Time | 16:45 |
|
||||||
|
| Location | DW PC Hall 2 |
|
||||||
|
| Chair | Natalia Cholewa |
|
||||||
|
| Minute Taker | Steven Liu |
|
||||||
|
| Attendees | Natalia Cholewa, Oskar Rasieński, Rithvik Sriram, Aysegul Aydinlik, Steven Liu, Mei Chang van der Werff |
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
### Opening
|
||||||
|
|
||||||
|
1. (1 min) Introduction by chair
|
||||||
|
|
||||||
|
> 4:53 PM Begin meeting
|
||||||
|
> Topics:
|
||||||
|
> Go through everything done
|
||||||
|
> Assign the last few tasks and clean up
|
||||||
|
> A bit of quality assurance
|
||||||
|
|
||||||
|
2. (1 min) Any additions to the agenda?
|
||||||
|
|
||||||
|
> None so far.
|
||||||
|
|
||||||
|
3. (1-2 min) TA announcements?
|
||||||
|
|
||||||
|
> Try to finish before Sunday due to concerns of GitLab performance.
|
||||||
|
> No need to explain everything, but mention meaningful additions in the README (refer to Brightspace announcement)
|
||||||
|
|
||||||
|
4. (1 min) Buddycheck reminder - January 30th
|
||||||
|
|
||||||
|
> remember to stick to the AID model when writing the Buddycheck.
|
||||||
|
|
||||||
|
### Final check-ups
|
||||||
|
5. (15 min) Go over all everything we've done and check how close we are to completion with every feature.
|
||||||
|
> We are basically all done except a few changes.
|
||||||
|
> Still some work on languages. PR to be made.
|
||||||
|
> Further contributions - try to add testing on client side if we have free time
|
||||||
|
* (5 min) Progress check regarding this week
|
||||||
|
> Rithvik - improved client features by providing interface to end user if server not found on URL
|
||||||
|
> Oskar - Translation and various bugfixes
|
||||||
|
> Mei - Highlight for favouriting, alphabetic ordering for ingredients, normalizing units, server tests
|
||||||
|
> Ayse - Turkish i18n, ingredientctrl tests, multiple searching, rest of 4.5 aka. printing etc.
|
||||||
|
> Steven - crud buttons for the shopping list, adding chinese, various bugfixes
|
||||||
|
> Nat - scaffolding for shopping list, toki pona
|
||||||
|
* (3 min) Showcase of the (almost final) version
|
||||||
|
> Implemented features look fully completed. Some finer parts may need revision
|
||||||
|
### Planning
|
||||||
|
6. (5 min) Assign the very last features to implement today.
|
||||||
|
* This can be as simple as going over them as well.
|
||||||
|
* Code freeze is **Sunday**. Make sure every issue is closed, every merge request is resolved and every other branch is deleted.
|
||||||
|
> 1. lang, pie chart constantly refreshes, it should refresh less
|
||||||
|
> 2. check stale TODOs and implement real TODOs otherwise
|
||||||
|
> 3. fix janky ingredient list
|
||||||
|
> 4. should have everything done today, worst case tmr morning.
|
||||||
|
> 5. housekeeping on the git repo (remove branches, clean issues, milestones, etc.)
|
||||||
|
### Closing
|
||||||
|
7. (2 min) Questions? (None)
|
||||||
|
8. (2 min) Summarize everything
|
||||||
|
> 98% done, need to fix tiny bugs, and that's it
|
||||||
|
9. (1 min) TA remarks
|
||||||
|
|
||||||
|
Closed at 5:09 PM, total elapsed: 16 minutes.
|
||||||
|
Estimated meeting time: ~25-30 minutes
|
||||||
60
docs/feedback/week-08.md
Normal file
60
docs/feedback/week-08.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
|
||||||
|
|
||||||
|
# Meeting feedback
|
||||||
|
|
||||||
|
|
||||||
|
**Quick Summary Scale**
|
||||||
|
Insufficient/Sufficient/Good/Excellent
|
||||||
|
|
||||||
|
|
||||||
|
#### Agenda
|
||||||
|
Feedback: **Excellent**
|
||||||
|
|
||||||
|
- The agenda was added on time to the repository.
|
||||||
|
- The agenda follows the required format.
|
||||||
|
- The individual points of discussion are clear and detailed.
|
||||||
|
- I liked that you kept the separate sections template for the agenda.
|
||||||
|
|
||||||
|
#### Performance of the Previous Minute Taker
|
||||||
|
Feedback: **Excellent**
|
||||||
|
|
||||||
|
- The notes have been merged to the agenda file.
|
||||||
|
- The notes are plentiful, detailed and clear.
|
||||||
|
- They contain concrete and realistic agreements.
|
||||||
|
- Everyone was assigned a task.
|
||||||
|
|
||||||
|
|
||||||
|
#### Chair performance
|
||||||
|
Feedback: **Excellent**
|
||||||
|
|
||||||
|
- You worked through your agenda well and carried the meeting forward.
|
||||||
|
- You engaged everyone into the conversation.
|
||||||
|
- Time estimates seemed accurate.
|
||||||
|
- I liked that you started with a brief summary of all feedbacks.
|
||||||
|
- You listened to everyone and also ensured that they agree with you.
|
||||||
|
|
||||||
|
|
||||||
|
#### Attitude & Relation
|
||||||
|
Feedback: **Excellent**
|
||||||
|
|
||||||
|
- Everyone took good ownership of the meeting and participated equally.
|
||||||
|
- Everyone was active and listened to all ideas.
|
||||||
|
- The atmosphere was practical and constructive.
|
||||||
|
- Everyone provided feedback on the agreements.
|
||||||
|
|
||||||
|
|
||||||
|
#### Potentially Shippable Product
|
||||||
|
Feedback: **Excellent**
|
||||||
|
|
||||||
|
- You presented the current shippable product during the meeting.
|
||||||
|
- There was great progress compared to last week.
|
||||||
|
- You are on a good track to create a fully working application by the end.
|
||||||
|
|
||||||
|
|
||||||
|
#### Work Contribution/Distribution in the Team
|
||||||
|
Feedback: **Excellent**
|
||||||
|
|
||||||
|
- You discussed the action points of the last week and your progress on them.
|
||||||
|
- Everyone reached their goal.
|
||||||
|
- Everyone has contributed to the project.
|
||||||
|
- I liked that you distributed all remaining tasks and set an internal deadline for the project.
|
||||||
102
locc.sh
Normal file
102
locc.sh
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# 1. Check for git repo
|
||||||
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
echo "Error: current directory is not a git repository." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Define path patterns
|
||||||
|
PATH_CLIENT='client/src/**/*.java'
|
||||||
|
PATH_SERVER='server/src/**/*.java'
|
||||||
|
PATH_COMMONS='commons/src/**/*.java'
|
||||||
|
PATH_PROD='*/src/main/*.java'
|
||||||
|
PATH_TEST='*/src/test/*.java'
|
||||||
|
PATH_ALL='*.java'
|
||||||
|
|
||||||
|
# 3. Helper functions
|
||||||
|
|
||||||
|
# Standard count: Includes imports, respects WS changes
|
||||||
|
count_lines_standard() {
|
||||||
|
git show --format="" --patch "$1" -- "$2" \
|
||||||
|
| grep -E '^\+[^+/][^/]+$' \
|
||||||
|
| grep -v '+ *[*@]' \
|
||||||
|
| wc -l
|
||||||
|
}
|
||||||
|
|
||||||
|
# Strict count: Ignores imports, ignores pure WS changes
|
||||||
|
count_lines_strict() {
|
||||||
|
git show --format="" --patch -w "$1" -- "$2" \
|
||||||
|
| grep -E '^\+[^+/][^/]+$' \
|
||||||
|
| grep -v 'import' \
|
||||||
|
| grep -v '+ *[*@]' \
|
||||||
|
| wc -l
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Analyzing commits on 'main'..." >&2
|
||||||
|
|
||||||
|
# 4. Main Loop
|
||||||
|
# Use %ae for Author Email
|
||||||
|
git log --no-merges --pretty=format:'%H %ae' main | {
|
||||||
|
|
||||||
|
declare -A client_count
|
||||||
|
declare -A server_count
|
||||||
|
declare -A commons_count
|
||||||
|
declare -A prod_count
|
||||||
|
declare -A test_count
|
||||||
|
declare -A total_count
|
||||||
|
declare -A strict_count
|
||||||
|
declare -A seen_users
|
||||||
|
|
||||||
|
while read -r hash raw_email; do
|
||||||
|
# Normalize email to lowercase
|
||||||
|
email=$(echo "$raw_email" | tr '[:upper:]' '[:lower:]')
|
||||||
|
seen_users["$email"]=1
|
||||||
|
|
||||||
|
# Run counts (Standard)
|
||||||
|
c_add=$(count_lines_standard "$hash" "$PATH_CLIENT")
|
||||||
|
s_add=$(count_lines_standard "$hash" "$PATH_SERVER")
|
||||||
|
k_add=$(count_lines_standard "$hash" "$PATH_COMMONS")
|
||||||
|
p_add=$(count_lines_standard "$hash" "$PATH_PROD")
|
||||||
|
t_add=$(count_lines_standard "$hash" "$PATH_TEST")
|
||||||
|
all_add=$(count_lines_standard "$hash" "$PATH_ALL")
|
||||||
|
|
||||||
|
# Run count (Strict)
|
||||||
|
strict_add=$(count_lines_strict "$hash" "$PATH_ALL")
|
||||||
|
|
||||||
|
# Accumulate
|
||||||
|
client_count["$email"]=$(( ${client_count["$email"]:-0} + c_add ))
|
||||||
|
server_count["$email"]=$(( ${server_count["$email"]:-0} + s_add ))
|
||||||
|
commons_count["$email"]=$(( ${commons_count["$email"]:-0} + k_add ))
|
||||||
|
prod_count["$email"]=$(( ${prod_count["$email"]:-0} + p_add ))
|
||||||
|
test_count["$email"]=$(( ${test_count["$email"]:-0} + t_add ))
|
||||||
|
total_count["$email"]=$(( ${total_count["$email"]:-0} + all_add ))
|
||||||
|
strict_count["$email"]=$(( ${strict_count["$email"]:-0} + strict_add ))
|
||||||
|
|
||||||
|
printf "." >&2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
echo "Done." >&2
|
||||||
|
|
||||||
|
# 5. Print Table
|
||||||
|
# Widths: Email=40, Others=10, Strict=13
|
||||||
|
printf "%-40s | %-10s | %-10s | %-10s | %-10s | %-10s | %-10s | %-13s\n" \
|
||||||
|
"User Email" "Client" "Server" "Commons" "Prod" "Test" "Total" "Total (Strict)"
|
||||||
|
printf "%s\n" "-----------------------------------------|------------|------------|------------|------------|------------|------------|----------------"
|
||||||
|
|
||||||
|
for email in "${!seen_users[@]}"; do
|
||||||
|
echo "$email"
|
||||||
|
done | sort | while read -r e; do
|
||||||
|
printf "%-40s | %-10d | %-10d | %-10d | %-10d | %-10d | %-10d | %-13d\n" \
|
||||||
|
"$e" \
|
||||||
|
"${client_count[$e]:-0}" \
|
||||||
|
"${server_count[$e]:-0}" \
|
||||||
|
"${commons_count[$e]:-0}" \
|
||||||
|
"${prod_count[$e]:-0}" \
|
||||||
|
"${test_count[$e]:-0}" \
|
||||||
|
"${total_count[$e]:-0}" \
|
||||||
|
"${strict_count[$e]:-0}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
@ -31,31 +31,37 @@ class PortCheckerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Test
|
@Test
|
||||||
void invalidPort(){
|
void findNotDefaultFreePort() throws IOException {
|
||||||
PortChecker checker = new PortChecker();
|
|
||||||
|
|
||||||
assertThrows(IllegalArgumentException.class, ()-> {
|
|
||||||
checker.isPortAvailable(-1);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assertThrows(IllegalArgumentException.class, ()-> {
|
|
||||||
checker.isPortAvailable(65536);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
void findFreePort() throws IOException {
|
|
||||||
PortChecker checker = new PortChecker();
|
PortChecker checker = new PortChecker();
|
||||||
|
|
||||||
int port = checker.findFreePort();
|
int port = checker.findFreePort();
|
||||||
int defaultPort = 8080;
|
int lowestPossiblePort = 0;
|
||||||
int lastPort = 8090;
|
int highestPossiblePort = 65535;
|
||||||
|
|
||||||
boolean greaterOrEqual = port >= defaultPort;
|
assertTrue(port > lowestPossiblePort);
|
||||||
boolean lessOrEqual = port <= lastPort;
|
assertTrue(port <= highestPossiblePort);
|
||||||
boolean inRange = greaterOrEqual && lessOrEqual;
|
assertTrue(checker.isPortAvailable(port));
|
||||||
boolean isItFree = checker.isPortAvailable(port);
|
}
|
||||||
|
@Test
|
||||||
|
void findDefaultFreePort() throws IOException {
|
||||||
|
PortChecker checker = new PortChecker();
|
||||||
|
|
||||||
assertTrue(inRange);
|
boolean free = checker.isPortAvailable(8080);
|
||||||
|
|
||||||
|
assertTrue(free);
|
||||||
|
assertEquals(checker.findFreePort(),8080);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidFreePort(){
|
||||||
|
PortChecker checker = new PortChecker();
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () ->
|
||||||
|
checker.isPortAvailable(-1)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () ->
|
||||||
|
checker.isPortAvailable(65536)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ import server.service.RecipeService;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.LongStream;
|
import java.util.stream.LongStream;
|
||||||
|
|
@ -67,7 +68,9 @@ public class RecipeControllerTest {
|
||||||
.mapToObj(x -> new Recipe(
|
.mapToObj(x -> new Recipe(
|
||||||
null,
|
null,
|
||||||
"Recipe " + x,
|
"Recipe " + x,
|
||||||
"en", List.of(), List.of()))
|
"en",
|
||||||
|
List.of(),
|
||||||
|
List.of()))
|
||||||
.toList();
|
.toList();
|
||||||
controller = new RecipeController(
|
controller = new RecipeController(
|
||||||
recipeService,
|
recipeService,
|
||||||
|
|
@ -80,8 +83,11 @@ public class RecipeControllerTest {
|
||||||
if (tags.contains("test-from-init-data")) {
|
if (tags.contains("test-from-init-data")) {
|
||||||
ids = LongStream
|
ids = LongStream
|
||||||
.range(0, NUM_RECIPES)
|
.range(0, NUM_RECIPES)
|
||||||
.map(idx -> recipeRepository.save(recipes.get((int) idx)).getId())
|
.map(idx -> recipeRepository
|
||||||
.boxed().toList();
|
.save(recipes.get((int) idx))
|
||||||
|
.getId())
|
||||||
|
.boxed()
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some tests need to know the stored IDs of objects
|
// Some tests need to know the stored IDs of objects
|
||||||
|
|
@ -117,9 +123,65 @@ public class RecipeControllerTest {
|
||||||
controller.getRecipes(
|
controller.getRecipes(
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.of(List.of("en", "nl"))).getBody().size());;
|
Optional.of(List.of("en", "nl")))
|
||||||
|
.getBody()
|
||||||
|
.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Tag("test-from-init-data")
|
||||||
|
public void getRecipesWithNegativeLimit(){
|
||||||
|
assertEquals(0,
|
||||||
|
controller.getRecipes(Optional.empty(),
|
||||||
|
Optional.of(-2),
|
||||||
|
Optional.of(List.of("en")))
|
||||||
|
.getBody().size());
|
||||||
|
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@Tag("test-from-init-data")
|
||||||
|
public void getRecipesWithSearch() {
|
||||||
|
|
||||||
|
recipeRepository.save(new Recipe(
|
||||||
|
null,
|
||||||
|
"banana pie",
|
||||||
|
"en",
|
||||||
|
List.of(),
|
||||||
|
List.of()));
|
||||||
|
|
||||||
|
assertEquals(1, controller.getRecipes(
|
||||||
|
Optional.of("banana"),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.of(List.of("en"))).getBody().size());
|
||||||
|
|
||||||
|
assertEquals("banana pie", Objects.requireNonNull(controller.getRecipes(
|
||||||
|
Optional.of("banana"),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.of(List.of("en"))).getBody()).getFirst().getName());
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@Tag("test-from-init-data")
|
||||||
|
public void getRecipesWithZeroLimit() {
|
||||||
|
assertEquals(0, controller.getRecipes(
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.of(0),
|
||||||
|
Optional.of(List.of("en"))).getBody().size());
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
@Tag("test-from-init-data")
|
||||||
|
public void getRecipesWithEmptySearch() {
|
||||||
|
var response = controller.getRecipes(
|
||||||
|
Optional.of(" "),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.of(List.of("en"))
|
||||||
|
);
|
||||||
|
assertEquals(recipes.size(), response.getBody().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Tag("test-from-init-data")
|
@Tag("test-from-init-data")
|
||||||
public void getSomeRecipes() {
|
public void getSomeRecipes() {
|
||||||
|
|
@ -129,7 +191,9 @@ public class RecipeControllerTest {
|
||||||
controller.getRecipes(
|
controller.getRecipes(
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.of(LIMIT),
|
Optional.of(LIMIT),
|
||||||
Optional.of(List.of("en"))).getBody().size());
|
Optional.of(List.of("en")))
|
||||||
|
.getBody()
|
||||||
|
.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -140,7 +204,9 @@ public class RecipeControllerTest {
|
||||||
controller.getRecipes(
|
controller.getRecipes(
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.of(List.of("en", "nl"))).getBody().size());
|
Optional.of(List.of("en", "nl")))
|
||||||
|
.getBody()
|
||||||
|
.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -151,7 +217,9 @@ public class RecipeControllerTest {
|
||||||
controller.getRecipes(
|
controller.getRecipes(
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.of(List.of("nl"))).getBody().size());
|
Optional.of(List.of("nl")))
|
||||||
|
.getBody()
|
||||||
|
.size());
|
||||||
}
|
}
|
||||||
@Test
|
@Test
|
||||||
@Tag("test-from-init-data")
|
@Tag("test-from-init-data")
|
||||||
|
|
@ -161,7 +229,9 @@ public class RecipeControllerTest {
|
||||||
controller.getRecipes(
|
controller.getRecipes(
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.of(LIMIT),
|
Optional.of(LIMIT),
|
||||||
Optional.of(List.of("en", "nl"))).getBody().size());
|
Optional.of(List.of("en", "nl")))
|
||||||
|
.getBody()
|
||||||
|
.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -172,7 +242,8 @@ public class RecipeControllerTest {
|
||||||
// The third item in the input list is the same as the third item retrieved from the database
|
// The third item in the input list is the same as the third item retrieved from the database
|
||||||
assertEquals(
|
assertEquals(
|
||||||
recipes.get(CHECK_INDEX),
|
recipes.get(CHECK_INDEX),
|
||||||
controller.getRecipe(recipeIds.get(CHECK_INDEX)).getBody());
|
controller.getRecipe(recipeIds.get(CHECK_INDEX))
|
||||||
|
.getBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -181,7 +252,8 @@ public class RecipeControllerTest {
|
||||||
// There does not exist a recipe with ID=3 since there are no items in the repository.
|
// There does not exist a recipe with ID=3 since there are no items in the repository.
|
||||||
assertEquals(
|
assertEquals(
|
||||||
HttpStatus.NOT_FOUND,
|
HttpStatus.NOT_FOUND,
|
||||||
controller.getRecipe((long) CHECK_INDEX).getStatusCode());
|
controller.getRecipe((long) CHECK_INDEX)
|
||||||
|
.getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -192,7 +264,8 @@ public class RecipeControllerTest {
|
||||||
|
|
||||||
// The object has been successfully deleted
|
// The object has been successfully deleted
|
||||||
assertEquals(HttpStatus.OK,
|
assertEquals(HttpStatus.OK,
|
||||||
controller.deleteRecipe(recipeIds.get(DELETE_INDEX)).getStatusCode());
|
controller.deleteRecipe(recipeIds.get(DELETE_INDEX))
|
||||||
|
.getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -211,7 +284,8 @@ public class RecipeControllerTest {
|
||||||
public void deleteOneRecipeFail() {
|
public void deleteOneRecipeFail() {
|
||||||
final Long DELETE_INDEX = 5L;
|
final Long DELETE_INDEX = 5L;
|
||||||
assertEquals(HttpStatus.BAD_REQUEST,
|
assertEquals(HttpStatus.BAD_REQUEST,
|
||||||
controller.deleteRecipe(DELETE_INDEX).getStatusCode());
|
controller.deleteRecipe(DELETE_INDEX)
|
||||||
|
.getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -219,10 +293,16 @@ public class RecipeControllerTest {
|
||||||
@Tag("need-ids")
|
@Tag("need-ids")
|
||||||
public void updateOneRecipeHasNewData() {
|
public void updateOneRecipeHasNewData() {
|
||||||
final int UPDATE_INDEX = 5;
|
final int UPDATE_INDEX = 5;
|
||||||
Recipe newRecipe = controller.getRecipe(recipeIds.get(UPDATE_INDEX)).getBody();
|
Recipe newRecipe = controller.getRecipe(recipeIds
|
||||||
|
.get(UPDATE_INDEX))
|
||||||
|
.getBody();
|
||||||
|
|
||||||
newRecipe.setName("New recipe");
|
newRecipe.setName("New recipe");
|
||||||
controller.updateRecipe(newRecipe.getId(), newRecipe);
|
controller.updateRecipe(newRecipe.getId(), newRecipe);
|
||||||
|
|
||||||
assertEquals("New recipe",
|
assertEquals("New recipe",
|
||||||
recipeRepository.getReferenceById(recipeIds.get(UPDATE_INDEX)).getName());
|
recipeRepository.getReferenceById(recipeIds
|
||||||
|
.get(UPDATE_INDEX))
|
||||||
|
.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue