chore: integrate changes from newest main

This commit is contained in:
Zhongheng Liu 2026-01-13 16:37:57 +01:00
commit 6b851767e7
Signed by: steven
GPG key ID: F69B980899C1C09D
10 changed files with 205 additions and 96 deletions

View file

@ -17,6 +17,8 @@ package client;
import client.scenes.FoodpalApplicationCtrl; import client.scenes.FoodpalApplicationCtrl;
import client.scenes.SearchBarCtrl; import client.scenes.SearchBarCtrl;
import client.scenes.nutrition.NutritionDetailsCtrl;
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.utils.ConfigService; import client.utils.ConfigService;
@ -47,7 +49,8 @@ public class MyModule implements Module {
binder.bind(LocaleManager.class).in(Scopes.SINGLETON); binder.bind(LocaleManager.class).in(Scopes.SINGLETON);
binder.bind(ServerUtils.class).in(Scopes.SINGLETON); binder.bind(ServerUtils.class).in(Scopes.SINGLETON);
binder.bind(WebSocketUtils.class).in(Scopes.SINGLETON); binder.bind(WebSocketUtils.class).in(Scopes.SINGLETON);
binder.bind(NutritionDetailsCtrl.class).in(Scopes.SINGLETON);
binder.bind(NutritionViewCtrl.class).in(Scopes.SINGLETON);
binder.bind(ConfigService.class).toInstance(new ConfigService(Path.of("config.json"))); binder.bind(ConfigService.class).toInstance(new ConfigService(Path.of("config.json")));
binder.bind(new TypeLiteral<WebSocketDataService<Long, Recipe>>() {}).toInstance( binder.bind(new TypeLiteral<WebSocketDataService<Long, Recipe>>() {}).toInstance(
new WebSocketDataService<>() new WebSocketDataService<>()

View file

@ -10,7 +10,7 @@ import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import client.exception.InvalidModificationException; import client.exception.InvalidModificationException;
import client.scenes.recipe.IngredientsPopupCtrl; import client.scenes.nutrition.NutritionViewCtrl;
import client.scenes.recipe.RecipeDetailCtrl; import client.scenes.recipe.RecipeDetailCtrl;
import client.utils.Config; import client.utils.Config;
@ -524,14 +524,14 @@ public class FoodpalApplicationCtrl implements LocaleAware {
private void openIngredientsPopup() { private void openIngredientsPopup() {
try { try {
var pair = client.UI.getFXML().load( var pair = client.UI.getFXML().load(
IngredientsPopupCtrl.class, NutritionViewCtrl.class,
"client", "scenes", "recipe", "IngredientsPopup.fxml" "client", "scenes", "nutrition", "NutritionView.fxml"
); );
var root = pair.getValue(); var root = pair.getValue();
var stage = new javafx.stage.Stage(); var stage = new javafx.stage.Stage();
stage.setTitle("Ingredients"); stage.setTitle("Nutrition values view");
stage.initModality(javafx.stage.Modality.APPLICATION_MODAL); stage.initModality(javafx.stage.Modality.APPLICATION_MODAL);
stage.setScene(new javafx.scene.Scene(root)); stage.setScene(new javafx.scene.Scene(root));
stage.showAndWait(); stage.showAndWait();

View file

@ -0,0 +1,148 @@
package client.scenes.Ingredient;
import client.scenes.nutrition.NutritionDetailsCtrl;
import client.utils.ServerUtils;
import commons.Ingredient;
import jakarta.inject.Inject;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextInputDialog;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
//TODO and check for capital letter milk and MILK are seen as different
public class IngredientListCtrl {
private final ServerUtils server;
@FXML
private NutritionDetailsCtrl nutritionDetailsCtrl;
@FXML
private ListView<Ingredient> ingredientListView;
@Inject
public IngredientListCtrl(
ServerUtils server,
NutritionDetailsCtrl nutritionDetailsCtrl
) {
this.server = server;
this.nutritionDetailsCtrl = nutritionDetailsCtrl;
}
@FXML
public void initialize() {
ingredientListView.setCellFactory(list -> new ListCell<>() {
@Override
protected void updateItem(Ingredient item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.getName());
}
}
});
ingredientListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
return;
}
nutritionDetailsCtrl.setItem(newValue);
});
refresh();
}
@FXML
private void addIngredient() {
TextInputDialog dialog = new TextInputDialog();
dialog.setTitle("Add Ingredient");
dialog.setHeaderText("Create a new ingredient");
dialog.setContentText("Name:");
Optional<String> result = dialog.showAndWait();
if (result.isEmpty()) {
return;
}
String name = result.get().trim();
if (name.isEmpty()) {
showError("Ingredient name cannot be empty.");
return;
}
try {
server.createIngredient(name); // calls POST /api/ingredients
refresh(); // reload list from server
} catch (IOException | InterruptedException e) {
showError("Failed to create ingredient: " + e.getMessage());
}
}
@FXML
private void refresh() {
try {
List<Ingredient> ingredients = server.getIngredients();
ingredientListView.getItems().setAll(ingredients);
} catch (IOException | InterruptedException e) {
showError("Failed to load ingredients: " + e.getMessage());
}
}
@FXML
private void deleteSelected() {
Ingredient selected = ingredientListView.getSelectionModel().getSelectedItem();
if (selected == null) {
showError("No ingredient selected.");
return;
}
try {
long usageCount = server.getIngredientUsage(selected.getId());
if (usageCount > 0) {
boolean proceed = confirmDeleteUsed(selected.getName(), usageCount);
if (!proceed) {
return;
}
}
server.deleteIngredient(selected.getId());
refresh();
} catch (IOException | InterruptedException e) {
showError("Failed to delete ingredient: " + e.getMessage());
}
}
@FXML
private void close() {
Stage stage = (Stage) ingredientListView.getScene().getWindow();
stage.close();
}
private boolean confirmDeleteUsed(String name, long usedInRecipes) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Warning");
alert.setHeaderText("Ingredient in use");
alert.setContentText("Ingredient '" + name + "' is used in " + usedInRecipes
+ " recipe(s). Delete anyway?");
var delete = new javafx.scene.control.ButtonType("Delete Anyway");
var cancel = new javafx.scene.control.ButtonType("Cancel");
alert.getButtonTypes().setAll(delete, cancel);
return alert.showAndWait().orElse(cancel) == delete;
}
private void showError(String msg) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText(null);
alert.setContentText(msg);
alert.showAndWait();
}
}

View file

@ -3,11 +3,16 @@ package client.scenes.nutrition;
import client.utils.LocaleAware; import client.utils.LocaleAware;
import client.utils.LocaleManager; import client.utils.LocaleManager;
import com.google.inject.Inject; import com.google.inject.Inject;
import commons.Ingredient;
import javafx.fxml.FXML;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
public class NutritionDetailsCtrl implements LocaleAware { public class NutritionDetailsCtrl implements LocaleAware {
private boolean visible;
private final LocaleManager manager; private final LocaleManager manager;
private Ingredient ingredient;
public Label ingredientName; public Label ingredientName;
public Label fatInputLabel; public Label fatInputLabel;
@ -20,6 +25,9 @@ public class NutritionDetailsCtrl implements LocaleAware {
public TextField proteinInputElement; public TextField proteinInputElement;
public TextField carbInputElement; public TextField carbInputElement;
@FXML
public VBox nutritionDetails;
@Inject @Inject
public NutritionDetailsCtrl( public NutritionDetailsCtrl(
LocaleManager manager LocaleManager manager
@ -31,6 +39,22 @@ public class NutritionDetailsCtrl implements LocaleAware {
} }
public void setVisible(boolean isVisible) {
nutritionDetails.setVisible(isVisible);
}
public void toggleVisible() {
nutritionDetails.setVisible(!visible);
visible = !visible;
}
public void setItem(Ingredient ingredient) {
this.ingredient = ingredient;
this.ingredientName.setText(ingredient.getName());
this.fatInputElement.setText(Double.toString(ingredient.getFatPer100g()));
this.proteinInputElement.setText(Double.toString(ingredient.getProteinPer100g()));
this.carbInputElement.setText(Double.toString(ingredient.getCarbsPer100g()));
setVisible(true);
}
@Override @Override
public LocaleManager getLocaleManager() { public LocaleManager getLocaleManager() {
return manager; return manager;

View file

@ -1,71 +1,10 @@
package client.scenes.nutrition; package client.scenes.nutrition;
import client.scenes.FoodpalApplicationCtrl;
import com.google.inject.Inject; import com.google.inject.Inject;
import commons.Ingredient;
import commons.Recipe;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.ListView;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NutritionViewCtrl { public class NutritionViewCtrl {
private ObservableList<Recipe> recipes;
private HashMap<Ingredient, Integer> ingredientStats;
public ListView<Ingredient> nutritionIngredientsView;
private final NutritionDetailsCtrl nutritionDetailsCtrl;
// TODO into Ingredient class definition
// FIXME MOST LIKELY CURRENTLY BROKEN. TO BE FIXED.
/**
* Comedically verbose function to count unique appearances of an ingredient by name in each recipe.
* For each recipe:
* 1. Collect unique ingredients that appeared in that recipe.
* 2. For each unique ingredient in said recipe:
* 1. Initialize the appearance for that ingredient to 0.
* 2. For each recipe in list:
* 1. If the name of the ingredient exists in the recipe list, increment the statistic by 1.
* 2. Else maintain the same value for that statistic.
* @param recipeList The recipe list
*/
private void updateIngredientStats(
List<Recipe> recipeList
) {
recipeList.forEach(recipe -> {
Set<Ingredient> uniqueIngredients = new HashSet<>(
recipe.getIngredients().stream().map(
ingredient -> ingredient.ingredient).toList());
nutritionIngredientsView.getItems().setAll(uniqueIngredients);
uniqueIngredients.forEach(ingredient -> {
ingredientStats.put(ingredient, 0);
recipeList.forEach(r ->
ingredientStats.put(
ingredient,
ingredientStats.get(ingredient) + (
(r.getIngredients().contains(ingredient))
? 1 : 0
)
)
);
});
});
}
@Inject @Inject
public NutritionViewCtrl( public NutritionViewCtrl(
FoodpalApplicationCtrl foodpalApplicationCtrl,
NutritionDetailsCtrl nutritionDetailsCtrl
) { ) {
this.recipes = foodpalApplicationCtrl.recipeList.getItems();
this.recipes.addListener((ListChangeListener<? super Recipe>) _ -> {
updateIngredientStats(this.recipes);
});
this.nutritionDetailsCtrl = nutritionDetailsCtrl;
this.nutritionIngredientsView.selectionModelProperty().addListener((observable, oldValue, newValue) -> {
});
} }
} }

View file

@ -8,7 +8,7 @@
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" <VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="client.scenes.recipe.IngredientsPopupCtrl" fx:controller="client.scenes.Ingredient.IngredientListCtrl"
spacing="10" prefWidth="420" prefHeight="520"> spacing="10" prefWidth="420" prefHeight="520">
<padding> <padding>

View file

@ -7,26 +7,24 @@
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.shape.Line?> <?import javafx.scene.shape.Line?>
<AnchorPane xmlns="http://javafx.com/javafx" <VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="client.scenes.nutrition.NutritionDetailsCtrl" fx:controller="client.scenes.nutrition.NutritionDetailsCtrl"
prefHeight="400.0" prefWidth="600.0"> fx:id="nutritionDetails"
<VBox visible="false"> visible="false">
<Label fx:id="ingredientName" /> <Label fx:id="ingredientName" />
<HBox> <HBox>
<Label fx:id="fatInputLabel">Fat: </Label> <Label fx:id="fatInputLabel">Fat: </Label>
<TextField fx:id="fatInputElement" /> <TextField fx:id="fatInputElement" />
</HBox> </HBox>
<HBox> <HBox>
<Label fx:id="proteinInputLabel">Protein: </Label> <Label fx:id="proteinInputLabel">Protein: </Label>
<TextField fx:id="proteinInputElement" /> <TextField fx:id="proteinInputElement" />
</HBox> </HBox>
<HBox> <HBox>
<Label fx:id="carbInputLabel">Carbohydrates: </Label> <Label fx:id="carbInputLabel">Carbohydrates: </Label>
<TextField fx:id="carbInputElement" /> <TextField fx:id="carbInputElement" />
</HBox> </HBox>
<Label fx:id="estimatedKcalLabel">Estimated: 0kcal</Label> <Label fx:id="estimatedKcalLabel">Estimated: 0kcal</Label>
<Label fx:id="usageLabel">Not used in any recipes</Label> <Label fx:id="usageLabel">Not used in any recipes</Label>
</VBox> </VBox>
</AnchorPane>

View file

@ -11,9 +11,7 @@
fx:controller="client.scenes.nutrition.NutritionViewCtrl" fx:controller="client.scenes.nutrition.NutritionViewCtrl"
prefHeight="400.0" prefWidth="600.0"> prefHeight="400.0" prefWidth="600.0">
<SplitPane> <SplitPane>
<ListView fx:id="nutritionIngredientsView" /> <fx:include source="IngredientList.fxml" />
<AnchorPane> <fx:include source="NutritionDetails.fxml" />
<fx:include source="NutritionDetails.fxml" />
</AnchorPane>
</SplitPane> </SplitPane>
</AnchorPane> </AnchorPane>

View file

@ -30,8 +30,7 @@
<ComboBox fx:id="langSelector" onAction="#changeLanguage" /> <ComboBox fx:id="langSelector" onAction="#changeLanguage" />
<!-- Ingredients --> <!-- Ingredients -->
<fx:include source="IngredientList.fxml" fx:id="ingredientList" <fx:include source="RecipeIngredientList.fxml" fx:id="ingredientList" />
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
<!-- Preparation --> <!-- Preparation -->
<fx:include source="RecipeStepList.fxml" fx:id="stepList" <fx:include source="RecipeStepList.fxml" fx:id="stepList"