diff --git a/client/pom.xml b/client/pom.xml index b9eb186..ebbef30 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -21,7 +21,13 @@ commons 0.0.1-SNAPSHOT - + + + org.slf4j + slf4j-api + 2.0.17 + compile + com.fasterxml.jackson.core jackson-databind diff --git a/client/src/main/java/client/MyModule.java b/client/src/main/java/client/MyModule.java index e40e613..14eaa3e 100644 --- a/client/src/main/java/client/MyModule.java +++ b/client/src/main/java/client/MyModule.java @@ -21,8 +21,10 @@ import client.scenes.nutrition.NutritionDetailsCtrl; import client.scenes.nutrition.NutritionViewCtrl; import client.scenes.recipe.IngredientListCtrl; import client.scenes.recipe.RecipeStepListCtrl; -import client.service.NonFunctionalShoppingListService; +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; @@ -60,8 +62,10 @@ public class MyModule implements Module { binder.bind(new TypeLiteral>() {}).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(NonFunctionalShoppingListService.class); + binder.bind(ShoppingListService.class).to(ShoppingListServiceImpl.class); binder.bind(new TypeLiteral>() {}).toInstance( new WebSocketDataService<>() ); diff --git a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java index ab60161..181279b 100644 --- a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java +++ b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java @@ -7,10 +7,12 @@ import java.util.List; import java.util.Optional; import java.util.logging.Logger; +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; @@ -34,8 +36,6 @@ import javafx.beans.property.SimpleListProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; @@ -515,11 +515,10 @@ public class FoodpalApplicationCtrl implements LocaleAware { } public void openShoppingListWindow() throws IOException { - FXMLLoader loader = new FXMLLoader(getClass().getResource("shopping/ShoppingList.fxml")); - Parent root = (Parent)loader.load(); + 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)); + stage.setScene(new Scene(root.getValue())); stage.show(); } diff --git a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java index f1eeb5d..04140d1 100644 --- a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java +++ b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java @@ -2,6 +2,7 @@ package client.scenes.recipe; import client.exception.UpdateException; import client.scenes.FoodpalApplicationCtrl; +import client.service.ShoppingListService; import client.utils.Config; import client.utils.ConfigService; import client.utils.LocaleAware; @@ -10,6 +11,7 @@ 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; @@ -20,6 +22,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.control.Button; import javafx.scene.control.ComboBox; @@ -46,6 +49,7 @@ public class RecipeDetailCtrl implements LocaleAware { private final FoodpalApplicationCtrl appCtrl; private final ConfigService configService; private final WebSocketDataService webSocketDataService; + private final ShoppingListService shoppingListService; public Spinner scaleSpinner; public Label inferredKcalLabel; @@ -67,12 +71,14 @@ public class RecipeDetailCtrl implements LocaleAware { ServerUtils server, FoodpalApplicationCtrl appCtrl, ConfigService configService, + ShoppingListService listService, WebSocketDataService webSocketDataService) { this.localeManager = localeManager; this.server = server; this.appCtrl = appCtrl; this.configService = configService; this.webSocketDataService = webSocketDataService; + this.shoppingListService = listService; } @FXML @@ -418,4 +424,13 @@ public class RecipeDetailCtrl implements LocaleAware { }); langSelector.getItems().addAll("en", "nl", "pl", "tok"); } + + 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)); + } } diff --git a/client/src/main/java/client/scenes/shopping/ShoppingListCell.java b/client/src/main/java/client/scenes/shopping/ShoppingListCell.java new file mode 100644 index 0000000..d684435 --- /dev/null +++ b/client/src/main/java/client/scenes/shopping/ShoppingListCell.java @@ -0,0 +1,67 @@ +package client.scenes.shopping; + +import client.scenes.recipe.OrderedEditableListCell; +import commons.FormalIngredient; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; +import javafx.util.Pair; + +import java.util.Optional; + +public class ShoppingListCell extends OrderedEditableListCell>> { + private Node makeEditor() { + HBox editor = new HBox(); + Spinner 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> 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> 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> newValue) { + super.commitEdit(newValue); + } + +} diff --git a/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java b/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java index b73ec9d..ec08c62 100644 --- a/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java +++ b/client/src/main/java/client/scenes/shopping/ShoppingListCtrl.java @@ -1,13 +1,19 @@ package client.scenes.shopping; +import client.UI; import client.service.ShoppingListService; import client.utils.LocaleAware; import client.utils.LocaleManager; import com.google.inject.Inject; import commons.FormalIngredient; +import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.ListCell; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; import javafx.scene.control.ListView; +import javafx.stage.Modality; +import javafx.stage.Stage; import javafx.util.Pair; import java.util.Optional; @@ -19,7 +25,6 @@ public class ShoppingListCtrl implements LocaleAware { @FXML private ListView>> shoppingListView; - @Inject public ShoppingListCtrl( ShoppingListService shopping, @@ -40,27 +45,37 @@ public class ShoppingListCtrl implements LocaleAware { } public void initializeComponents() { - this.shoppingListView.setCellFactory(l -> new ListCell<>() { - @Override - protected void updateItem(Pair> 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); - } - }); - + this.shoppingListView.setEditable(true); + this.shoppingListView.setCellFactory(l -> new ShoppingListCell()); this.shoppingListView.getItems().setAll( this.shopping.getItems() ); } + private void refreshList() { + this.shoppingListView.getItems().setAll( + this.shopping.getItems() + ); + } + + public void handleAddItem(ActionEvent actionEvent) { + Stage stage = new Stage(); + Pair root = UI.getFXML().load(ShoppingListNewItemPromptCtrl.class, + "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(); + } + + public void handleRemoveItem(ActionEvent actionEvent) { + var x = this.shoppingListView.getSelectionModel().getSelectedItem(); + this.shopping.getItems().remove(x); + refreshList(); + } } diff --git a/client/src/main/java/client/scenes/shopping/ShoppingListNewItemPromptCtrl.java b/client/src/main/java/client/scenes/shopping/ShoppingListNewItemPromptCtrl.java new file mode 100644 index 0000000..09bd44f --- /dev/null +++ b/client/src/main/java/client/scenes/shopping/ShoppingListNewItemPromptCtrl.java @@ -0,0 +1,96 @@ +package client.scenes.shopping; + +import client.utils.LocaleAware; +import client.utils.LocaleManager; +import client.utils.server.ServerUtils; +import com.google.inject.Inject; +import commons.FormalIngredient; +import commons.Ingredient; +import commons.Unit; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.ActionEvent; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.Function; + +public class ShoppingListNewItemPromptCtrl implements LocaleAware { + public MenuButton ingredientSelection; + private final ObjectProperty selected = new SimpleObjectProperty<>(); + private final ObjectProperty selectedUnit = new SimpleObjectProperty<>(); + private final ServerUtils server; + private final LocaleManager localeManager; + private Consumer newValueConsumer; + public MenuButton unitSelect; + public Spinner amountSelect; + + @Inject + public ShoppingListNewItemPromptCtrl(ServerUtils server, LocaleManager localeManager) { + this.server = server; + this.localeManager = localeManager; + } + + public void setNewValueConsumer(Consumer consumer) { + this.newValueConsumer = consumer; + } + + public void confirmAdd(ActionEvent actionEvent) { + if (selected.get() == null || selectedUnit.get() == null) { + System.err.println("You must select both an ingredient and an unit"); + return; + } + FormalIngredient fi = new FormalIngredient(selected.get(), amountSelect.getValue(), selectedUnit.get().suffix); + newValueConsumer.accept(fi); + Stage stage = (Stage) ingredientSelection.getScene().getWindow(); + stage.close(); + } + + public void cancelAdd(ActionEvent actionEvent) { + } + private void makeMenuItems( + MenuButton menu, + Iterable items, + Function labelMapper, + Consumer onSelect) { + // Iterates over the list of items and applies the label and onSelect handlers. + for (T item : items) { + MenuItem mi = new MenuItem(); + mi.setText(labelMapper.apply(item)); + mi.setOnAction(_ -> { + menu.setText(labelMapper.apply(item)); + onSelect.accept(item); + }); + menu.getItems().add(mi); + } + } + @Override + public void updateText() { + + } + @Override + public void initializeComponents() { + try { + amountSelect.setValueFactory( + new SpinnerValueFactory.DoubleSpinnerValueFactory(0, Double.MAX_VALUE, 0)); + amountSelect.setEditable(true); + makeMenuItems(ingredientSelection, server.getIngredients(), Ingredient::getName, selected::set); + makeMenuItems(unitSelect, + Arrays.stream(Unit.values()).filter(u -> u.formal).toList(), + Unit::toString, selectedUnit::set); + } catch (IOException | InterruptedException e) { + System.err.println(e.getMessage()); + } + } + + @Override + public LocaleManager getLocaleManager() { + return localeManager; + } +} diff --git a/client/src/main/java/client/service/ShoppingListItem.java b/client/src/main/java/client/service/ShoppingListItem.java new file mode 100644 index 0000000..91dd98a --- /dev/null +++ b/client/src/main/java/client/service/ShoppingListItem.java @@ -0,0 +1,6 @@ +package client.service; + +import commons.FormalIngredient; +public class ShoppingListItem { + private FormalIngredient i; +} diff --git a/client/src/main/java/client/service/ShoppingListService.java b/client/src/main/java/client/service/ShoppingListService.java index f757165..04ef26b 100644 --- a/client/src/main/java/client/service/ShoppingListService.java +++ b/client/src/main/java/client/service/ShoppingListService.java @@ -14,6 +14,15 @@ public abstract class ShoppingListService { public ShoppingListService(ShoppingListViewModel viewModel) { this.viewModel = viewModel; } + + public ShoppingListViewModel getViewModel() { + return viewModel; + } + + public void setViewModel(ShoppingListViewModel viewModel) { + this.viewModel = viewModel; + } + public abstract void putIngredient(FormalIngredient ingredient); public abstract void putIngredient(FormalIngredient ingredient, Recipe recipe); public abstract void putIngredient(FormalIngredient ingredient, String recipeName); diff --git a/client/src/main/java/client/service/ShoppingListServiceImpl.java b/client/src/main/java/client/service/ShoppingListServiceImpl.java new file mode 100644 index 0000000..9941ee6 --- /dev/null +++ b/client/src/main/java/client/service/ShoppingListServiceImpl.java @@ -0,0 +1,71 @@ +package client.service; + +import com.google.inject.Inject; +import commons.FormalIngredient; +import commons.Recipe; +import javafx.util.Pair; + +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +public class ShoppingListServiceImpl extends ShoppingListService { + private final Logger logger = Logger.getLogger(ShoppingListServiceImpl.class.getName()); + @Inject + public ShoppingListServiceImpl( + ShoppingListViewModel model + ) { + super(model); + } + + @Override + public void putIngredient(FormalIngredient ingredient) { + getViewModel().getListItems().add(new Pair<>(ingredient, Optional.empty())); + } + + @Override + public void putIngredient(FormalIngredient ingredient, Recipe recipe) { + Pair> val = new Pair<>(ingredient, Optional.of(recipe.getName())); + logger.info("putting ingredients into shopping list: " + val); + getViewModel().getListItems().add(val); + } + + @Override + public void putIngredient(FormalIngredient ingredient, String recipeName) { + getViewModel().getListItems().add(new Pair<>(ingredient, Optional.of(recipeName))); + } + + @Override + public void putArbitraryItem(String name) { + + } + + @Override + public FormalIngredient purgeIngredient(Long id) { + 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(); + return null; + } + + @Override + public void reset() { + getViewModel().getListItems().clear(); + } + + @Override + public List>> getItems() { + return getViewModel().getListItems(); + } + + @Override + public String makePrintable() { + return "TODO"; + } +} diff --git a/client/src/main/java/client/service/ShoppingListViewModel.java b/client/src/main/java/client/service/ShoppingListViewModel.java index 30e03df..aaaa237 100644 --- a/client/src/main/java/client/service/ShoppingListViewModel.java +++ b/client/src/main/java/client/service/ShoppingListViewModel.java @@ -4,6 +4,7 @@ import commons.FormalIngredient; import javafx.beans.property.ListProperty; import javafx.beans.property.SimpleListProperty; import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.util.Pair; import org.apache.commons.lang3.NotImplementedException; @@ -19,4 +20,7 @@ public class ShoppingListViewModel { throw new NotImplementedException(); } + public ObservableList>> getListItems() { + return listItems.get(); + } } diff --git a/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml b/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml index 28ba793..602b827 100644 --- a/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml +++ b/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml @@ -25,6 +25,7 @@ diff --git a/client/src/main/resources/client/scenes/shopping/ShoppingList.fxml b/client/src/main/resources/client/scenes/shopping/ShoppingList.fxml index 314caf3..272e30d 100644 --- a/client/src/main/resources/client/scenes/shopping/ShoppingList.fxml +++ b/client/src/main/resources/client/scenes/shopping/ShoppingList.fxml @@ -4,12 +4,20 @@ - - - - - - - - + + + + + + + + + + + diff --git a/client/src/main/resources/client/scenes/shopping/ShoppingListItemAddModal.fxml b/client/src/main/resources/client/scenes/shopping/ShoppingListItemAddModal.fxml new file mode 100644 index 0000000..aee4c20 --- /dev/null +++ b/client/src/main/resources/client/scenes/shopping/ShoppingListItemAddModal.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + Unit... + Your ingredient... + + + + + + +