Merge branch 'feature/client-shopping-list-view' into 'main'

Client shopping list view

Closes #79

See merge request cse1105/2025-2026/teams/csep-team-76!84
This commit is contained in:
Zhongheng Liu 2026-01-23 00:31:57 +01:00
commit 186e74ddd3
13 changed files with 225 additions and 0 deletions

View file

@ -21,6 +21,9 @@ import client.scenes.nutrition.NutritionDetailsCtrl;
import client.scenes.nutrition.NutritionViewCtrl; import client.scenes.nutrition.NutritionViewCtrl;
import client.scenes.recipe.IngredientListCtrl; import client.scenes.recipe.IngredientListCtrl;
import client.scenes.recipe.RecipeStepListCtrl; import client.scenes.recipe.RecipeStepListCtrl;
import client.service.NonFunctionalShoppingListService;
import client.service.ShoppingListService;
import client.service.ShoppingListViewModel;
import client.utils.ConfigService; import client.utils.ConfigService;
import client.utils.LocaleManager; import client.utils.LocaleManager;
import client.utils.server.ServerUtils; import client.utils.server.ServerUtils;
@ -57,6 +60,8 @@ public class MyModule implements Module {
binder.bind(new TypeLiteral<WebSocketDataService<Long, Recipe>>() {}).toInstance( binder.bind(new TypeLiteral<WebSocketDataService<Long, Recipe>>() {}).toInstance(
new WebSocketDataService<>() new WebSocketDataService<>()
); );
binder.bind(ShoppingListViewModel.class).toInstance(new ShoppingListViewModel());
binder.bind(ShoppingListService.class).to(NonFunctionalShoppingListService.class);
binder.bind(new TypeLiteral<WebSocketDataService<Long, Ingredient>>() {}).toInstance( binder.bind(new TypeLiteral<WebSocketDataService<Long, Ingredient>>() {}).toInstance(
new WebSocketDataService<>() new WebSocketDataService<>()
); );

View file

@ -34,6 +34,9 @@ import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.fxml.FXML; 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.Alert;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
@ -41,6 +44,7 @@ import javafx.scene.control.ListCell;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleButton;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@ -510,6 +514,15 @@ public class FoodpalApplicationCtrl implements LocaleAware {
updatedBadgeTimer.playFromStart(); updatedBadgeTimer.playFromStart();
} }
public void openShoppingListWindow() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("shopping/ShoppingList.fxml"));
Parent root = (Parent)loader.load();
Stage stage = new Stage();
stage.setTitle(this.getLocaleString("menu.shopping.title"));
stage.setScene(new Scene(root));
stage.show();
}
} }

View file

@ -0,0 +1,66 @@
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 javafx.fxml.FXML;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Pair;
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.setCellFactory(l -> new ListCell<>() {
@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);
}
});
this.shoppingListView.getItems().setAll(
this.shopping.getItems()
);
}
}

View file

@ -0,0 +1,62 @@
package client.service;
import com.google.inject.Inject;
import commons.FormalIngredient;
import commons.Recipe;
import javafx.util.Pair;
import org.apache.commons.lang3.NotImplementedException;
import java.util.List;
import java.util.Optional;
public class NonFunctionalShoppingListService extends ShoppingListService {
@Inject
public NonFunctionalShoppingListService(ShoppingListViewModel viewModel) {
super(viewModel);
}
@Override
public void putIngredient(FormalIngredient ingredient) {
throw new NotImplementedException();
}
@Override
public void putIngredient(FormalIngredient ingredient, Recipe recipe) {
throw new NotImplementedException();
}
@Override
public void putIngredient(FormalIngredient ingredient, String recipeName) {
throw new NotImplementedException();
}
@Override
public void putArbitraryItem(String name) {
throw new NotImplementedException();
}
@Override
public FormalIngredient purgeIngredient(Long id) {
throw new NotImplementedException();
}
@Override
public FormalIngredient purgeIngredient(String ingredientName) {
throw new NotImplementedException();
}
@Override
public void reset() {
throw new NotImplementedException();
}
@Override
public List<Pair<FormalIngredient, Optional<String>>> getItems() {
throw new NotImplementedException();
}
@Override
public String makePrintable() {
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,29 @@
package client.service;
import com.google.inject.Inject;
import commons.FormalIngredient;
import commons.Recipe;
import javafx.util.Pair;
import java.util.List;
import java.util.Optional;
public abstract class ShoppingListService {
private ShoppingListViewModel viewModel;
@Inject
public ShoppingListService(ShoppingListViewModel viewModel) {
this.viewModel = viewModel;
}
public abstract void putIngredient(FormalIngredient ingredient);
public abstract void putIngredient(FormalIngredient ingredient, Recipe recipe);
public abstract void putIngredient(FormalIngredient ingredient, String recipeName);
public abstract void putArbitraryItem(String name);
public abstract FormalIngredient purgeIngredient(Long id);
public abstract FormalIngredient purgeIngredient(String ingredientName);
public abstract void reset();
public abstract List<Pair<FormalIngredient, Optional<String>>> getItems();
public abstract String makePrintable();
}

View file

@ -0,0 +1,22 @@
package client.service;
import commons.FormalIngredient;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.util.Pair;
import org.apache.commons.lang3.NotImplementedException;
import java.util.Optional;
public class ShoppingListViewModel {
/**
* The formal ingredient provides the ingredient and its amount,
* and the string (optional) describes the recipe where it came from.
*/
private final ListProperty<Pair<FormalIngredient, Optional<String>>> listItems = new SimpleListProperty<>(FXCollections.observableArrayList());
public void addArbitrary() {
throw new NotImplementedException();
}
}

View file

@ -70,6 +70,9 @@
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" /> <ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" />
</HBox> </HBox>
<Button fx:id="shoppingListButton"
onAction="#openShoppingListWindow"
text="Shopping List" />
<Button fx:id="manageIngredientsButton" <Button fx:id="manageIngredientsButton"
onAction="#openIngredientsPopup" onAction="#openIngredientsPopup"
text="Ingredients..." /> text="Ingredients..." />

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.AnchorPane?>
<TitledPane animated="false" collapsible="false" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" text="Shopping List" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/25">
<content>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="200.0">
<children>
<ListView fx:id="shoppingListView" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
</children>
</AnchorPane>
</content>
</TitledPane>

View file

@ -38,6 +38,8 @@ menu.search=Search...
menu.label.selected-langs=Languages menu.label.selected-langs=Languages
menu.shopping.title=Shopping list
lang.en.display=English lang.en.display=English
lang.nl.display=Dutch lang.nl.display=Dutch
lang.pl.display=Polish lang.pl.display=Polish

View file

@ -36,6 +36,8 @@ menu.button.close=Sluiten
menu.label.selected-langs=Talen menu.label.selected-langs=Talen
menu.shopping.title=Boodschappenlijst
menu.search=Zoeken... menu.search=Zoeken...
lang.en.display=Engels lang.en.display=Engels
lang.nl.display=Nederlands lang.nl.display=Nederlands

View file

@ -38,6 +38,8 @@ menu.search=Szukaj...
menu.label.selected-langs=Języki menu.label.selected-langs=Języki
menu.shopping.title=Lista zakupów
lang.en.display=Inglisz lang.en.display=Inglisz
lang.nl.display=Holenderski lang.nl.display=Holenderski
lang.pl.display=Polski lang.pl.display=Polski

View file

@ -38,6 +38,8 @@ menu.search=o alasa
menu.label.selected-langs=toki wile menu.label.selected-langs=toki wile
menu.shopping.title=ijo wile mani mute
lang.en.display=toki Inli lang.en.display=toki Inli
lang.nl.display=toki Netelan lang.nl.display=toki Netelan
lang.pl.display=toki Posuka lang.pl.display=toki Posuka

View file

@ -38,6 +38,8 @@ menu.search=Arama...
menu.label.selected-langs=Diller menu.label.selected-langs=Diller
menu.shopping.title=Al??veri? listesi
lang.en.display=\u0130ngilizce lang.en.display=\u0130ngilizce
lang.nl.display=Hollandaca lang.nl.display=Hollandaca
lang.pl.display=Leh\u00E7e lang.pl.display=Leh\u00E7e