Merge branch 'fix/favourites-rendering' into 'main'

fix: favourites rendering properly after a list change propagated from server.

See merge request cse1105/2025-2026/teams/csep-team-76!89
This commit is contained in:
Zhongheng Liu 2026-01-22 23:21:48 +01:00
commit bfe6505c54

View file

@ -2,12 +2,10 @@ package client.scenes;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors;
import client.exception.InvalidModificationException; import client.exception.InvalidModificationException;
import client.scenes.nutrition.NutritionViewCtrl; import client.scenes.nutrition.NutritionViewCtrl;
@ -21,7 +19,6 @@ import client.utils.LocaleManager;
import client.utils.server.ServerUtils; import client.utils.server.ServerUtils;
import client.utils.WebSocketDataService; import client.utils.WebSocketDataService;
import client.utils.WebSocketUtils; import client.utils.WebSocketUtils;
import commons.Ingredient;
import commons.Recipe; import commons.Recipe;
import commons.ws.Topics; import commons.ws.Topics;
@ -32,13 +29,16 @@ import commons.ws.messages.Message;
import commons.ws.messages.UpdateRecipeMessage; import commons.ws.messages.UpdateRecipeMessage;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML; import javafx.fxml.FXML;
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;
import javafx.scene.control.ListCell; import javafx.scene.control.ListCell;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleButton;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.NotImplementedException;
@ -66,8 +66,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
@FXML @FXML
public ListView<Recipe> recipeList; public ListView<Recipe> recipeList;
@FXML private final ListProperty<Recipe> favouriteRecipeList = new SimpleListProperty<>();
private ListView<Ingredient> ingredientListView;
@FXML @FXML
private Button addRecipeButton; private Button addRecipeButton;
@ -88,8 +87,6 @@ public class FoodpalApplicationCtrl implements LocaleAware {
@FXML @FXML
private Button manageIngredientsButton; private Button manageIngredientsButton;
private List<Recipe> allRecipes = new ArrayList<>();
@FXML @FXML
private Label updatedBadge; private Label updatedBadge;
@ -287,6 +284,13 @@ public class FoodpalApplicationCtrl implements LocaleAware {
openSelectedRecipe(); openSelectedRecipe();
} }
}); });
recipeList.getItems().addListener((ListChangeListener.Change<? extends Recipe> c) -> {
favouriteRecipeList.set(
FXCollections.observableList(
recipeList.getItems().stream().filter(r -> config.isFavourite(r.getId())).toList()
));
System.out.println(favouriteRecipeList);
});
this.initializeSearchBar(); this.initializeSearchBar();
refresh(); refresh();
@ -325,8 +329,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
logger.severe(msg); logger.severe(msg);
printError(msg); printError(msg);
} }
recipeList.getItems().setAll(recipes);
allRecipes = new ArrayList<>(recipes);
applyRecipeFilterAndKeepSelection(); applyRecipeFilterAndKeepSelection();
showUpdatedBadge(); showUpdatedBadge();
@ -422,16 +425,20 @@ public class FoodpalApplicationCtrl implements LocaleAware {
public void applyRecipeFilterAndKeepSelection() { public void applyRecipeFilterAndKeepSelection() {
Recipe selected = recipeList.getSelectionModel().getSelectedItem(); Recipe selected = recipeList.getSelectionModel().getSelectedItem();
Long selectedId = selected == null ? null : selected.getId(); Long selectedId = selected == null ? null : selected.getId();
List<Recipe> view = recipeList.getItems().stream().toList();
List<Recipe> view = allRecipes;
if (favouritesOnlyToggle != null && favouritesOnlyToggle.isSelected()) { if (favouritesOnlyToggle != null && favouritesOnlyToggle.isSelected()) {
view = allRecipes.stream() view = favouriteRecipeList.get();
.filter(r -> config.isFavourite(r.getId())) recipeList.getItems().setAll(view);
.collect(Collectors.toList()); }
try {
if (favouritesOnlyToggle != null && !favouritesOnlyToggle.isSelected()) {
recipeList.getItems().setAll(server.getRecipes(config.getRecipeLanguages()));
}
}
catch (IOException | InterruptedException e) {
logger.severe(e.getMessage());
} }
recipeList.getItems().setAll(view);
// restore selection if possible // restore selection if possible
if (selectedId != null) { if (selectedId != null) {
@ -452,119 +459,6 @@ public class FoodpalApplicationCtrl implements LocaleAware {
this.recipeDetailController.refreshFavouriteButton(); this.recipeDetailController.refreshFavouriteButton();
this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty()); this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty());
} }
//Delete Ingredient button click
@FXML
private void handleDeleteIngredient() {
// Get selected ingredient
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
if (selectedIngredient == null) {
// Show an error message if no ingredient is selected
showError("No ingredient selected", "Please select an ingredient to delete.");
return;
}
// Check if the ingredient is used in any recipe
checkIngredientUsage(selectedIngredient);
}
// Check if ingredient is used in any recipe before deleting
private void checkIngredientUsage(Ingredient ingredient) {
try {
long usageCount = server.getIngredientUsage(ingredient.getId()); // Check ingredient usage via ServerUtils
if (usageCount > 0) {
// If ingredient is used, show a warning dialog
showWarningDialog(ingredient, usageCount);
} else {
// If not used, delete
deleteIngredient(ingredient);
}
} catch (IOException | InterruptedException e) {
showError("Error", "Failed to check ingredient usage: " + e.getMessage());
}
}
private void deleteIngredient(Ingredient ingredient) {
try {
server.deleteIngredient(ingredient.getId()); // Call ServerUtils to delete the ingredient
showConfirmation("Success", "Ingredient '" + ingredient.getName() + "' has been deleted.");
refreshIngredientList(); // refresh the ingredient list
} catch (IOException | InterruptedException e) {
showError("Error", "Failed to delete ingredient: " + e.getMessage());
}
}
private void showWarningDialog(Ingredient ingredient, long usedInRecipes) {
Alert alert = new Alert(Alert.AlertType.WARNING);
}
private void showError(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
private void showConfirmation(String title, String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
private void refreshIngredientList() {
// Refresh
ingredientListView.getItems().clear();
}
@FXML
private void handleAddIngredient() {
//ask the user for the ingredient name
TextInputDialog dialog = new TextInputDialog();
dialog.setTitle("Add Ingredient");
dialog.setHeaderText("Enter the ingredient name:");
dialog.setContentText("Ingredient:");
// Wait for the user to enter a value
Optional<String> result = dialog.showAndWait();
result.ifPresent(recipeName ->{
String trim = recipeName.trim();
if(trim.isEmpty()){
showError("Invalid Input", "Cannot have empty ingredient name");
return;
}
if(ingredientListView.getItems().stream()
.map(Ingredient::getName)
.anyMatch(ingName -> ingName.equalsIgnoreCase(trim))){
showError("Duplicate Ingredient", "Cannot have duplicate ingredients," +
" Please provide unique Ingredient names");
}
});
}
// display ingredient name
@FXML
private void initializeIngredients() {
ingredientListView.setCellFactory(list -> new ListCell<Ingredient>() {
@Override
protected void updateItem(Ingredient item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.getName()); // Display the ingredient name in the ListView
}
}
});
}
@FXML @FXML
private void openIngredientsPopup() { private void openIngredientsPopup() {
try { try {