Compare commits
54 changed files with 227 additions and 2288 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
|
||||
|
||||
The client needs to be launched **after** a server is already running, see Usage.Server section:
|
||||
|
||||
```
|
||||
mvn -pl client -am javafx:run
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
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:
|
||||
By default, the server listens to the port `8080`.
|
||||
[TODO(1)]:: Configurable port.
|
||||
|
||||
```
|
||||
mvn -pl server -am spring-boot:run
|
||||
```
|
||||
|
||||
## Implemented features
|
||||
## Features
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
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.
|
||||
- Recipe tracking in an intuitive GUI.
|
||||
- Ability to input arbitrary amounts of an ingredient in a recipe. The design is very human. See Manual.Ingredients for more usage details.
|
||||
- Native localization in more than 2, and less than 4 languages.
|
||||
- Configurable via JSON, See Manual.Configuration.
|
||||
|
||||
## Manual
|
||||
|
||||
|
|
@ -58,5 +58,5 @@ The configuration is with JSON, read from `config.json` in the working directory
|
|||
|
||||
### 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 from the dropdown.
|
||||
- To input a **formal** ingredient, you write the numeric amount in the first input box, then the Unit in the selection dropdown, and then write the name of the ingredient, e.g. salt, apples, etc. should it be not visible already on the platform.
|
||||
- To input an **informal** ingredient, describe the amount in the first input box, like "some of", or "a sprinkle of", then select "<NONE>" in the unit selection box, and write the name of your ingredient or pick from one of the availables from the dropdown.
|
||||
|
|
|
|||
|
|
@ -21,13 +21,7 @@
|
|||
<artifactId>commons</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</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>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public class IngredientController {
|
|||
private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client
|
||||
|
||||
@FXML
|
||||
public void handleDeleteIngredient(ActionEvent event) {
|
||||
private void handleDeleteIngredient(ActionEvent event) {
|
||||
// Get selected ingredient
|
||||
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
||||
if (selectedIngredient == null) {
|
||||
|
|
|
|||
|
|
@ -21,11 +21,6 @@ import client.scenes.nutrition.NutritionDetailsCtrl;
|
|||
import client.scenes.nutrition.NutritionViewCtrl;
|
||||
import client.scenes.recipe.IngredientListCtrl;
|
||||
import client.scenes.recipe.RecipeStepListCtrl;
|
||||
import client.scenes.shopping.ShoppingListCtrl;
|
||||
import client.scenes.shopping.ShoppingListNewItemPromptCtrl;
|
||||
import client.service.ShoppingListService;
|
||||
import client.service.ShoppingListServiceImpl;
|
||||
import client.service.ShoppingListViewModel;
|
||||
import client.utils.ConfigService;
|
||||
import client.utils.LocaleManager;
|
||||
import client.utils.server.ServerUtils;
|
||||
|
|
@ -62,10 +57,6 @@ public class MyModule implements Module {
|
|||
binder.bind(new TypeLiteral<WebSocketDataService<Long, Recipe>>() {}).toInstance(
|
||||
new WebSocketDataService<>()
|
||||
);
|
||||
binder.bind(ShoppingListNewItemPromptCtrl.class).in(Scopes.SINGLETON);
|
||||
binder.bind(ShoppingListCtrl.class).in(Scopes.SINGLETON);
|
||||
binder.bind(ShoppingListViewModel.class).toInstance(new ShoppingListViewModel());
|
||||
binder.bind(ShoppingListService.class).to(ShoppingListServiceImpl.class);
|
||||
binder.bind(new TypeLiteral<WebSocketDataService<Long, Ingredient>>() {}).toInstance(
|
||||
new WebSocketDataService<>()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package client;
|
|||
|
||||
import client.scenes.FoodpalApplicationCtrl;
|
||||
import client.scenes.MainCtrl;
|
||||
import client.scenes.ServerConnectionDialogCtrl;
|
||||
import client.utils.server.ServerUtils;
|
||||
import com.google.inject.Injector;
|
||||
import javafx.application.Application;
|
||||
|
|
@ -28,14 +27,9 @@ public class UI extends Application {
|
|||
|
||||
var serverUtils = INJECTOR.getInstance(ServerUtils.class);
|
||||
if (!serverUtils.isServerAvailable()) {
|
||||
var connectionHandler = INJECTOR.getInstance(ServerConnectionDialogCtrl.class);
|
||||
boolean serverConnected = connectionHandler.promptForURL();
|
||||
|
||||
if(!serverConnected){
|
||||
var msg = "User Cancelled Server connection. Shutting down";
|
||||
System.err.print(msg);
|
||||
return;
|
||||
}
|
||||
var msg = "Server needs to be started before the client, but it does not seem to be available. Shutting down.";
|
||||
System.err.println(msg);
|
||||
return;
|
||||
}
|
||||
var foodpal = FXML.load(FoodpalApplicationCtrl.class, "client", "scenes", "FoodpalApplication.fxml");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
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.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import client.UI;
|
||||
import client.exception.InvalidModificationException;
|
||||
import client.scenes.nutrition.NutritionViewCtrl;
|
||||
import client.scenes.recipe.RecipeDetailCtrl;
|
||||
|
||||
import client.scenes.shopping.ShoppingListCtrl;
|
||||
import client.utils.Config;
|
||||
import client.utils.ConfigService;
|
||||
import client.utils.DefaultValueFactory;
|
||||
|
|
@ -21,6 +21,7 @@ import client.utils.LocaleManager;
|
|||
import client.utils.server.ServerUtils;
|
||||
import client.utils.WebSocketDataService;
|
||||
import client.utils.WebSocketUtils;
|
||||
import commons.Ingredient;
|
||||
import commons.Recipe;
|
||||
|
||||
import commons.ws.Topics;
|
||||
|
|
@ -31,20 +32,15 @@ import commons.ws.messages.Message;
|
|||
import commons.ws.messages.UpdateRecipeMessage;
|
||||
import jakarta.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextInputDialog;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
|
@ -70,7 +66,8 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
@FXML
|
||||
public ListView<Recipe> recipeList;
|
||||
|
||||
private final ListProperty<Recipe> favouriteRecipeList = new SimpleListProperty<>();
|
||||
@FXML
|
||||
private ListView<Ingredient> ingredientListView;
|
||||
|
||||
@FXML
|
||||
private Button addRecipeButton;
|
||||
|
|
@ -91,6 +88,8 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
@FXML
|
||||
private Button manageIngredientsButton;
|
||||
|
||||
private List<Recipe> allRecipes = new ArrayList<>();
|
||||
|
||||
@FXML
|
||||
private Label updatedBadge;
|
||||
|
||||
|
|
@ -288,13 +287,6 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
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();
|
||||
refresh();
|
||||
|
|
@ -333,7 +325,8 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
logger.severe(msg);
|
||||
printError(msg);
|
||||
}
|
||||
recipeList.getItems().setAll(recipes);
|
||||
|
||||
allRecipes = new ArrayList<>(recipes);
|
||||
applyRecipeFilterAndKeepSelection();
|
||||
|
||||
showUpdatedBadge();
|
||||
|
|
@ -429,21 +422,17 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
public void applyRecipeFilterAndKeepSelection() {
|
||||
Recipe selected = recipeList.getSelectionModel().getSelectedItem();
|
||||
Long selectedId = selected == null ? null : selected.getId();
|
||||
List<Recipe> view = recipeList.getItems().stream().toList();
|
||||
|
||||
List<Recipe> view = allRecipes;
|
||||
|
||||
if (favouritesOnlyToggle != null && favouritesOnlyToggle.isSelected()) {
|
||||
view = favouriteRecipeList.get();
|
||||
recipeList.getItems().setAll(view);
|
||||
}
|
||||
try {
|
||||
if (favouritesOnlyToggle != null && !favouritesOnlyToggle.isSelected()) {
|
||||
recipeList.getItems().setAll(server.getRecipes(config.getRecipeLanguages()));
|
||||
}
|
||||
}
|
||||
catch (IOException | InterruptedException e) {
|
||||
logger.severe(e.getMessage());
|
||||
view = allRecipes.stream()
|
||||
.filter(r -> config.isFavourite(r.getId()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
recipeList.getItems().setAll(view);
|
||||
|
||||
// restore selection if possible
|
||||
if (selectedId != null) {
|
||||
Recipe match = view.stream()
|
||||
|
|
@ -463,6 +452,114 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
this.recipeDetailController.refreshFavouriteButton();
|
||||
this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty());
|
||||
}
|
||||
|
||||
//Delete Ingredient button click
|
||||
@FXML
|
||||
private void handleDeleteIngredient() {
|
||||
// Get selected ingredient
|
||||
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
||||
|
||||
if (selectedIngredient == null) {
|
||||
// Show an error message if no ingredient is selected
|
||||
showError("No ingredient selected", "Please select an ingredient to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the ingredient is used in any recipe
|
||||
checkIngredientUsage(selectedIngredient);
|
||||
}
|
||||
|
||||
// Check if ingredient is used in any recipe before deleting
|
||||
private void checkIngredientUsage(Ingredient ingredient) {
|
||||
try {
|
||||
long usageCount = server.getIngredientUsage(ingredient.getId()); // Check ingredient usage via ServerUtils
|
||||
|
||||
if (usageCount > 0) {
|
||||
// If ingredient is used, show a warning dialog
|
||||
showWarningDialog(ingredient, usageCount);
|
||||
} else {
|
||||
// If not used, delete
|
||||
deleteIngredient(ingredient);
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Error", "Failed to check ingredient usage: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteIngredient(Ingredient ingredient) {
|
||||
try {
|
||||
server.deleteIngredient(ingredient.getId()); // Call ServerUtils to delete the ingredient
|
||||
showConfirmation("Success", "Ingredient '" + ingredient.getName() + "' has been deleted.");
|
||||
refreshIngredientList(); // refresh the ingredient list
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Error", "Failed to delete ingredient: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void showWarningDialog(Ingredient ingredient, long usedInRecipes) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
}
|
||||
|
||||
private void showError(String title, String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void showConfirmation(String title, String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void refreshIngredientList() {
|
||||
// Refresh
|
||||
ingredientListView.getItems().clear();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@FXML
|
||||
private void handleAddIngredient() {
|
||||
//ask the user for the ingredient name
|
||||
TextInputDialog dialog = new TextInputDialog();
|
||||
dialog.setTitle("Add Ingredient");
|
||||
dialog.setHeaderText("Enter the ingredient name:");
|
||||
dialog.setContentText("Ingredient:");
|
||||
|
||||
// Wait for the user to enter a value
|
||||
Optional<String> result = dialog.showAndWait();
|
||||
|
||||
result.ifPresent(name -> {
|
||||
// Create a new Ingredient object
|
||||
Ingredient newIngredient = new Ingredient();
|
||||
newIngredient.setName(name);
|
||||
|
||||
// Add the new ingredient to the ListView
|
||||
ingredientListView.getItems().add(newIngredient);
|
||||
});
|
||||
}
|
||||
|
||||
// display ingredient name
|
||||
@FXML
|
||||
private void initializeIngredients() {
|
||||
ingredientListView.setCellFactory(list -> new ListCell<Ingredient>() {
|
||||
@Override
|
||||
protected void updateItem(Ingredient item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(item.getName()); // Display the ingredient name in the ListView
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void openIngredientsPopup() {
|
||||
try {
|
||||
|
|
@ -514,14 +611,6 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
updatedBadgeTimer.playFromStart();
|
||||
}
|
||||
|
||||
public void openShoppingListWindow() throws IOException {
|
||||
var root = UI.getFXML().load(ShoppingListCtrl.class, "client", "scenes", "shopping", "ShoppingList.fxml");
|
||||
Stage stage = new Stage();
|
||||
stage.setTitle(this.getLocaleString("menu.shopping.title"));
|
||||
stage.setScene(new Scene(root.getValue()));
|
||||
stage.show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package client.scenes.Ingredient;
|
||||
|
||||
import client.exception.DuplicateIngredientException;
|
||||
import client.scenes.nutrition.NutritionDetailsCtrl;
|
||||
import client.utils.LocaleAware;
|
||||
import client.utils.LocaleManager;
|
||||
|
|
@ -67,8 +66,8 @@ public class IngredientListCtrl implements LocaleAware {
|
|||
return this.localeManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeComponents() {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
ingredientListView.setCellFactory(list -> new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(Ingredient item, boolean empty) {
|
||||
|
|
@ -114,8 +113,6 @@ public class IngredientListCtrl implements LocaleAware {
|
|||
refresh(); // reload list from server
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Failed to create ingredient: " + e.getMessage());
|
||||
} catch (DuplicateIngredientException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package client.scenes;
|
||||
|
||||
import client.utils.Config;
|
||||
import client.utils.LocaleAware;
|
||||
import client.utils.LocaleManager;
|
||||
import com.google.inject.Inject;
|
||||
|
|
@ -47,7 +46,7 @@ public class LangSelectMenuCtrl implements LocaleAware {
|
|||
|
||||
@Override
|
||||
public void initializeComponents() {
|
||||
langSelectMenu.getItems().setAll(Config.languages);
|
||||
langSelectMenu.getItems().setAll("en", "pl", "nl", "tok", "tr");
|
||||
langSelectMenu.setValue(manager.getLocale().getLanguage());
|
||||
langSelectMenu.setConverter(new StringConverter<String>() {
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package client.scenes;
|
||||
|
||||
import client.utils.Config;
|
||||
import client.utils.ConfigService;
|
||||
import client.utils.LocaleAware;
|
||||
import client.utils.LocaleManager;
|
||||
|
|
@ -55,7 +54,7 @@ public class LanguageFilterCtrl implements LocaleAware {
|
|||
public void initializeComponents() {
|
||||
var items = this.langFilterMenu.getItems();
|
||||
|
||||
final List<String> languages = List.of(Config.languages);
|
||||
final List<String> languages = List.of("en", "nl", "pl", "tok", "tr");
|
||||
this.selectedLanguages = this.configService.getConfig().getRecipeLanguages();
|
||||
this.updateMenuButtonDisplay();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,15 +10,11 @@ import javafx.animation.PauseTransition;
|
|||
import javafx.concurrent.Task;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Controller for the search bar component.
|
||||
|
|
@ -103,11 +99,7 @@ public class SearchBarCtrl implements LocaleAware {
|
|||
currentSearchTask = new Task<>() {
|
||||
@Override
|
||||
protected List<Recipe> call() throws IOException, InterruptedException {
|
||||
var recipes = serverUtils.getRecipesFiltered(
|
||||
"",
|
||||
configService.getConfig().getRecipeLanguages()
|
||||
);
|
||||
return applyMultiTermAndFilter(recipes, filter);
|
||||
return serverUtils.getRecipesFiltered(filter, configService.getConfig().getRecipeLanguages());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -154,61 +146,8 @@ public class SearchBarCtrl implements LocaleAware {
|
|||
});
|
||||
|
||||
this.searchField.setOnKeyReleased(event -> {
|
||||
if (event.getCode() == KeyCode.ESCAPE) {
|
||||
searchField.clear();
|
||||
this.onSearch();
|
||||
return;
|
||||
}
|
||||
// This cancels the current debounce timer and restarts it.
|
||||
this.searchDebounce.playFromStart();
|
||||
});
|
||||
}
|
||||
private List<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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
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,7 +8,6 @@ import com.google.inject.Inject;
|
|||
import commons.FormalIngredient;
|
||||
import commons.Recipe;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
|
|
@ -79,13 +78,7 @@ public class IngredientListCtrl implements LocaleAware {
|
|||
if (recipe == null) {
|
||||
this.ingredients = FXCollections.observableArrayList(new ArrayList<>());
|
||||
} else {
|
||||
List<RecipeIngredient> ingredientList = recipe
|
||||
.getIngredients()
|
||||
.stream()
|
||||
.sorted(Comparator.comparing(ingredient -> ingredient
|
||||
.getIngredient()
|
||||
.getName()))
|
||||
.toList();
|
||||
List<RecipeIngredient> ingredientList = recipe.getIngredients();
|
||||
this.ingredients = FXCollections.observableArrayList(ingredientList);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package client.scenes.recipe;
|
||||
|
||||
import client.utils.server.ServerUtils;
|
||||
import client.exception.DuplicateIngredientException;
|
||||
import commons.Ingredient;
|
||||
import jakarta.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
|
|
@ -68,15 +67,8 @@ public class IngredientsPopupCtrl {
|
|||
server.createIngredient(name); // calls POST /api/ingredients
|
||||
refresh(); // reload list from server
|
||||
} catch (IOException | InterruptedException e) {
|
||||
|
||||
showError("Failed to create ingredient: " + e.getMessage());
|
||||
} catch (DuplicateIngredientException e) {
|
||||
showError("An ingredient with the name " + name + " already exists." +
|
||||
" Please provide a different name."); //checks if error received has the DUPLICATE string and creates a showError instance with an appropriate error message and description
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package client.scenes.recipe;
|
|||
|
||||
import client.exception.UpdateException;
|
||||
import client.scenes.FoodpalApplicationCtrl;
|
||||
import client.scenes.nutrition.NutritionPieChartCtrl;
|
||||
import client.service.ShoppingListService;
|
||||
import client.utils.Config;
|
||||
import client.utils.ConfigService;
|
||||
import client.utils.LocaleAware;
|
||||
|
|
@ -12,7 +10,6 @@ import client.utils.PrintExportService;
|
|||
import client.utils.server.ServerUtils;
|
||||
import client.utils.WebSocketDataService;
|
||||
import com.google.inject.Inject;
|
||||
import commons.FormalIngredient;
|
||||
import commons.Recipe;
|
||||
|
||||
import java.io.File;
|
||||
|
|
@ -23,10 +20,7 @@ import java.util.function.BiConsumer;
|
|||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
|
|
@ -40,8 +34,6 @@ import javafx.scene.layout.HBox;
|
|||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* Controller for the recipe detail view.
|
||||
|
|
@ -54,12 +46,9 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
private final FoodpalApplicationCtrl appCtrl;
|
||||
private final ConfigService configService;
|
||||
private final WebSocketDataService<Long, Recipe> webSocketDataService;
|
||||
private final ShoppingListService shoppingListService;
|
||||
|
||||
public Spinner<Double> scaleSpinner;
|
||||
public Label inferredKcalLabel;
|
||||
public Spinner<Integer> servingsSpinner;
|
||||
public Label inferredServeSizeLabel;
|
||||
|
||||
@FXML
|
||||
private IngredientListCtrl ingredientListController;
|
||||
|
|
@ -76,14 +65,12 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
ServerUtils server,
|
||||
FoodpalApplicationCtrl appCtrl,
|
||||
ConfigService configService,
|
||||
ShoppingListService listService,
|
||||
WebSocketDataService<Long, Recipe> webSocketDataService) {
|
||||
this.localeManager = localeManager;
|
||||
this.server = server;
|
||||
this.appCtrl = appCtrl;
|
||||
this.configService = configService;
|
||||
this.webSocketDataService = webSocketDataService;
|
||||
this.shoppingListService = listService;
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
|
@ -107,9 +94,6 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
@FXML
|
||||
private ComboBox<String> langSelector;
|
||||
|
||||
@FXML
|
||||
private NutritionPieChartCtrl pieChartController;
|
||||
|
||||
private ListView<Recipe> getParentRecipeList() {
|
||||
return this.appCtrl.recipeList;
|
||||
}
|
||||
|
|
@ -140,6 +124,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
*
|
||||
* @throws IOException Upon invalid recipe response.
|
||||
* @throws InterruptedException Upon request interruption.
|
||||
*
|
||||
* @see FoodpalApplicationCtrl#refresh()
|
||||
*/
|
||||
private void refresh() throws IOException, InterruptedException {
|
||||
|
|
@ -172,31 +157,24 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
|
||||
// If there is a scale
|
||||
// Prevents issues from first startup
|
||||
if (scaleSpinner.getValue() != null && servingsSpinner.getValue() != null) {
|
||||
if (scaleSpinner.getValue() != null) {
|
||||
Double scale = scaleSpinner.getValue();
|
||||
// see impl. creates a scaled context for the recipe such that its non-scaled value is kept as a reference.
|
||||
this.recipeView = new ScalableRecipeView(recipe, scale);
|
||||
// TODO i18n
|
||||
inferredKcalLabel.textProperty().bind(Bindings.createStringBinding(() ->
|
||||
String.format("Inferred %.1f kcal/100g for this recipe",
|
||||
Double.isNaN(this.recipeView.scaledKcalProperty().get()) ?
|
||||
0.0 : this.recipeView.scaledKcalProperty().get())
|
||||
String.format("Inferred %.1f kcal/100g for this recipe",
|
||||
Double.isNaN(this.recipeView.scaledKcalProperty().get()) ?
|
||||
0.0 : this.recipeView.scaledKcalProperty().get())
|
||||
, this.recipeView.scaledKcalProperty()));
|
||||
recipeView.servingsProperty().set(servingsSpinner.getValue());
|
||||
inferredServeSizeLabel.textProperty().bind(Bindings.createStringBinding(
|
||||
() -> String.format("Inferred size per serving: %.1f g", recipeView.servingSizeProperty().get()),
|
||||
recipeView.servingSizeProperty()));
|
||||
// expose the scaled view to list controllers
|
||||
this.ingredientListController.refetchFromRecipe(this.recipeView.getScaled());
|
||||
this.stepListController.refetchFromRecipe(this.recipeView.getScaled());
|
||||
this.pieChartController.setRecipe(recipe);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ingredientListController.refetchFromRecipe(recipe);
|
||||
this.stepListController.refetchFromRecipe(recipe);
|
||||
|
||||
this.pieChartController.setRecipe(recipe);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -207,7 +185,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
* @param recipeConsumer The helper function to use when updating the recipe -
|
||||
* how to update it
|
||||
* @return The created callback to use in a setUpdateCallback or related
|
||||
* function
|
||||
* function
|
||||
*/
|
||||
private <T> Consumer<T> createUpdateRecipeCallback(BiConsumer<Recipe, T> recipeConsumer) {
|
||||
return recipes -> {
|
||||
|
|
@ -347,7 +325,6 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
PrintExportService.exportToFile(recipeText, dirPath, filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the favourite status of the currently viewed recipe in the
|
||||
* application configuration and writes the changes to disk.
|
||||
|
|
@ -425,47 +402,6 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
// triggers a UI update each time the spinner changes to a different value.
|
||||
setCurrentlyViewedRecipe(recipe);
|
||||
});
|
||||
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();
|
||||
langSelector.getItems().addAll("en", "nl", "pl", "tok");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,15 @@ import commons.Recipe;
|
|||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
public class ScalableRecipeView {
|
||||
private final ObjectProperty<Recipe> recipe = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<Recipe> scaled = new SimpleObjectProperty<>();
|
||||
private final DoubleProperty scale = new SimpleDoubleProperty();
|
||||
private final DoubleProperty scaledKcal = new SimpleDoubleProperty();
|
||||
private final IntegerProperty servings = new SimpleIntegerProperty();
|
||||
private final DoubleProperty servingSize = new SimpleDoubleProperty();
|
||||
private final SimpleDoubleProperty scaledKcal = new SimpleDoubleProperty();
|
||||
public ScalableRecipeView(
|
||||
Recipe recipe,
|
||||
Double scale
|
||||
|
|
@ -28,10 +24,10 @@ public class ScalableRecipeView {
|
|||
this.recipe, this.scale);
|
||||
this.scaled.bind(binding);
|
||||
this.scaledKcal.bind(Bindings.createDoubleBinding(() -> this.scaled.get().kcal(), this.scaled));
|
||||
this.servingSize.bind(Bindings.createDoubleBinding(
|
||||
() -> this.scaled.get().weight() * ( 1.0 / this.servings.get()),
|
||||
this.servings)
|
||||
);
|
||||
}
|
||||
|
||||
public double getScale() {
|
||||
return scale.get();
|
||||
}
|
||||
|
||||
public Recipe getRecipe() {
|
||||
|
|
@ -42,14 +38,23 @@ public class ScalableRecipeView {
|
|||
return scaled.get();
|
||||
}
|
||||
|
||||
public DoubleProperty scaledKcalProperty() {
|
||||
return scaledKcal;
|
||||
public double getScaledKcal() {
|
||||
return scaledKcal.get();
|
||||
}
|
||||
|
||||
public IntegerProperty servingsProperty() {
|
||||
return servings;
|
||||
public DoubleProperty scaleProperty() {
|
||||
return scale;
|
||||
}
|
||||
public DoubleProperty servingSizeProperty() {
|
||||
return servingSize;
|
||||
|
||||
public ObjectProperty<Recipe> scaledProperty() {
|
||||
return scaled;
|
||||
}
|
||||
|
||||
public ObjectProperty<Recipe> recipeProperty() {
|
||||
return recipe;
|
||||
}
|
||||
|
||||
public SimpleDoubleProperty scaledKcalProperty() {
|
||||
return scaledKcal;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,244 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue