diff --git a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java index d923ffd..cacccb9 100644 --- a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java +++ b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java @@ -24,6 +24,8 @@ 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; @@ -37,6 +39,8 @@ 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. @@ -132,7 +136,6 @@ 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 { @@ -171,9 +174,9 @@ public class RecipeDetailCtrl implements LocaleAware { 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( @@ -197,7 +200,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 Consumer createUpdateRecipeCallback(BiConsumer recipeConsumer) { return recipes -> { @@ -337,6 +340,7 @@ 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,12 +429,36 @@ public class RecipeDetailCtrl implements LocaleAware { langSelector.getItems().addAll(Config.languages); } + @FXML public void handleAddAllToShoppingList(ActionEvent actionEvent) { - System.out.println("handleAddAllToShoppingList"); - // TODO BACKLOG Add overview screen - recipe.getIngredients().stream() - .filter(x -> x.getClass().equals(FormalIngredient.class)) - .map(FormalIngredient.class::cast) - .forEach(x -> shoppingListService.putIngredient(x, recipe)); + 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(); } } diff --git a/client/src/main/java/client/scenes/shopping/AddOverviewCtrl.java b/client/src/main/java/client/scenes/shopping/AddOverviewCtrl.java new file mode 100644 index 0000000..113831b --- /dev/null +++ b/client/src/main/java/client/scenes/shopping/AddOverviewCtrl.java @@ -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 rows = FXCollections.observableArrayList(); + + @FXML + private TableView overviewTable; + + @FXML + private TableColumn nameColumn; + + @FXML + private TableColumn amountColumn; + + @FXML + private TableColumn unitColumn; + + @FXML + public void initialize() { + initializeComponents(); + } + + private static final List 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 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 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 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); + } + } +} diff --git a/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java b/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java index ec08c62..1367936 100644 --- a/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java +++ b/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java @@ -4,6 +4,7 @@ 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; @@ -11,11 +12,14 @@ 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 { @@ -47,10 +51,9 @@ public class ShoppingListCtrl implements LocaleAware { public void initializeComponents() { this.shoppingListView.setEditable(true); this.shoppingListView.setCellFactory(l -> new ShoppingListCell()); - this.shoppingListView.getItems().setAll( - this.shopping.getItems() - ); + this.shoppingListView.setItems(this.shopping.getViewModel().getListItems()); } + private void refreshList() { this.shoppingListView.getItems().setAll( this.shopping.getItems() @@ -63,14 +66,14 @@ public class ShoppingListCtrl implements LocaleAware { "client", "scenes", "shopping", "ShoppingListItemAddModal.fxml"); root.getKey().setNewValueConsumer(fi -> { this.shopping.putIngredient(fi); - refreshList(); + }); stage.setScene(new Scene(root.getValue())); stage.setTitle("My modal window"); stage.initModality(Modality.WINDOW_MODAL); stage.initOwner( ((Node)actionEvent.getSource()).getScene().getWindow() ); - stage.show(); + stage.showAndWait(); } public void handleRemoveItem(ActionEvent actionEvent) { @@ -78,4 +81,39 @@ public class ShoppingListCtrl implements LocaleAware { 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(); + } + } } diff --git a/client/src/main/java/client/service/ShoppingListServiceImpl.java b/client/src/main/java/client/service/ShoppingListServiceImpl.java index 9941ee6..21c16f2 100644 --- a/client/src/main/java/client/service/ShoppingListServiceImpl.java +++ b/client/src/main/java/client/service/ShoppingListServiceImpl.java @@ -37,20 +37,45 @@ public class ShoppingListServiceImpl extends ShoppingListService { @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) { - FormalIngredient fi = getViewModel().getListItems().stream() - .filter(i -> - i.getKey().getIngredient().getName().equals(ingredientName)) - .findFirst().orElseThrow(NullPointerException::new).getKey(); + 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; } @@ -66,6 +91,34 @@ public class ShoppingListServiceImpl extends ShoppingListService { @Override public String makePrintable() { - return "TODO"; + StringBuilder sb = new StringBuilder(); + + for (var item : getViewModel().getListItems()) { + FormalIngredient ingredient = item.getKey(); + Optional 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(); } } diff --git a/client/src/main/resources/client/scenes/shopping/AddOverview.fxml b/client/src/main/resources/client/scenes/shopping/AddOverview.fxml new file mode 100644 index 0000000..7fb31f5 --- /dev/null +++ b/client/src/main/resources/client/scenes/shopping/AddOverview.fxml @@ -0,0 +1,32 @@ + + + + + + + + + +