refactor(client): client-side refactor to new modelling

This commit is contained in:
Zhongheng Liu 2025-12-22 03:00:22 +02:00
commit f04bbc037e
9 changed files with 147 additions and 49 deletions

View file

@ -11,7 +11,7 @@ import client.scenes.recipe.RecipeDetailCtrl;
import client.utils.Config;
import client.utils.ConfigService;
import client.utils.DefaultRecipeFactory;
import client.utils.DefaultValueFactory;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import client.utils.ServerUtils;
@ -217,7 +217,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
*/
@FXML
private void addRecipe() {
Recipe newRecipe = DefaultRecipeFactory.getDefaultRecipe(); // Create default recipe
Recipe newRecipe = DefaultValueFactory.getDefaultRecipe(); // Create default recipe
try {
newRecipe = server.addRecipe(newRecipe); // get the new recipe id
refresh(); // refresh view with server recipes

View file

@ -2,6 +2,7 @@ package client.scenes.nutrition;
import client.scenes.FoodpalApplicationCtrl;
import com.google.inject.Inject;
import commons.Ingredient;
import commons.Recipe;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
@ -14,12 +15,12 @@ import java.util.Set;
public class NutritionViewCtrl {
private ObservableList<Recipe> recipes;
private HashMap<String, Integer> ingredientStats;
public ListView<String> nutritionIngredientsView;
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:
@ -29,13 +30,15 @@ public class NutritionViewCtrl {
* 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
* @param recipeList The recipe list
*/
private void updateIngredientStats(
List<Recipe> recipeList
) {
recipeList.forEach(recipe -> {
Set<String> uniqueIngredients = new HashSet<>(recipe.getIngredients());
Set<Ingredient> uniqueIngredients = new HashSet<>(
recipe.getIngredients().stream().map(
ingredient -> ingredient.ingredient).toList());
nutritionIngredientsView.getItems().setAll(uniqueIngredients);
uniqueIngredients.forEach(ingredient -> {
ingredientStats.put(ingredient, 0);

View file

@ -1,32 +1,71 @@
package client.scenes.recipe;
import commons.FormalIngredient;
import commons.Ingredient;
import commons.RecipeIngredient;
import commons.Unit;
import commons.VagueIngredient;
import javafx.collections.FXCollections;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import java.util.Optional;
/**
* A custom {@link OrderedEditableListCell<String>} for displaying and editing ingredients in an
* IngredientList. Allows inline editing of ingredient names.
* A custom {@link OrderedEditableListCell<String>} for
* displaying and editing ingredients in an IngredientList.
* Allows inline editing of ingredient names.
*
* @see IngredientListCtrl
*/
public class IngredientListCell extends OrderedEditableListCell<String> {
public class IngredientListCell extends OrderedEditableListCell<RecipeIngredient> {
@Override
public void commitEdit(String newValue) {
public void commitEdit(RecipeIngredient newValue) {
super.commitEdit(newValue);
}
/**
* Starts an edit box on the ingredient.
* It needs an {@link HBox HBox} container to fit 3 input boxes.
* @see OrderedEditableListCell
*/
@Override
public void startEdit() {
super.startEdit();
TextField amountInput = new TextField("0");
RecipeIngredient ingredient = getItem();
Optional<Double> templateAmount = Optional.empty();
Optional<Unit> unit = Optional.empty();
// Initialize the input fields with some initial data if we get a formal ingredient
if (ingredient.getClass().equals(FormalIngredient.class)) {
FormalIngredient formal = (FormalIngredient) ingredient;
templateAmount = Optional.of(formal.getAmount());
unit = Optional.of(Unit.fromString(formal.getUnitSuffix()).orElseThrow(
() -> new RuntimeException("FormalIngredient whereas invalid unit")));
}
// TODO initialize some other data in the case of vague ingredient
// Initialize the input boxes
TextField amountInput = new TextField(templateAmount.map(Object::toString).orElse(null));
ChoiceBox<Unit> unitChoice = new ChoiceBox<>();
unitChoice.setItems(FXCollections.observableArrayList(Unit.GRAM, Unit.KILOGRAM));
TextField nameInput = new TextField("ingredient");
// initialize the current unit if it is present
unit.ifPresent(unitChoice::setValue);
unitChoice.setItems(FXCollections.observableArrayList(Unit.GRAMME, Unit.KILOGRAMME, Unit.TONNE, Unit.INFORMAL));
// calls makeInputLine to create the inline edit container
HBox container = makeInputLine(ingredient, amountInput, unitChoice);
// set the graphic to the edit box
this.setText(null);
this.setGraphic(container);
}
private HBox makeInputLine(RecipeIngredient ingredient, TextField amountInput, ChoiceBox<Unit> unitChoice) {
TextField nameInput = new TextField(ingredient.ingredient.name);
HBox container = new HBox(amountInput, unitChoice, nameInput);
container.setOnKeyReleased(event -> {
if (event.getCode() != KeyCode.ENTER) {
@ -34,15 +73,16 @@ public class IngredientListCell extends OrderedEditableListCell<String> {
}
Unit unit = unitChoice.getValue();
String name = nameInput.getText();
if (unit == null) {
if (unit == null || !unit.isFormal()) {
String desc = amountInput.getText();
commitEdit(desc + " " + name);
Ingredient newIngredient = new Ingredient(name, 0., 0., 0.);
commitEdit(new VagueIngredient(newIngredient, desc));
return;
}
int amount = Integer.parseInt(amountInput.getText());
commitEdit(amount + " " + unit + " of " + name);
double amount = Double.parseDouble(amountInput.getText());
Ingredient newIngredient = new Ingredient(name, 0., 0., 0.);
commitEdit(new FormalIngredient(newIngredient, amount, unit.suffix));
});
this.setText(null);
this.setGraphic(container);
return container;
}
}

View file

@ -1,5 +1,6 @@
package client.scenes.recipe;
import client.utils.DefaultValueFactory;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import com.google.inject.Inject;
@ -7,6 +8,8 @@ import commons.Recipe;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import commons.RecipeIngredient;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
@ -40,15 +43,15 @@ public class IngredientListCtrl implements LocaleAware {
* changes and update the recipe accordingly.
* </p>
*/
private ObservableList<String> ingredients;
private ObservableList<RecipeIngredient> ingredients;
/**
* A callback function that is called when the ingredient list is updated.
*/
private Consumer<List<String>> updateCallback;
private Consumer<List<RecipeIngredient>> updateCallback;
@FXML
public ListView<String> ingredientListView;
public ListView<RecipeIngredient> ingredientListView;
@FXML
public Label ingredientsLabel;
@ -69,7 +72,7 @@ public class IngredientListCtrl implements LocaleAware {
if (recipe == null) {
this.ingredients = FXCollections.observableArrayList(new ArrayList<>());
} else {
List<String> ingredientList = recipe.getIngredients();
List<RecipeIngredient> ingredientList = recipe.getIngredients();
this.ingredients = FXCollections.observableArrayList(ingredientList);
}
@ -82,7 +85,7 @@ public class IngredientListCtrl implements LocaleAware {
*
* @param callback The function to call upon each update.
*/
public void setUpdateCallback(Consumer<List<String>> callback) {
public void setUpdateCallback(Consumer<List<RecipeIngredient>> callback) {
this.updateCallback = callback;
}
@ -99,7 +102,7 @@ public class IngredientListCtrl implements LocaleAware {
* @param event The action event.
*/
private void handleIngredientAdd(ActionEvent event) {
this.ingredients.add("Ingredient " + (this.ingredients.size() + 1));
this.ingredients.add(DefaultValueFactory.getDefaultFormalIngredient());
this.refresh();
this.updateCallback.accept(this.ingredients);
@ -112,9 +115,9 @@ public class IngredientListCtrl implements LocaleAware {
*
* @param event The edit event.
*/
private void handleIngredientEdit(EditEvent<String> event) {
private void handleIngredientEdit(EditEvent<RecipeIngredient> event) {
int index = event.getIndex();
String newValue = event.getNewValue();
RecipeIngredient newValue = event.getNewValue();
this.ingredients.set(index, newValue);
this.refresh();
@ -154,9 +157,6 @@ public class IngredientListCtrl implements LocaleAware {
@Override
public void initializeComponents() {
// TODO: set up communication with the server
// this would probably be best done with the callback (so this class doesn't
// interact with the server at all)
this.ingredientListView.setEditable(true);
this.ingredientListView.setCellFactory(
list -> {

View file

@ -2,6 +2,12 @@ package client.scenes.recipe;
import javafx.scene.control.ListCell;
/**
* The abstract class that pre-defines common features between
* {@link IngredientListCell IngredientListCell} and
* {@link RecipeStepListCell RecipeStepListCell}.
* @param <DataType> The type of data the list cell contains.
*/
public abstract class OrderedEditableListCell<DataType> extends ListCell<DataType> {
/**
* Get the display text for the given item, prefixed with its index.
@ -23,6 +29,10 @@ public abstract class OrderedEditableListCell<DataType> extends ListCell<DataTyp
this.setText(this.getDisplayText(item.toString()));
}
}
/**
* A method stub that should be supplemented by child classes.
*/
public void startEdit() {
super.startEdit();
}

View file

@ -6,12 +6,14 @@ import javafx.scene.input.KeyCode;
/**
* A custom ListCell for displaying and editing ingredients in an
* RecipeStepList. Allows inline editing of ingredient names.
*
* The RecipeStepList only needs one {@link TextField TextField} element.
* @see IngredientListCtrl
* @see RecipeStepListCtrl
*/
public class RecipeStepListCell extends OrderedEditableListCell<String> {
@Override
public void startEdit() {
super.startEdit();
TextField textField = new TextField(this.getItem());
// Commit edit on Enter key press

View file

@ -1,15 +0,0 @@
package client.utils;
import commons.Recipe;
import java.util.List;
public class DefaultRecipeFactory {
public static Recipe getDefaultRecipe() {
return new Recipe(
null,
"Untitled recipe",
List.of(),
List.of());
}
}

View file

@ -0,0 +1,57 @@
package client.utils;
import commons.FormalIngredient;
import commons.Ingredient;
import commons.Recipe;
import commons.Unit;
import commons.VagueIngredient;
import java.util.List;
/**
* A factory for default values used in the client UX flow.
* @see DefaultValueFactory#getDefaultRecipe()
* @see DefaultValueFactory#getDefaultFormalIngredient()
* @see DefaultValueFactory#getDefaultVagueIngredient()
*/
public class DefaultValueFactory {
/**
* Instantiates a recipe with a default name and null-ID.
* The ID is <code>null</code> such that it can be updated
* as appropriate on the backend.
* @return The default recipe.
*/
public static Recipe getDefaultRecipe() {
return new Recipe(
null,
"Untitled recipe",
List.of(),
List.of());
}
/**
* Instantiates a default formal ingredient with a default name and default ID.
* Note in particular the ID being <code>0L</code>, see IngredientController
* server-side implementation for more details on safeguards.
* @return The default formal ingredient.
*/
public static FormalIngredient getDefaultFormalIngredient() {
return new FormalIngredient(
new Ingredient(0L, "default ingredient", 0., 0., 0.),
0.0,
Unit.GRAMME.suffix
);
}
/**
* Instantiates a vague ingredient. Not currently in use.
* @see DefaultValueFactory#getDefaultFormalIngredient()
* @return The vague ingredient.
*/
public static VagueIngredient getDefaultVagueIngredient() {
return new VagueIngredient(
new Ingredient(null,
"default ingredient", 0., 0., 0.),
"Some");
}
}

View file

@ -3,6 +3,7 @@ package client.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import commons.Recipe;
import commons.RecipeIngredient;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.ClientBuilder;
import org.glassfish.jersey.client.ClientConfig;
@ -170,8 +171,8 @@ public class ServerUtils {
}
public void addRecipeIngredient(Recipe recipe, String ingredient) throws IOException, InterruptedException {
List<String> ingredients = new ArrayList<>(recipe.getIngredients());
public void addRecipeIngredient(Recipe recipe, RecipeIngredient ingredient) throws IOException, InterruptedException {
List<RecipeIngredient> ingredients = new ArrayList<>(recipe.getIngredients());
ingredients.add(ingredient);
recipe.setIngredients(ingredients);