Merge branch 'AddOverview' into 'main'
last points of 4.5 Closes #83 and #81 See merge request cse1105/2025-2026/teams/csep-team-76!92
This commit is contained in:
commit
3e8442e517
6 changed files with 419 additions and 22 deletions
|
|
@ -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 {
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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,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>
|
||||
|
|
@ -18,6 +18,8 @@
|
|||
<HBox>
|
||||
<Button onAction="#handleAddItem">Add</Button>
|
||||
<Button onAction="#handleRemoveItem">Delete</Button>
|
||||
<Button onAction="#handlePrint">Print</Button>
|
||||
<Button onAction="#handleReset">Reset</Button>
|
||||
</HBox>
|
||||
</VBox>
|
||||
</TitledPane>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue