chore: etc

This commit is contained in:
Zhongheng Liu 2026-01-16 23:41:47 +01:00
commit 895aecba3d
Signed by: steven
GPG key ID: F69B980899C1C09D
19 changed files with 404 additions and 55 deletions

View file

@ -128,6 +128,11 @@
<artifactId>logback-classic</artifactId>
<version>1.5.20</version> <!-- or latest compatible version -->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
</dependencies>

View file

@ -15,6 +15,7 @@
*/
package client;
import client.model.ApplicationDataViewModel;
import client.scenes.FoodpalApplicationCtrl;
import client.scenes.SearchBarCtrl;
import client.scenes.nutrition.NutritionDetailsCtrl;
@ -58,5 +59,6 @@ public class MyModule implements Module {
binder.bind(new TypeLiteral<WebSocketDataService<Long, Ingredient>>() {}).toInstance(
new WebSocketDataService<>()
);
binder.bind(ApplicationDataViewModel.class).in(Scopes.SINGLETON);
}
}

View file

@ -0,0 +1,41 @@
package client.model;
import commons.Recipe;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import java.util.List;
import java.util.Optional;
public class ApplicationDataViewModel {
private final ListProperty<RecipeViewModel> recipes = new SimpleListProperty<>();
private final ListProperty<IngredientViewModel> ingredients = new SimpleListProperty<>();
public ApplicationDataViewModel() {}
public ApplicationDataViewModel(ObservableList<RecipeViewModel> recipes, ObservableList<IngredientViewModel> ingredients) {
this.recipes.set(recipes);
this.ingredients.set(ingredients);
}
public Optional<RecipeViewModel> getRecipeViewById(Long id) {
return recipes.get().stream().filter(r -> r.getId().equals(id)).findFirst();
}
public Optional<IngredientViewModel> getIngredientViewById(Long id) {
return ingredients.get().stream().filter(i -> i.idProperty().get() == id).findFirst();
}
public ObservableList<RecipeViewModel> getRecipes() {
return recipes.get();
}
public ObservableList<IngredientViewModel> getIngredients() {
return ingredients.get();
}
public void setRecipes(List<RecipeViewModel> recipes) {
this.recipes.setAll(recipes);
}
public void setIngredients(List<IngredientViewModel> ingredients) {
this.ingredients.setAll(ingredients);
}
}

View file

@ -0,0 +1,6 @@
package client.model;
public abstract class BaseViewModel<T> {
public abstract void updateFrom(T item);
public abstract T into();
}

View file

@ -0,0 +1,66 @@
package client.model;
import commons.FormalIngredient;
import commons.RecipeIngredient;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class FormalIngredientViewModel extends RecipeIngredientViewModel<FormalIngredient> {
private final LongProperty id = new SimpleLongProperty();
private final ObjectProperty<IngredientViewModel> ingredientView = new SimpleObjectProperty<>();
private final DoubleProperty amount = new SimpleDoubleProperty();
private final StringProperty unitSuffix = new SimpleStringProperty();
private final DoubleProperty kcal = new SimpleDoubleProperty();
public FormalIngredientViewModel(FormalIngredient ingredient) {
updateFrom(ingredient);
kcal.bind(Bindings.createDoubleBinding(() -> {
System.out.println(ingredientView.get().getKcal());
System.out.println("evaluating kcal: " + ingredientView.get().getKcal() * amount.get());
return ingredientView.get().getKcal() * amount.get();
}));
}
@Override
public void updateFrom(FormalIngredient item) {
id.set(item.getId());
ingredientView.set(new IngredientViewModel(item.getIngredient()));
amount.set(item.getAmount());
unitSuffix.set(item.getUnitSuffix());
}
@Override
public FormalIngredient into() {
return new FormalIngredient();
}
@Override
public RecipeIngredient getReal() {
return null;
}
@Override
public RecipeIngredient getScaled() {
return null;
}
@Override
public void setReal(RecipeIngredient real) {
}
@Override
public void setScale(Number scale) {
}
@Override
public DoubleProperty kcalProperty() {
return kcal;
}
}

View file

@ -1,4 +1,4 @@
package client.Ingredient;
package client.model;
import commons.Ingredient;
import javafx.beans.binding.Bindings;
@ -12,7 +12,7 @@ import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class IngredientViewModel {
public class IngredientViewModel extends BaseViewModel<Ingredient> {
private final LongProperty id = new SimpleLongProperty();
private final StringProperty name = new SimpleStringProperty();
private final DoubleProperty protein = new SimpleDoubleProperty();
@ -22,7 +22,7 @@ public class IngredientViewModel {
private final ReadOnlyDoubleWrapper kcal = new ReadOnlyDoubleWrapper();
public IngredientViewModel() {
DoubleBinding computeKcal = Bindings.createDoubleBinding(
() -> toIngredient().kcalPer100g(),
() -> into().kcalPer100g(),
protein, carbs, fat
);
@ -61,7 +61,7 @@ public class IngredientViewModel {
return carbs;
}
public Ingredient toIngredient() {
public Ingredient into() {
Ingredient i = new Ingredient();
i.setId(id.get());
i.setName(name.get());

View file

@ -0,0 +1,9 @@
package client.model;
import commons.RecipeIngredient;
import javafx.beans.property.DoubleProperty;
public abstract class RecipeIngredientViewModel<T>
extends BaseViewModel<T> implements ScaleAware<RecipeIngredient> {
public abstract DoubleProperty kcalProperty();
}

View file

@ -0,0 +1,112 @@
package client.model;
import commons.FormalIngredient;
import commons.Recipe;
import commons.RecipeIngredient;
import commons.VagueIngredient;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.List;
public class RecipeViewModel extends BaseViewModel<Recipe> implements ScaleAware<Recipe> {
// Immutable
private final Long id;
// Mutable values
private final StringProperty name = new SimpleStringProperty();
private final StringProperty locale = new SimpleStringProperty();
private final ListProperty<RecipeIngredientViewModel<?>> ingredients = new SimpleListProperty<>(FXCollections.observableArrayList(
item -> new Observable[] { item.kcalProperty() }
));
private final ObjectProperty<ObservableList<String>> steps = new SimpleObjectProperty<>();
private final DoubleProperty kcal = new SimpleDoubleProperty();
private final DoubleProperty scale = new SimpleDoubleProperty();
public RecipeViewModel(Recipe recipe) {
this.id = recipe.getId();
this.kcal.bind(Bindings.createDoubleBinding(() -> {
List<RecipeIngredientViewModel<?>> list = ingredients.get();
System.out.println(list);
if (list == null || list.isEmpty()) {
return 0.0;
}
System.out.println("Compiled kcal double binding");
return list.stream().mapToDouble(i -> i.kcalProperty().get()).sum();
}, ingredients));
updateFrom(recipe);
}
@Override
public void updateFrom(Recipe item) {
name.setValue(item.getName());
locale.setValue(item.getLocale());
List<RecipeIngredientViewModel<?>> list = item.getIngredients().stream().<RecipeIngredientViewModel<?>>map(recipeIngredient -> switch (recipeIngredient) {
case FormalIngredient f -> new FormalIngredientViewModel(f);
case VagueIngredient v -> new VagueIngredientViewModel(v);
default -> throw new IllegalStateException("Unexpected value: " + recipeIngredient);
}).toList();
ingredients.setAll(list);
steps.setValue(FXCollections.observableList(item.getPreparationSteps()));
}
@Override
public Recipe into() {
return new Recipe(
id, name.getValue(), locale.getValue(),
ingredients.stream().map(i -> (RecipeIngredient) i.into()).toList(),
steps.getValue());
}
@Override
public Recipe getReal() {
return into();
}
public Long getId() {
return id;
}
@Override
public Recipe getScaled() {
return into().scaleBy(scale.get());
}
@Override
public void setReal(Recipe real) {
updateFrom(real);
}
@Override
public void setScale(Number scale) {
this.scale.setValue(scale);
}
public StringProperty nameProperty() {
return name;
}
public StringProperty localeProperty() {
return locale;
}
public ListProperty<RecipeIngredientViewModel<?>> ingredientsProperty() {
return ingredients;
}
public ObjectProperty<ObservableList<String>> stepsProperty() {
return steps;
}
public DoubleProperty kcalProperty() {
return kcal;
}
public DoubleProperty scaleProperty() {
return scale;
}
}

View file

@ -0,0 +1,12 @@
package client.model;
/**
* An interface describing a value that has a real and scaled counterpart.
* @param <T>
*/
public interface ScaleAware<T> {
T getReal();
T getScaled();
void setReal(T real);
void setScale(Number scale);
}

View file

@ -0,0 +1,47 @@
package client.model;
import commons.RecipeIngredient;
import commons.VagueIngredient;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
public class VagueIngredientViewModel extends RecipeIngredientViewModel<VagueIngredient> {
private final SimpleObjectProperty<VagueIngredient> vagueIngredient = new SimpleObjectProperty<>();
public VagueIngredientViewModel(VagueIngredient ingredient) {
this.vagueIngredient.set(ingredient);
}
@Override
public void updateFrom(VagueIngredient item) {
}
@Override
public VagueIngredient into() {
return vagueIngredient.get();
}
@Override
public DoubleProperty kcalProperty() {
return null;
}
@Override
public RecipeIngredient getReal() {
return null;
}
@Override
public RecipeIngredient getScaled() {
return null;
}
@Override
public void setReal(RecipeIngredient real) {
}
@Override
public void setScale(Number scale) {
}
}

View file

@ -10,6 +10,8 @@ import java.util.logging.Logger;
import java.util.stream.Collectors;
import client.exception.InvalidModificationException;
import client.model.ApplicationDataViewModel;
import client.model.RecipeViewModel;
import client.scenes.nutrition.NutritionViewCtrl;
import client.scenes.recipe.RecipeDetailCtrl;
@ -51,7 +53,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
private final LocaleManager localeManager;
private final WebSocketDataService<Long, Recipe> dataService;
private final Logger logger = Logger.getLogger(FoodpalApplicationCtrl.class.getName());
private final ApplicationDataViewModel applicationDataViewModel;
@FXML
private RecipeDetailCtrl recipeDetailController;
@ -98,7 +100,8 @@ public class FoodpalApplicationCtrl implements LocaleAware {
WebSocketUtils webSocketUtils,
LocaleManager localeManager,
ConfigService configService,
WebSocketDataService<Long, Recipe> recipeDataService
WebSocketDataService<Long, Recipe> recipeDataService,
ApplicationDataViewModel applicationDataViewModel
) {
this.server = server;
this.webSocketUtils = webSocketUtils;
@ -106,6 +109,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
this.configService = configService;
this.dataService = recipeDataService;
this.applicationDataViewModel = applicationDataViewModel;
setupDataService();
logger.info("WebSocket processor initialized.");
@ -290,6 +294,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
searchBarController.getFilter(),
this.configService.getConfig().getRecipeLanguages()
);
applicationDataViewModel.setRecipes(recipes.stream().map(RecipeViewModel::new).toList());
} catch (IOException | InterruptedException e) {
recipes = Collections.emptyList();
String msg = "Failed to load recipes: " + e.getMessage();

View file

@ -1,6 +1,7 @@
package client.scenes.nutrition;
import client.Ingredient.IngredientViewModel;
import client.model.ApplicationDataViewModel;
import client.model.IngredientViewModel;
import client.exception.IllegalInputFormatException;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
@ -32,6 +33,7 @@ public class NutritionDetailsCtrl implements LocaleAware {
private final ServerUtils server;
private final SimpleObjectProperty<IngredientViewModel> ingredient =
new SimpleObjectProperty<>(new IngredientViewModel());
private ApplicationDataViewModel applicationDataViewModel;
private final Logger logger = Logger.getLogger(this.getClass().getName());
public Label ingredientName;
@ -51,10 +53,12 @@ public class NutritionDetailsCtrl implements LocaleAware {
@Inject
public NutritionDetailsCtrl(
LocaleManager manager,
ServerUtils server
ServerUtils server,
ApplicationDataViewModel applicationDataViewModel
) {
this.manager = manager;
this.server = server;
this.applicationDataViewModel = applicationDataViewModel;
}
@Override
@ -77,6 +81,9 @@ public class NutritionDetailsCtrl implements LocaleAware {
Ingredient newIngredient = server.updateIngredient(updateIngredient());
Platform.runLater(() -> {
this.ingredient.get().updateFrom(newIngredient);
applicationDataViewModel.getIngredientViewById(newIngredient.getId())
.orElseThrow(() -> new RuntimeException("error!"))
.updateFrom(newIngredient);
logger.info("Updated ingredient to " + newIngredient);
});
} catch (IllegalInputFormatException e) {
@ -106,7 +113,7 @@ public class NutritionDetailsCtrl implements LocaleAware {
setVisible(true);
}
private Ingredient updateIngredient() throws IllegalInputFormatException {
Ingredient current = this.ingredient.get().toIngredient();
Ingredient current = this.ingredient.get().into();
try {
double f = Double.parseDouble(this.fatInputElement.getText());
double p = Double.parseDouble(this.proteinInputElement.getText());

View file

@ -1,6 +1,8 @@
package client.scenes.recipe;
import client.exception.UpdateException;
import client.model.ApplicationDataViewModel;
import client.model.RecipeViewModel;
import client.scenes.FoodpalApplicationCtrl;
import client.utils.Config;
import client.utils.ConfigService;
@ -18,6 +20,8 @@ import java.nio.file.Path;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
@ -32,8 +36,8 @@ import javafx.scene.text.Font;
import javafx.stage.DirectoryChooser;
/**
* Controller for the recipe detail view.
* Manages displaying and editing recipe details such as name, ingredients, and
* Controller for the recipeViewModel detail view.
* Manages displaying and editing recipeViewModel details such as name, ingredients, and
* steps.
*/
public class RecipeDetailCtrl implements LocaleAware {
@ -42,6 +46,8 @@ public class RecipeDetailCtrl implements LocaleAware {
private final FoodpalApplicationCtrl appCtrl;
private final ConfigService configService;
private final WebSocketDataService<Long, Recipe> webSocketDataService;
private final ApplicationDataViewModel applicationDataViewModel;
public Label kcalLabel;
@FXML
private IngredientListCtrl ingredientListController;
@ -49,7 +55,7 @@ public class RecipeDetailCtrl implements LocaleAware {
@FXML
private RecipeStepListCtrl stepListController;
private Recipe recipe;
private SimpleObjectProperty<RecipeViewModel> recipeViewModel = new SimpleObjectProperty<>();
@Inject
public RecipeDetailCtrl(
@ -57,12 +63,14 @@ public class RecipeDetailCtrl implements LocaleAware {
ServerUtils server,
FoodpalApplicationCtrl appCtrl,
ConfigService configService,
WebSocketDataService<Long, Recipe> webSocketDataService) {
WebSocketDataService<Long, Recipe> webSocketDataService,
ApplicationDataViewModel applicationDataViewModel) {
this.localeManager = localeManager;
this.server = server;
this.appCtrl = appCtrl;
this.configService = configService;
this.webSocketDataService = webSocketDataService;
this.applicationDataViewModel = applicationDataViewModel;
}
@FXML
@ -92,9 +100,9 @@ public class RecipeDetailCtrl implements LocaleAware {
/**
* Convenience method for a frequently needed operation to retrieve the
* currently selected recipe.
* currently selected recipeViewModel.
*
* @return The currently selected recipe in the parent recipe list.
* @return The currently selected recipeViewModel in the parent recipeViewModel list.
*/
private Optional<Recipe> getSelectedRecipe() {
return Optional.ofNullable(
@ -112,19 +120,19 @@ public class RecipeDetailCtrl implements LocaleAware {
}
/**
* Refreshes the recipe list from the server.
* Refreshes the recipeViewModel list from the server.
*
* @throws IOException Upon invalid recipe response.
* @throws IOException Upon invalid recipeViewModel response.
* @throws InterruptedException Upon request interruption.
*
* @see FoodpalApplicationCtrl#refresh()
*/
private void refresh() throws IOException, InterruptedException {
public void refresh() throws IOException, InterruptedException {
this.appCtrl.refresh();
}
/**
* Toggles the visibility of the recipe details screen.
* Toggles the visibility of the recipeViewModel details screen.
*
* @param visible Whether the details screen should be visible.
*/
@ -133,17 +141,16 @@ public class RecipeDetailCtrl implements LocaleAware {
}
/**
* Sets the currently viewed recipe in the detail view.
* Sets the currently viewed recipeViewModel in the detail view.
*
* @param recipe The recipe to view.
* @param recipe The recipeViewModel to view.
*/
public void setCurrentlyViewedRecipe(Recipe recipe) {
if (recipe == null) {
return;
}
this.recipe = recipe;
this.recipeViewModel.set(new RecipeViewModel(recipe));
this.showName(recipe.getName());
this.ingredientListController.refetchFromRecipe(recipe);
this.stepListController.refetchFromRecipe(recipe);
@ -152,11 +159,11 @@ public class RecipeDetailCtrl implements LocaleAware {
}
/**
* Create a callback that takes in the selected recipe and a helper type
* and updates the recipe based on that.
* Create a callback that takes in the selected recipeViewModel and a helper type
* and updates the recipeViewModel based on that.
* Also, posts the update to the server.
*
* @param recipeConsumer The helper function to use when updating the recipe -
* @param recipeConsumer The helper function to use when updating the recipeViewModel -
* how to update it
* @return The created callback to use in a setUpdateCallback or related
* function
@ -165,7 +172,7 @@ public class RecipeDetailCtrl implements LocaleAware {
return recipes -> {
Recipe selectedRecipe = this.getSelectedRecipe().orElseThrow(
() -> new NullPointerException(
"Null recipe whereas ingredients are edited"));
"Null recipeViewModel whereas ingredients are edited"));
recipeConsumer.accept(selectedRecipe, recipes);
@ -174,9 +181,10 @@ public class RecipeDetailCtrl implements LocaleAware {
webSocketDataService.add(selectedRecipe.getId(), recipe -> {
int idx = getParentRecipeList().getItems().indexOf(selectedRecipe);
getParentRecipeList().getItems().set(idx, recipe);
recipeViewModel.get().updateFrom(recipe);
});
} catch (IOException | InterruptedException e) {
throw new UpdateException("Unable to update recipe to server for " +
throw new UpdateException("Unable to update recipeViewModel to server for " +
selectedRecipe);
}
};
@ -190,15 +198,16 @@ public class RecipeDetailCtrl implements LocaleAware {
// Initialize callback for ingredient list updates
this.ingredientListController.setUpdateCallback(
this.createUpdateRecipeCallback(Recipe::setIngredients));
this.createUpdateRecipeCallback(Recipe::setIngredients)
);
// Ditto, for step list updates
this.stepListController.setUpdateCallback(
this.createUpdateRecipeCallback(Recipe::setPreparationSteps));
}
/**
* Revised edit recipe control flow, deprecates the use of a separate
* AddNameCtrl. This is automagically called when a new recipe is created,
* Revised edit recipeViewModel control flow, deprecates the use of a separate
* AddNameCtrl. This is automagically called when a new recipeViewModel is created,
* making for a more seamless UX.
* Public for reference by ApplicationCtrl.
*/
@ -213,14 +222,15 @@ public class RecipeDetailCtrl implements LocaleAware {
}
String newName = edit.getText();
this.recipe.setName(newName);
this.recipeViewModel.get().nameProperty().setValue(newName);
Recipe r = this.recipeViewModel.get().into();
System.out.println(r.toDetailedString());
try {
server.updateRecipe(this.recipe);
server.updateRecipe(r);
// this.refresh();
} catch (IOException | InterruptedException e) {
// throw a nice blanket UpdateException
throw new UpdateException("Error occurred when updating recipe name!");
throw new UpdateException("Error occurred when updating recipeViewModel name!");
}
this.showName(edit.getText());
@ -231,7 +241,7 @@ public class RecipeDetailCtrl implements LocaleAware {
}
/**
* Remove the selected recipe from the server and refresh the recipe list.
* Remove the selected recipeViewModel from the server and refresh the recipeViewModel list.
* Internally calls {@link FoodpalApplicationCtrl#removeSelectedRecipe()}.
*/
@FXML
@ -241,17 +251,17 @@ public class RecipeDetailCtrl implements LocaleAware {
/**
* Refreshes the favourite button state based on whether the currently viewed
* recipe is marked as a favourite in the application configuration.
* recipeViewModel is marked as a favourite in the application configuration.
*/
public void refreshFavouriteButton() {
if (recipe == null) {
if (recipeViewModel == null || recipeViewModel.get() == null) {
favouriteButton.setDisable(true);
favouriteButton.setText("");
return;
}
favouriteButton.setDisable(false);
favouriteButton.setText(this.getConfig().isFavourite(recipe.getId()) ? ""
favouriteButton.setText(this.getConfig().isFavourite(recipeViewModel.get().getReal().getId()) ? ""
: "");
}
@ -262,8 +272,8 @@ public class RecipeDetailCtrl implements LocaleAware {
*/
@FXML
private void printRecipe() {
if (recipe == null) {
return; // Do nothing if no recipe selected
if (recipeViewModel == null) {
return; // Do nothing if no recipeViewModel selected
}
// Open directory chooser
@ -278,9 +288,9 @@ public class RecipeDetailCtrl implements LocaleAware {
}
// Ask for filename
TextInputDialog dialog = new TextInputDialog(recipe.getName() + ".txt");
TextInputDialog dialog = new TextInputDialog(recipeViewModel.get().getReal().getName() + ".txt");
dialog.setTitle("Save Recipe");
dialog.setHeaderText("Enter filename for the recipe");
dialog.setHeaderText("Enter filename for the recipeViewModel");
dialog.setContentText("Filename:");
Optional<String> result = dialog.showAndWait();
@ -292,18 +302,18 @@ public class RecipeDetailCtrl implements LocaleAware {
}
// Use PrintExportService methods
String recipeText = PrintExportService.buildRecipeText(recipe);
String recipeText = PrintExportService.buildRecipeText(recipeViewModel.get().getReal());
Path dirPath = selectedDirectory.toPath();
PrintExportService.exportToFile(recipeText, dirPath, filename);
}
}
/**
* Toggles the favourite status of the currently viewed recipe in the
* Toggles the favourite status of the currently viewed recipeViewModel in the
* application configuration and writes the changes to disk.
*/
@FXML
private void toggleFavourite() {
long id = this.recipe.getId();
long id = this.recipeViewModel.get().getReal().getId();
Config config = this.getConfig();
if (config.isFavourite(id)) {
@ -337,16 +347,16 @@ public class RecipeDetailCtrl implements LocaleAware {
}
/**
* Switch the recipe's language.
* Switch the recipeViewModel's language.
*/
@FXML
void changeLanguage() {
recipe.setLocale(this.langSelector.getValue());
recipeViewModel.get().getReal().setLocale(this.langSelector.getValue());
try {
server.updateRecipe(this.recipe);
server.updateRecipe(this.recipeViewModel.get().getReal());
} catch (IOException | InterruptedException e) {
throw new UpdateException("Error occurred when updating recipe locale!");
throw new UpdateException("Error occurred when updating recipeViewModel locale!");
}
}
@ -365,7 +375,11 @@ public class RecipeDetailCtrl implements LocaleAware {
@Override
public void initializeComponents() {
initStepsIngredientsList();
recipeViewModel.addListener((_, ov, nv) -> {
if (nv != null) {
kcalLabel.textProperty().bind(recipeViewModel.get().kcalProperty().asString("%.1f kcal"));
}
});
langSelector.getItems().addAll("en", "nl", "pl");
}
}

View file

@ -35,4 +35,5 @@
<!-- Preparation -->
<fx:include source="RecipeStepList.fxml" fx:id="stepList"
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
<Label>Total energy in this recipe: </Label><Label fx:id="kcalLabel" /><Label>kcal</Label>
</VBox>

View file

@ -76,4 +76,9 @@ public class FormalIngredient extends RecipeIngredient implements Scalable<Forma
public int hashCode() {
return Objects.hash(super.hashCode(), amount, unitSuffix);
}
@Override
public double kcal() {
return ingredient.kcalPer100g() * amountInBaseUnit();
}
}

View file

@ -37,7 +37,7 @@ import java.util.Objects;
// TABLE named recipes
@Entity
@Table(name = "recipes")
public class Recipe {
public class Recipe implements Scalable<Recipe> {
// PRIMARY Key, unique id for recipe, generated automatically.
@Id
@ -191,5 +191,16 @@ public class Recipe {
", preparationSteps=" + preparationSteps +
'}';
}
public double kcal() {
return this.ingredients.stream().mapToDouble(RecipeIngredient::kcal).sum();
}
@Override
public Recipe scaleBy(double factor) {
return new Recipe(id, name, locale, ingredients.stream().map(ri -> switch (ri) {
case FormalIngredient f -> f.scaleBy(factor);
case VagueIngredient v -> v;
default -> throw new IllegalArgumentException("Unexpected child class of RecipeIngredient!");
}).toList(), preparationSteps);
}
}

View file

@ -92,4 +92,5 @@ public abstract class RecipeIngredient {
public int hashCode() {
return Objects.hash(id, ingredient);
}
public abstract double kcal();
}

View file

@ -1,8 +1,8 @@
package commons;
public interface Scalable<IngredientType extends RecipeIngredient> {
default IngredientType scaleBy(int numPortions) {
public interface Scalable<T> {
default T scaleBy(int numPortions) {
return scaleBy((double) numPortions);
};
IngredientType scaleBy(double factor);
T scaleBy(double factor);
}

View file

@ -50,4 +50,9 @@ public class VagueIngredient extends RecipeIngredient {
public int hashCode() {
return Objects.hashCode(description);
}
@Override
public double kcal() {
return 0;
}
}