Merge remote-tracking branch 'origin/main' into feature/backend-websockets

This commit is contained in:
Natalia Cholewa 2025-12-04 22:52:16 +01:00
commit cfacc37739
37 changed files with 2335 additions and 256 deletions

View file

@ -17,15 +17,14 @@ package client;
import static com.google.inject.Guice.createInjector;
import java.io.IOException;
import java.net.URISyntaxException;
import client.scenes.AddStepsCtrl;
import client.scenes.MainCtrl;
import client.scenes.AddIngredientCtrl;
import client.scenes.AddNameCtrl;
import client.scenes.FoodpalApplicationCtrl;
import com.google.inject.Injector;
import client.scenes.AddQuoteCtrl;
import client.scenes.MainCtrl;
import client.scenes.QuoteOverviewCtrl;
import client.utils.ServerUtils;
import client.utils.ServerUtilsExample;
import javafx.application.Application;
import javafx.stage.Stage;
@ -34,24 +33,26 @@ public class Main extends Application {
private static final Injector INJECTOR = createInjector(new MyModule());
private static final MyFXML FXML = new MyFXML(INJECTOR);
public static void main(String[] args) throws URISyntaxException, IOException {
public static void main(String[] args){
launch();
}
@Override
public void start(Stage primaryStage) throws Exception {
var serverUtils = INJECTOR.getInstance(ServerUtils.class);
var serverUtils = INJECTOR.getInstance(ServerUtilsExample.class);
if (!serverUtils.isServerAvailable()) {
var msg = "Server needs to be started before the client, but it does not seem to be available. Shutting down.";
System.err.println(msg);
return;
}
var overview = FXML.load(QuoteOverviewCtrl.class, "client", "scenes", "QuoteOverview.fxml");
var add = FXML.load(AddQuoteCtrl.class, "client", "scenes", "AddQuote.fxml");
var addName = FXML.load(AddNameCtrl.class, "client", "scenes", "AddName.fxml");
var foodpal = FXML.load(FoodpalApplicationCtrl.class, "client", "scenes", "FoodpalApplication.fxml");
var addIngredient = FXML.load(AddIngredientCtrl.class, "client", "scenes", "AddIngredient.fxml");
var addStep = FXML.load(AddStepsCtrl.class, "client", "scenes", "AddSteps.fxml");
var mainCtrl = INJECTOR.getInstance(MainCtrl.class);
mainCtrl.initialize(primaryStage, overview, add);
mainCtrl.setup(primaryStage, addName, foodpal, addIngredient, addStep);
}
}

View file

@ -15,20 +15,23 @@
*/
package client;
import client.scenes.FoodpalApplicationCtrl;
import client.utils.LocaleManager;
import com.google.inject.Binder;
import com.google.inject.Module;
import com.google.inject.Scopes;
import client.scenes.AddQuoteCtrl;
import client.scenes.AddNameCtrl;
import client.scenes.MainCtrl;
import client.scenes.QuoteOverviewCtrl;
public class MyModule implements Module {
@Override
public void configure(Binder binder) {
binder.bind(MainCtrl.class).in(Scopes.SINGLETON);
binder.bind(AddQuoteCtrl.class).in(Scopes.SINGLETON);
binder.bind(QuoteOverviewCtrl.class).in(Scopes.SINGLETON);
binder.bind(AddNameCtrl.class).in(Scopes.SINGLETON);
binder.bind(FoodpalApplicationCtrl.class).in(Scopes.SINGLETON);
binder.bind(LocaleManager.class).in(Scopes.SINGLETON);
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client.scenes;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import client.utils.ServerUtils;
import com.google.inject.Inject;
import commons.Recipe;
import jakarta.ws.rs.WebApplicationException;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.stage.Modality;
import java.io.IOException;
public class AddIngredientCtrl implements LocaleAware {
private final ServerUtils server;
private final MainCtrl mainCtrl;
private final FoodpalApplicationCtrl foodpalCtrl;
private final LocaleManager localeManager;
@FXML
public TextField ingredient;
@FXML
public Button okButton;
@FXML
public Button cancelButton;
@FXML
public Label ingredientLabel;
@Inject
public AddIngredientCtrl(ServerUtils server, MainCtrl mainCtrl,
FoodpalApplicationCtrl foodpalCtrl, LocaleManager localeManager) {
this.mainCtrl = mainCtrl;
this.server = server;
this.foodpalCtrl = foodpalCtrl;
this.localeManager = localeManager;
}
@Override
public void updateText() {
ingredientLabel.setText(getLocaleString("add.ingredient.label"));
okButton.setText(getLocaleString("button.ok"));
cancelButton.setText(getLocaleString("button.cancel"));
}
@Override
public LocaleManager getLocaleManager() {
return localeManager;
}
public void cancel() throws IOException, InterruptedException {
clearFields();
mainCtrl.showFoodpal();
}
public void ok() throws IOException, InterruptedException {
try {
Recipe selected = foodpalCtrl.getSelectedRecipe();
server.addRecipeIngredient(selected, ingredient.getText());
} catch (WebApplicationException e) {
var alert = new Alert(Alert.AlertType.ERROR);
alert.initModality(Modality.APPLICATION_MODAL);
alert.setContentText(e.getMessage());
alert.showAndWait();
return;
}
clearFields();
mainCtrl.showFoodpal();
}
private void clearFields() {
ingredient.clear();
}
public void keyPressed(KeyEvent e) throws IOException, InterruptedException {
switch (e.getCode()) {
case ENTER:
ok();
break;
case ESCAPE:
cancel();
break;
default:
break;
}
}
}

View file

@ -15,47 +15,68 @@
*/
package client.scenes;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import client.utils.ServerUtils;
import com.google.inject.Inject;
import client.utils.ServerUtils;
import commons.Person;
import commons.Quote;
import jakarta.ws.rs.WebApplicationException;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.stage.Modality;
public class AddQuoteCtrl {
import java.io.IOException;
public class AddNameCtrl implements LocaleAware {
private final ServerUtils server;
private final MainCtrl mainCtrl;
@FXML
private TextField firstName;
private final LocaleManager localeManager;
@FXML
private TextField lastName;
public TextField recipeName;
@FXML
private TextField quote;
public Label recipeNameLabel;
@FXML
public Button cancelButton;
@FXML
public Button okButton;
@Inject
public AddQuoteCtrl(ServerUtils server, MainCtrl mainCtrl) {
public AddNameCtrl(ServerUtils server, MainCtrl mainCtrl, LocaleManager localeManager) {
this.mainCtrl = mainCtrl;
this.server = server;
this.localeManager = localeManager;
}
public void cancel() {
@Override
public void updateText() {
recipeNameLabel.setText(getLocaleString("add.recipe.label"));
okButton.setText(getLocaleString("button.ok"));
cancelButton.setText(getLocaleString("button.cancel"));
}
@Override
public LocaleManager getLocaleManager() {
return localeManager;
}
public void cancel() throws IOException, InterruptedException {
clearFields();
mainCtrl.showOverview();
mainCtrl.showFoodpal();
}
public void ok() {
public void ok() throws IOException, InterruptedException {
try {
server.addQuote(getQuote());
server.addRecipeName(recipeName.getText());
} catch (WebApplicationException e) {
var alert = new Alert(Alert.AlertType.ERROR);
@ -63,25 +84,19 @@ public class AddQuoteCtrl {
alert.setContentText(e.getMessage());
alert.showAndWait();
return;
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
clearFields();
mainCtrl.showOverview();
}
private Quote getQuote() {
var p = new Person(firstName.getText(), lastName.getText());
var q = quote.getText();
return new Quote(p, q);
mainCtrl.showFoodpal();
}
private void clearFields() {
firstName.clear();
lastName.clear();
quote.clear();
recipeName.clear();
}
public void keyPressed(KeyEvent e) {
public void keyPressed(KeyEvent e) throws IOException, InterruptedException {
switch (e.getCode()) {
case ENTER:
ok();
@ -93,4 +108,5 @@ public class AddQuoteCtrl {
break;
}
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client.scenes;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import client.utils.ServerUtils;
import com.google.inject.Inject;
import commons.Recipe;
import jakarta.ws.rs.WebApplicationException;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.stage.Modality;
import java.io.IOException;
public class AddStepsCtrl implements LocaleAware {
private final ServerUtils server;
private final MainCtrl mainCtrl;
private final FoodpalApplicationCtrl foodpalCtrl;
private final LocaleManager localeManager;
@FXML
public TextField preparationStep;
@FXML
public Button okButton;
@FXML
public Button cancelButton;
@FXML
public Label preparationStepLabel;
@Inject
public AddStepsCtrl(ServerUtils server, MainCtrl mainCtrl,
FoodpalApplicationCtrl foodpalCtrl, LocaleManager localeManager) {
this.mainCtrl = mainCtrl;
this.server = server;
this.foodpalCtrl = foodpalCtrl;
this.localeManager = localeManager;
}
@Override
public void updateText() {
preparationStepLabel.setText(getLocaleString("add.step.label"));
okButton.setText(getLocaleString("button.ok"));
cancelButton.setText(getLocaleString("button.cancel"));
}
@Override
public LocaleManager getLocaleManager() {
return localeManager;
}
public void cancel() throws IOException, InterruptedException {
clearFields();
mainCtrl.showFoodpal();
}
public void ok() throws IOException, InterruptedException {
try {
Recipe selected = foodpalCtrl.getSelectedRecipe();
server.addRecipeStep(selected, preparationStep.getText());
} catch (WebApplicationException e) {
var alert = new Alert(Alert.AlertType.ERROR);
alert.initModality(Modality.APPLICATION_MODAL);
alert.setContentText(e.getMessage());
alert.showAndWait();
return;
}
clearFields();
mainCtrl.showFoodpal();
}
private void clearFields() {
preparationStep.clear();
}
public void keyPressed(KeyEvent e) throws IOException, InterruptedException {
switch (e.getCode()) {
case ENTER:
ok();
break;
case ESCAPE:
cancel();
break;
default:
break;
}
}
}

View file

@ -0,0 +1,276 @@
package client.scenes;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import client.utils.ServerUtils;
import commons.Recipe;
import jakarta.inject.Inject;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
public class FoodpalApplicationCtrl implements LocaleAware {
private final MainCtrl mainCtrl;
private final ServerUtils server;
private final LocaleManager localeManager;
// everything in the top bar ===
@FXML
private Button flagEnButton; //already here for advanced stuff
@FXML
private Button flagNlButton; //already here for advanced stuff
@FXML
private Button flagPlButton; //already here for advanced stuff
// everything in the left lane
@FXML
public Label recipesLabel;
@FXML
private ListView<Recipe> recipeList;
@FXML
private Button addRecipeButton;
@FXML
private Button removeRecipeButton;
@FXML
public Button cloneRecipeButton;
// === CENTER: RECIPE DETAILS ===
@FXML
private Label recipeNameLabel;
@FXML
private Button editRecipeTitleButton;
@FXML
public Button removeRecipeButton2;
@FXML
public Button printRecipeButton;
@FXML
public Label ingredientsLabel;
@FXML
public Label preparationLabel;
@FXML
private ListView<String> ingredientsListView;
@FXML
private Button addIngredientButton;
@FXML
private ListView<String> preparationListView;
@FXML
private Button addPreparationStepButton;
@Inject
public FoodpalApplicationCtrl(MainCtrl mainCtrl, ServerUtils server, LocaleManager localeManager) {
this.mainCtrl = mainCtrl;
this.server = server;
this.localeManager = localeManager;
}
@Override
public void initializeComponents() {
// Show recipe name in the list
recipeList.setCellFactory(list -> new ListCell<>() {
@Override
protected void updateItem(Recipe item, boolean empty) {
super.updateItem(item, empty);
setText(empty || item == null ? "" : item.getName());
}
});
// When your selection changes, update details in the panel
recipeList.getSelectionModel().selectedItemProperty().addListener(
(obs, oldRecipe, newRecipe) -> showRecipeDetails(newRecipe)
);
// Double-click to go to detail screen
recipeList.setOnMouseClicked(event -> {
final int DOUBLE_CLICK = 2; //to not get magic number:P
if (event.getClickCount() == DOUBLE_CLICK) {
openSelectedRecipe();
}
});
refresh();
}
@Override
public void updateText() {
addRecipeButton.setText(getLocaleString("menu.button.add.recipe"));
removeRecipeButton.setText(getLocaleString("menu.button.remove.recipe"));
cloneRecipeButton.setText(getLocaleString("menu.button.clone"));
editRecipeTitleButton.setText(getLocaleString("menu.button.edit"));
removeRecipeButton2.setText(getLocaleString("menu.button.remove.recipe"));
printRecipeButton.setText(getLocaleString("menu.button.print"));
recipesLabel.setText(getLocaleString("menu.label.recipes"));
ingredientsLabel.setText(getLocaleString("menu.label.ingredients"));
preparationLabel.setText(getLocaleString("menu.label.preparation"));
addIngredientButton.setText(getLocaleString("menu.button.add.ingredient"));
addPreparationStepButton.setText(getLocaleString("menu.button.add.step"));
}
@Override
public LocaleManager getLocaleManager() {
return localeManager;
}
// till the all the code from everyone is implemented for now to not have errors
private void showRecipeDetails(Recipe recipe) {
if (recipe == null) {
recipeNameLabel.setText("");
ingredientsListView.getItems().clear();
preparationListView.getItems().clear();
return;
}
recipeNameLabel.setText(recipe.getName());
ingredientsListView.getItems().setAll(recipe.getIngredients());
preparationListView.getItems().setAll(recipe.getPreparationSteps());
}
// Button handlers
@FXML
public void refresh() {
// TODO: someone else doing this
List<Recipe> recipes;
try {
recipes = server.getRecipes();
} catch (IOException | InterruptedException e) {
recipes = Collections.emptyList();
System.err.println("Failed to load recipes: " + e.getMessage());
}
recipeList.getItems().setAll(recipes);
// Select first recipe in the list by default
if (!recipes.isEmpty()) {
recipeList.getSelectionModel().selectFirst();
}
}
/**
* Adds a recipe, by going to a different scene, there you insert the title and bam recipe created
*/
@FXML
private void addRecipe() {
// Navigate to "create recipe" screen (should be like a pop-up or new screen or just another place in the app)
mainCtrl.showAddName();
}
@FXML
private void removeSelectedRecipe() throws IOException, InterruptedException {
Recipe selected = recipeList.getSelectionModel().getSelectedItem();
if (selected == null) {
return;
}
server.deleteRecipe(selected.getId());
recipeList.getItems().remove(selected);
}
@FXML
private void openSelectedRecipe() {
Recipe selected = recipeList.getSelectionModel().getSelectedItem();
if (selected == null) {
return;
}
// Let MainCtrl open the full detail screen
mainCtrl.showAddName(); //I had showrecipedetail but intelij says showoverview
}
@FXML
private void editRecipeTitle() {
// TODO: someone else todo
// For now reuse openSelectedRecipe()
openSelectedRecipe();
}
/**
* You can add ingredients. you get send to a different scene that creates the ingredient
* It doesn't automatically refresh yet so before you can see the new ingredient you have to first click on a
* different recipe and come back after
*/
@FXML
private void addIngredient() {
System.out.println("Add ingredient clicked");
mainCtrl.showAddIngredient();
}
/**
* You can add steps. you get send to a different scene that creates the Step
* It doesn't automatically refresh yet so before you can see the new step you have to first click on a
* different recipe and come back after
*/
@FXML
private void addPreparationStep() {
System.out.println("Add preparation step clicked");
mainCtrl.showAddSteps();
}
// Language buttons
@FXML
private void switchLocale(ActionEvent event) {
Button button = (Button)event.getSource();
String lang = (String)button.getUserData();
localeManager.setLocale(Locale.of(lang));
}
@FXML
private void makePrintable() {
System.out.println("Recipe printed");
}
/**
* Get the recipe that was selected
* @return the selected recipe
*/
public Recipe getSelectedRecipe() {
return recipeList.getSelectionModel().getSelectedItem();
}
/**
* Clones a recipe, when clicking on the button "clone"
*/
public void cloneRecipe() throws IOException, InterruptedException {
Recipe selected = recipeList.getSelectionModel().getSelectedItem();
if (selected == null) {
return;
}
server.cloneRecipe(selected.getId());
refresh();
}
}

View file

@ -15,43 +15,127 @@
*/
package client.scenes;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import jakarta.inject.Inject;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.util.Pair;
public class MainCtrl {
import java.io.IOException;
public class MainCtrl implements LocaleAware {
private final LocaleManager localeManager;
@FXML
private Stage primaryStage;
private QuoteOverviewCtrl overviewCtrl;
private Scene overview;
@FXML
private AddNameCtrl addNameCtrl;
private Scene addName;
private AddQuoteCtrl addCtrl;
private Scene add;
@FXML
private FoodpalApplicationCtrl foodpalCtrl;
private Scene foodpal;
@FXML
private AddIngredientCtrl addIngredientCtrl;
private Scene addIngredient;
@FXML
private AddStepsCtrl addStepsCtrl;
private Scene addStep;
private String addNameTitle = "add.name.title";
private String addIngredientTitle = "add.step.title";
private String addStepTitle = "add.ingredient.title";
@Inject
public MainCtrl(LocaleManager localeManager) {
this.localeManager = localeManager;
}
public void setup(Stage primaryStage,
Pair<AddNameCtrl, Parent> addName,
Pair<FoodpalApplicationCtrl, Parent> foodpal,
Pair<AddIngredientCtrl, Parent> addIngredient,
Pair<AddStepsCtrl, Parent> addStep) throws IOException, InterruptedException {
public void initialize(Stage primaryStage, Pair<QuoteOverviewCtrl, Parent> overview,
Pair<AddQuoteCtrl, Parent> add) {
this.primaryStage = primaryStage;
this.overviewCtrl = overview.getKey();
this.overview = new Scene(overview.getValue());
this.addCtrl = add.getKey();
this.add = new Scene(add.getValue());
this.addNameCtrl = addName.getKey();
this.addName = new Scene(addName.getValue());
showOverview();
this.foodpalCtrl = foodpal.getKey();
this.foodpal = new Scene(foodpal.getValue());
this.addIngredientCtrl = addIngredient.getKey();
this.addIngredient = new Scene(addIngredient.getValue());
this.addStepsCtrl = addStep.getKey();
this.addStep = new Scene(addStep.getValue());
showFoodpal();
primaryStage.show();
initialize(); // Initialize LocaleManager stuff manually since MainCtrl isn't loaded from FXML.
}
public void showOverview() {
primaryStage.setTitle("Quotes: Overview");
primaryStage.setScene(overview);
overviewCtrl.refresh();
@Override
public void updateText() {
addNameTitle = getLocaleString("add.recipe.title");
addStepTitle = getLocaleString("add.step.title");
addIngredientTitle = getLocaleString("add.ingredient.title");
}
public void showAdd() {
primaryStage.setTitle("Quotes: Adding Quote");
primaryStage.setScene(add);
add.setOnKeyPressed(e -> addCtrl.keyPressed(e));
@Override
public LocaleManager getLocaleManager() {
return localeManager;
}
public void showAddName() {
primaryStage.setTitle(addNameTitle);
primaryStage.setScene(addName);
addName.setOnKeyPressed(e -> {
try {
addNameCtrl.keyPressed(e);
} catch (IOException | InterruptedException ex) {
throw new RuntimeException(ex);
}
});
}
public void showFoodpal() {
primaryStage.setTitle("FoodPal");
primaryStage.setScene(foodpal);
foodpalCtrl.refresh();
}
public void showAddIngredient() {
primaryStage.setTitle(addIngredientTitle);
primaryStage.setScene(addIngredient);
addIngredient.setOnKeyPressed(e -> {
try {
addIngredientCtrl.keyPressed(e);
} catch (IOException | InterruptedException ex) {
throw new RuntimeException(ex);
}
});
}
public void showAddSteps() {
primaryStage.setTitle(addStepTitle);
primaryStage.setScene(addStep);
addStep.setOnKeyPressed(e -> {
try {
addStepsCtrl.keyPressed(e);
} catch (IOException | InterruptedException ex) {
throw new RuntimeException(ex);
}
});
}
}

View file

@ -1,71 +0,0 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client.scenes;
import java.net.URL;
import java.util.ResourceBundle;
import com.google.inject.Inject;
import client.utils.ServerUtils;
import commons.Quote;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
public class QuoteOverviewCtrl implements Initializable {
private final ServerUtils server;
private final MainCtrl mainCtrl;
private ObservableList<Quote> data;
@FXML
private TableView<Quote> table;
@FXML
private TableColumn<Quote, String> colFirstName;
@FXML
private TableColumn<Quote, String> colLastName;
@FXML
private TableColumn<Quote, String> colQuote;
@Inject
public QuoteOverviewCtrl(ServerUtils server, MainCtrl mainCtrl) {
this.server = server;
this.mainCtrl = mainCtrl;
}
@Override
public void initialize(URL location, ResourceBundle resources) {
colFirstName.setCellValueFactory(q -> new SimpleStringProperty(q.getValue().person.firstName));
colLastName.setCellValueFactory(q -> new SimpleStringProperty(q.getValue().person.lastName));
colQuote.setCellValueFactory(q -> new SimpleStringProperty(q.getValue().quote));
}
public void addQuote() {
mainCtrl.showAdd();
}
public void refresh() {
var quotes = server.getQuotes();
data = FXCollections.observableList(quotes);
table.setItems(data);
}
}

View file

@ -0,0 +1,78 @@
package client.scenes.recipe;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
/**
* A custom ListCell for displaying and editing ingredients in an
* IngredientList. Allows inline editing of ingredient names.
*
* @see IngredientListCtrl
*/
public class IngredientListCell extends ListCell<String> {
// TODO: change all this to use actual Ingredient objects
// and all that :)
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
this.setText(null);
} else {
this.setText(item);
}
}
@Override
public void startEdit() {
super.startEdit();
TextField textField = new TextField(this.getItem());
// Commit edit on Enter key press
textField.setOnAction(event -> {
this.commitEdit(textField.getText());
});
// Cancel edit on Escape key press
textField.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
this.cancelEdit();
}
});
this.setText(null);
this.setGraphic(textField);
}
/**
* Check if the input is valid (non-empty).
*
* @param input The input string to validate.
* @return true if valid, false otherwise.
*/
private boolean isInputValid(String input) {
return input != null && !input.trim().isEmpty();
}
@Override
public void cancelEdit() {
super.cancelEdit();
this.setText(this.getItem());
this.setGraphic(null);
}
@Override
public void commitEdit(String newValue) {
this.setGraphic(null);
if (!isInputValid(newValue)) {
newValue = this.getItem(); // Revert to old value if input is invalid
}
super.commitEdit(newValue);
}
}

View file

@ -0,0 +1,150 @@
package client.scenes.recipe;
import commons.Recipe;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.function.Function;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.ListView.EditEvent;
/**
* Controller for the ingredient list view in the recipe detail scene.
* Manages displaying, adding, editing, and deleting ingredients.
*
* @see RecipeStepListCell The custom cell implementation.
*/
public class IngredientListCtrl implements Initializable {
/**
* <p>
* The list of ingredients currently displayed.
* <br>
* As the ingredient list in {@link Recipe} is immutable,
* this copies that list upon initialization to this mutable list.
* <br>
* Please use the {@link #setUpdateCallback(Function)} function to listen for
* changes and update the recipe accordingly.
* </p>
*/
private ObservableList<String> ingredients;
/**
* A callback function that is called when the ingredient list is updated.
*/
private Function<List<String>, Void> updateCallback;
@FXML
ListView<String> ingredientListView;
@FXML
Button addIngredientButton;
@FXML
Button deleteIngredientButton;
/**
* Set the recipe and refetch data from it.
* This replaces the current ingredient list.
* Only use this once per recipe when initializing the detail view.
*
* @param recipe The recipe to fetch data from.
*/
public void refetchFromRecipe(Recipe recipe) {
if (recipe == null) {
this.ingredients = FXCollections.observableArrayList(new ArrayList<>());
} else {
List<String> ingredientList = recipe.getIngredients();
this.ingredients = FXCollections.observableArrayList(ingredientList);
}
this.ingredientListView.setItems(this.ingredients);
this.refresh();
}
/**
* Set a callback that's called when the ingredient list changes.
*
* @param callback The function to call upon each update.
*/
public void setUpdateCallback(Function<List<String>, Void> callback) {
this.updateCallback = callback;
}
/**
* Refresh the ingredient list view.
*/
private void refresh() {
ingredientListView.refresh();
}
/**
* Handle ingredient addition. Automatically calls update callback.
*
* @param event The action event.
*/
private void handleIngredientAdd(ActionEvent event) {
this.ingredients.add("Ingredient " + (this.ingredients.size() + 1));
this.refresh();
this.updateCallback.apply(this.ingredients);
var select = this.ingredientListView.getSelectionModel();
select.select(this.ingredients.size() - 1);
}
/**
* Handle ingredient edits. Automatically calls update callback.
*
* @param event The edit event.
*/
private void handleIngredientEdit(EditEvent<String> event) {
int index = event.getIndex();
String newValue = event.getNewValue();
this.ingredients.set(index, newValue);
this.refresh();
this.updateCallback.apply(this.ingredients);
}
/**
* Handle ingredient deletion. Automatically calls update callback.
*
* @param event The action event.
*/
private void handleIngredientDelete(ActionEvent event) {
var select = this.ingredientListView.getSelectionModel();
int selectedIndex = select.getSelectedIndex();
// No index is selected, don't do anything
if (selectedIndex < 0) {
return;
}
this.ingredients.remove(selectedIndex);
this.refresh();
this.updateCallback.apply(this.ingredients);
}
@FXML
public void initialize(URL location, ResourceBundle resources) {
// 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 -> {
return new RecipeStepListCell();
});
this.ingredientListView.setOnEditCommit(this::handleIngredientEdit);
this.addIngredientButton.setOnAction(this::handleIngredientAdd);
this.deleteIngredientButton.setOnAction(this::handleIngredientDelete);
this.refresh();
}
}

View file

@ -0,0 +1,96 @@
package client.scenes.recipe;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
/**
* A custom ListCell for displaying and editing ingredients in an
* RecipeStepList. Allows inline editing of ingredient names.
*
* @see RecipeStepListCtrl
*/
public class RecipeStepListCell extends ListCell<String> {
/**
* Get the display text for the given item, prefixed with its index.
* Looks like "1. Step description".
*
* @param item The step description.
* @return The display text.
*/
private String getDisplayText(String item) {
return this.getIndex() + ". " + item;
}
/**
* Get the display text for the current item.
* Looks like "1. Step description".
*
* @return The display text.
*/
private String getDisplayText() {
return this.getDisplayText(this.getItem());
}
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
this.setText(null);
} else {
this.setText(this.getDisplayText(item));
}
}
@Override
public void startEdit() {
super.startEdit();
TextField textField = new TextField(this.getItem());
// Commit edit on Enter key press
textField.setOnAction(event -> {
this.commitEdit(textField.getText());
});
// Cancel edit on Escape key press
textField.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
this.cancelEdit();
}
});
this.setText(null);
this.setGraphic(textField);
}
/**
* Check if the input is valid (non-empty).
*
* @param input The input string to validate.
* @return true if valid, false otherwise.
*/
private boolean isInputValid(String input) {
return input != null && !input.trim().isEmpty();
}
@Override
public void cancelEdit() {
super.cancelEdit();
this.setText(this.getDisplayText());
this.setGraphic(null);
}
@Override
public void commitEdit(String newValue) {
this.setGraphic(null);
if (!isInputValid(newValue)) {
newValue = this.getItem(); // Revert to old value if input is invalid
}
super.commitEdit(newValue);
}
}

View file

@ -0,0 +1,150 @@
package client.scenes.recipe;
import commons.Recipe;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.function.Function;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.ListView.EditEvent;
/**
* Controller for the step list view in the recipe detail scene.
* Manages displaying, adding, editing, and deleting steps.
*
* @see RecipeStepListCell The custom cell implementation.
*/
public class RecipeStepListCtrl implements Initializable {
/**
* <p>
* The list of recipe steps currently displayed.
* <br>
* As the step list in {@link Recipe} is immutable,
* this copies that list upon initialization to this mutable list.
* <br>
* Please use the {@link #setUpdateCallback(Function)} function to listen for
* changes and update the recipe accordingly.
* </p>
*/
private ObservableList<String> steps;
/**
* A callback function that is called when the step list is updated.
*/
private Function<List<String>, Void> updateCallback;
@FXML
ListView<String> recipeStepListView;
@FXML
Button addStepButton;
@FXML
Button deleteStepButton;
/**
* Set the recipe and refetch data from it.
* This replaces the current step list.
* Only use this once per recipe when initializing the detail view.
*
* @param recipe The recipe to fetch data from.
*/
public void refetchFromRecipe(Recipe recipe) {
if (recipe == null) {
this.steps = FXCollections.observableArrayList(new ArrayList<>());
} else {
List<String> stepList = recipe.getPreparationSteps();
this.steps = FXCollections.observableArrayList(stepList);
}
this.recipeStepListView.setItems(this.steps);
this.refresh();
}
/**
* Set a callback that's called when the step list changes.
*
* @param callback The function to call upon each update.
*/
public void setUpdateCallback(Function<List<String>, Void> callback) {
this.updateCallback = callback;
}
/**
* Refresh the step list view.
*/
private void refresh() {
recipeStepListView.refresh();
}
/**
* Handle step addition. Automatically calls update callback.
*
* @param event The action event.
*/
private void handleIngredientAdd(ActionEvent event) {
this.steps.add("Step " + (this.steps.size() + 1));
this.refresh();
this.updateCallback.apply(this.steps);
var select = this.recipeStepListView.getSelectionModel();
select.select(this.steps.size() - 1);
}
/**
* Handle step edits. Automatically calls update callback.
*
* @param event The edit event.
*/
private void handleIngredientEdit(EditEvent<String> event) {
int index = event.getIndex();
String newValue = event.getNewValue();
this.steps.set(index, newValue);
this.refresh();
this.updateCallback.apply(this.steps);
}
/**
* Handle step deletion. Automatically calls update callback.
*
* @param event The action event.
*/
private void handleIngredientDelete(ActionEvent event) {
var select = this.recipeStepListView.getSelectionModel();
int selectedIndex = select.getSelectedIndex();
// No index is selected, don't do anything
if (selectedIndex < 0) {
return;
}
this.steps.remove(selectedIndex);
this.refresh();
this.updateCallback.apply(this.steps);
}
@FXML
public void initialize(URL location, ResourceBundle resources) {
// 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.recipeStepListView.setEditable(true);
this.recipeStepListView.setCellFactory(
list -> {
return new RecipeStepListCell();
});
this.recipeStepListView.setOnEditCommit(this::handleIngredientEdit);
this.addStepButton.setOnAction(this::handleIngredientAdd);
this.deleteStepButton.setOnAction(this::handleIngredientDelete);
this.refresh();
}
}

View file

@ -0,0 +1,42 @@
package client.utils;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;
public interface LocaleAware extends Initializable {
/**
* Updates all text when locale changes.
* Must be implemented by each controller.
*/
void updateText();
/**
* Returns the injected LocaleManager.
* Must be implemented by each controller.
*/
LocaleManager getLocaleManager();
@Override
default void initialize(URL location, ResourceBundle resources) {
updateText();
getLocaleManager().getBundleProperty().addListener((_, _, _) -> updateText());
initializeComponents();
}
default void initialize() {
initialize(null, null);
}
default String getLocaleString(String key) {
return getLocaleManager().getBundle().getString(key);
}
/**
* Optional function for initialization of components. Runs after localization.
*/
default void initializeComponents() {}
}

View file

@ -0,0 +1,47 @@
package client.utils;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.util.Locale;
import java.util.ResourceBundle;
public class LocaleManager {
private final ObjectProperty<Locale> currentLocale = new SimpleObjectProperty<>(Locale.ENGLISH);
private final ObjectProperty<ResourceBundle> currentBundle = new SimpleObjectProperty<>();
private static final String RESOURCE_BUNDLE_PATH = "locale/lang";
public LocaleManager() {
// TODO: Set currentLocale to config value instead of EN default.
updateBundle();
currentLocale.addListener((_, _, _) -> updateBundle());
}
private void updateBundle() {
ResourceBundle bundle = ResourceBundle.getBundle(RESOURCE_BUNDLE_PATH,
currentLocale.get(),
ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_PROPERTIES)
);
currentBundle.set(bundle);
}
@SuppressWarnings("unused")
public void setLocale(Locale locale) {
currentLocale.set(locale);
}
@SuppressWarnings("unused")
public Locale getLocale() {
return currentLocale.get();
}
@SuppressWarnings("unused")
public ResourceBundle getBundle() {
return currentBundle.get();
}
@SuppressWarnings("unused")
public ObjectProperty<ResourceBundle> getBundleProperty() {
return currentBundle;
}
}

View file

@ -1,64 +1,147 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import commons.Recipe;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.ClientBuilder;
import org.glassfish.jersey.client.ClientConfig;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ConnectException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import org.glassfish.jersey.client.ClientConfig;
import commons.Quote;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.GenericType;
public class ServerUtils {
private static final String SERVER = "http://localhost:8080/api";
private final HttpClient client;
private final ObjectMapper objectMapper = new ObjectMapper();
private final int statusOK = 200;
private static final String SERVER = "http://localhost:8080/";
public ServerUtils() {
client = HttpClient.newHttpClient();
}
public void getQuotesTheHardWay() throws IOException, URISyntaxException {
var url = new URI("http://localhost:8080/api/quotes").toURL();
var is = url.openConnection().getInputStream();
var br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
/**
* Gets all the recipes from the backend
* @return a JSON string with all the recipes
*/
public List<Recipe> getRecipes() throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/recipes"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("No recipe to get. Server responds with " + response.body());
}
return objectMapper.readValue(response.body(), new TypeReference<List<Recipe>>() {
});// JSON string-> List<Recipe> (Jackson)
}
/**
* Gets a single recipe based on its id
* @param id every recipe has it's unique id
* @return a singe recipe with the given id
*/
public Recipe findId(long id) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER +"/recipe/" + id))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("failed finding recipe with id: "+ id + " body: " + response.body());
}
return objectMapper.readValue(response.body(),Recipe.class);
}
/**
* Adds a recipe to the backend
* @param newRecipe the recipe to be added
* @return a recipe
*/
public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException {
//Make sure the name of the newRecipe is unique
List<Recipe> allRecipes = getRecipes();
int version = 1;
String originalName = newRecipe.getName();
while (allRecipes.stream().anyMatch(r -> r.getName().equals(newRecipe.getName()))) {
String newName = originalName + "(" + version++ + ")";
newRecipe.setName(newName);
}
String json = objectMapper.writeValueAsString(newRecipe);
//Recipe to backend
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/recipe/new"))
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("Failed to add Recipe: " + newRecipe.toDetailedString() + "body: " + response.body());
}
return objectMapper.readValue(response.body(),Recipe.class);
}
/**
* Deletes a recipe
* @param id the recipe that get deleted
*/
public void deleteRecipe(long id) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/recipe/" + id))
.DELETE()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("Failed removing recipe with id: " + id + "body: " + response.body());
}
}
public List<Quote> getQuotes() {
return ClientBuilder.newClient(new ClientConfig()) //
.target(SERVER).path("api/quotes") //
.request(APPLICATION_JSON) //
.get(new GenericType<List<Quote>>() {});
}
/**
* Clones a recipe
* @param id the id of the recipe to be cloned
* @return a duplicated recipe of the given recipe
*/
public Recipe cloneRecipe(long id) throws IOException, InterruptedException {
//Get the recipe
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/recipe/" + id))
.GET() //Needs to be changed to POST() when api is changed
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
public Quote addQuote(Quote quote) {
return ClientBuilder.newClient(new ClientConfig()) //
.target(SERVER).path("api/quotes") //
.request(APPLICATION_JSON) //
.post(Entity.entity(quote, APPLICATION_JSON), Quote.class);
//200 is the status code for success, other codes can mean there is no recipe to clone
if(response.statusCode() != statusOK){
throw new IOException("Failed to get recipe to clone with id: " + id + "body: " + response.body());
}
// recipe exists so you can make a "new" recipe aka the clone
Recipe recipe = objectMapper.readValue(response.body(), Recipe.class);
recipe.setId(null); // otherwise the id is the same as the original, and that's wrong
return addRecipe(recipe);
}
public boolean isServerAvailable() {
@ -74,4 +157,44 @@ public class ServerUtils {
}
return true;
}
public void addRecipeName(String name) throws IOException, InterruptedException {
Recipe newRecipe = new Recipe();
newRecipe.setName(name);
addRecipe(newRecipe);
}
public void addRecipeIngredient(Recipe recipe, String ingredient) throws IOException, InterruptedException {
List<String> ingredients = new ArrayList<>(recipe.getIngredients());
ingredients.add(ingredient);
recipe.setIngredients(ingredients);
updateRecipe(recipe);
}
public void updateRecipe(Recipe recipe) throws IOException, InterruptedException {
String json = objectMapper.writeValueAsString(recipe);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SERVER + "/recipe/" + recipe.getId()))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString((json))) // Needs to be changed to PUT() when api changed
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("Failed to update recipe: " + recipe.toDetailedString() + "body: " + response.body());
}
objectMapper.readValue(response.body(), Recipe.class);
}
public void addRecipeStep(Recipe recipe, String preparationStep) throws IOException, InterruptedException {
List<String> preparationSteps = new ArrayList<>(recipe.getPreparationSteps());
preparationSteps.add(preparationStep);
recipe.setPreparationSteps(preparationSteps);
updateRecipe(recipe);
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright 2021 Delft University of Technology
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client.utils;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import java.net.ConnectException;
import java.util.List;
import jakarta.ws.rs.core.GenericType;
import org.glassfish.jersey.client.ClientConfig;
import commons.Quote;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
public class ServerUtilsExample {
private static final String SERVER = "http://localhost:8080/";
// public void getQuotesTheHardWay() throws IOException, URISyntaxException {
// var url = new URI("http://localhost:8080/api/quotes").toURL();
// var is = url.openConnection().getInputStream();
// var br = new BufferedReader(new InputStreamReader(is));
// String line;
// while ((line = br.readLine()) != null) {
// System.out.println(line);
// }
// }
public List<Quote> getQuotes() {
return ClientBuilder.newClient(new ClientConfig()) //
.target(SERVER).path("api/quotes") //
.request(APPLICATION_JSON) //
.get(new GenericType<List<Quote>>() {});
}
public Quote addQuote(Quote quote) {
return ClientBuilder.newClient(new ClientConfig()) //
.target(SERVER).path("api/quotes") //
.request(APPLICATION_JSON) //
.post(Entity.entity(quote, APPLICATION_JSON), Quote.class);
}
public boolean isServerAvailable() {
try {
ClientBuilder.newClient(new ClientConfig()) //
.target(SERVER) //
.request(APPLICATION_JSON) //
.get();
} catch (ProcessingException e) {
if (e.getCause() instanceof ConnectException) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="94.0" prefWidth="470.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.AddIngredientCtrl">
<children>
<Button fx:id="okButton" layoutX="417.0" layoutY="54.0" mnemonicParsing="false" onAction="#ok" text="Ok" />
<Button fx:id="cancelButton" layoutX="349.0" layoutY="54.0" mnemonicParsing="false" onAction="#cancel" text="Cancel" />
<TextField fx:id="ingredient" layoutX="120.0" layoutY="24.0" prefHeight="18.0" prefWidth="292.0" />
<Label fx:id="ingredientLabel" layoutX="28.0" layoutY="29.0" text="Ingredient Name" />
</children>
</AnchorPane>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="94.0" prefWidth="470.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.AddNameCtrl">
<children>
<Button fx:id="okButton" layoutX="417.0" layoutY="54.0" mnemonicParsing="false" onAction="#ok" text="Ok" />
<Button fx:id="cancelButton" layoutX="349.0" layoutY="54.0" mnemonicParsing="false" onAction="#cancel" text="Cancel" />
<TextField fx:id="recipeName" layoutX="109.0" layoutY="24.0" prefHeight="18.0" prefWidth="292.0" />
<Label fx:id="recipeNameLabel" layoutX="28.0" layoutY="29.0" text="Recipe Name" />
</children>
</AnchorPane>

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="146.0" prefWidth="470.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.AddQuoteCtrl">
<children>
<Button layoutX="419.0" layoutY="105.0" mnemonicParsing="false" onAction="#ok" text="Ok" />
<Button layoutX="350.0" layoutY="105.0" mnemonicParsing="false" onAction="#cancel" text="Cancel" />
<TextField fx:id="firstName" layoutX="109.0" layoutY="24.0" prefHeight="26.0" prefWidth="79.0" />
<TextField fx:id="lastName" layoutX="286.0" layoutY="24.0" />
<TextField fx:id="quote" layoutX="109.0" layoutY="61.0" prefHeight="26.0" prefWidth="345.0" />
<Label layoutX="216.0" layoutY="29.0" text="Last Name" />
<Label layoutX="28.0" layoutY="29.0" text="First Name" />
<Label layoutX="28.0" layoutY="66.0" text="Quote" />
</children>
</AnchorPane>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="94.0" prefWidth="470.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.AddStepsCtrl">
<children>
<Button fx:id="okButton" layoutX="417.0" layoutY="54.0" mnemonicParsing="false" onAction="#ok" text="Ok" />
<Button fx:id="cancelButton" layoutX="349.0" layoutY="54.0" mnemonicParsing="false" onAction="#cancel" text="Cancel" />
<TextField fx:id="preparationStep" layoutX="120.0" layoutY="24.0" prefHeight="18.0" prefWidth="292.0" />
<Label fx:id="preparationStepLabel" layoutX="28.0" layoutY="29.0" text="Preparation step" />
</children>
</AnchorPane>

View file

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<BorderPane prefHeight="800.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.FoodpalApplicationCtrl">
<!-- TOP BAR -->
<top>
<HBox spacing="10">
<padding>
<Insets bottom="10" left="10" right="10" top="10" />
</padding>
<GridPane>
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="198.0" minWidth="10.0" prefWidth="130.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="95.0" minWidth="2.0" prefWidth="70.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints maxHeight="54.0" minHeight="10.0" prefHeight="54.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="25.0" minHeight="6.0" prefHeight="6.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label prefHeight="56.0" prefWidth="158.0" text="FoodPal">
<font>
<Font name="System Bold" size="29.0" />
</font>
</Label>
<ButtonBar prefHeight="40.0" prefWidth="200.0" GridPane.rowIndex="2">
<buttons>
<ToolBar prefHeight="35.0" prefWidth="123.0">
<items>
<Button fx:id="flagEnButton" minWidth="33.0" onAction="#switchLocale" userData="en" prefHeight="25.0" prefWidth="33.0" text="EN" />
<Button fx:id="flagNlButton" onAction="#switchLocale" userData="nl" text="NL" />
<Button fx:id="flagPlButton" onAction="#switchLocale" userData="pl" text="PL" />
</items>
</ToolBar>
</buttons>
</ButtonBar>
</children>
</GridPane>
<Pane HBox.hgrow="ALWAYS" />
<HBox spacing="5" />
<GridPane prefHeight="90.0" prefWidth="114.0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="315.0" minWidth="10.0" prefWidth="32.1666259765625" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="315.0" minWidth="10.0" prefWidth="24.8333740234375" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="173.0" minWidth="10.0" prefWidth="28.6666259765625" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="95.0" minWidth="10.0" prefWidth="34.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Button fx:id="refreshButton" onAction="#refresh" prefHeight="25.0" prefWidth="34.0" text="⟳" GridPane.columnIndex="3" GridPane.rowIndex="2" />
</children>
</GridPane>
</HBox>
</top>
<!-- LEFT: RECIPE LIST -->
<left>
<VBox spacing="10">
<padding>
<Insets bottom="10" left="10" right="10" top="10" />
</padding>
<Label fx:id="recipesLabel" text="Recipes">
<font>
<Font name="System Bold" size="15.0" />
</font></Label>
<ListView fx:id="recipeList" />
<HBox spacing="10">
<Button fx:id="addRecipeButton" onAction="#addRecipe" text="Add Recipe" />
<Button fx:id="removeRecipeButton" onAction="#removeSelectedRecipe" text="Remove Recipe" />
<Button fx:id= "cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
</HBox>
</VBox>
</left>
<!-- CENTER: RECIPE DETAILS -->
<center>
<VBox spacing="20">
<padding>
<Insets bottom="10" left="10" right="10" top="10" />
</padding>
<!-- Recipe title row -->
<HBox spacing="10">
<Label fx:id="recipeNameLabel" text="Recipe Name">
<font>
<Font name="System Bold" size="14.0" />
</font></Label>
<Button fx:id="editRecipeTitleButton" onAction="#editRecipeTitle" text="Edit" />
<Button fx:id="removeRecipeButton2" mnemonicParsing="false" onAction="#removeSelectedRecipe" text="Remove Recipe" />
<Button fx:id="printRecipeButton" mnemonicParsing="false" onAction="#makePrintable" text="Print Recipe" />
</HBox>
<!-- Ingredients -->
<VBox spacing="10">
<Label fx:id="ingredientsLabel" text="Ingredients">
<font>
<Font name="System Bold" size="14.0" />
</font>
</Label>
<ListView fx:id="ingredientsListView" prefHeight="167.0" prefWidth="912.0" />
<Button fx:id="addIngredientButton" onAction="#addIngredient" text="Add Ingredient" />
</VBox>
<!-- Preparation -->
<Label fx:id="preparationLabel" text="Preparation">
<font>
<Font name="System Bold" size="14.0" />
</font>
</Label>
<ListView fx:id="preparationListView" prefHeight="200.0" />
<Button fx:id="addPreparationStepButton" onAction="#addPreparationStep" text="Add Step" />
<GridPane>
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="890.6666870117188" minWidth="10.0" prefWidth="854.6666870117188" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="445.3333740234375" minWidth="10.0" prefWidth="57.33331298828125" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
</GridPane>
</VBox>
</center>
</BorderPane>

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.QuoteOverviewCtrl">
<children>
<Button layoutX="503.0" layoutY="360.0" mnemonicParsing="false" onAction="#addQuote" text="Add Quote" />
<Button layoutX="431.0" layoutY="360.0" mnemonicParsing="false" onAction="#refresh" text="Refresh" />
<TableView fx:id="table" layoutX="14.0" layoutY="14.0" prefHeight="337.0" prefWidth="572.0">
<columns>
<TableColumn fx:id="colFirstName" prefWidth="113.0" text="First Name" />
<TableColumn fx:id="colLastName" prefWidth="114.0" text="Last Name" />
<TableColumn fx:id="colQuote" prefWidth="344.0" text="Quote" />
</columns>
</TableView>
</children>
</AnchorPane>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.RecipeStepListCtrl">
<children>
<VBox minHeight="200.0" minWidth="200.0">
<children>
<HBox alignment="CENTER_LEFT" spacing="20.0">
<children>
<Label text="Ingredients">
<padding>
<Insets bottom="5.0" top="5.0" />
</padding>
<font>
<Font name="System Bold" size="16.0" />
</font>
</Label>
<HBox alignment="CENTER" spacing="10.0">
<children>
<Button fx:id="addIngredientButton" mnemonicParsing="false" text="Add" />
<Button fx:id="deleteIngredientButton" mnemonicParsing="false" text="Delete" />
</children>
</HBox>
</children>
<padding>
<Insets left="10.0" right="10.0" />
</padding>
</HBox>
<ListView fx:id="ingredientListView" prefHeight="200.0" prefWidth="200.0" />
</children>
</VBox>
</children>
</AnchorPane>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.RecipeStepListCtrl">
<children>
<VBox minHeight="200.0" minWidth="200.0">
<children>
<HBox alignment="CENTER_LEFT" spacing="20.0">
<children>
<Label text="Steps">
<padding>
<Insets bottom="5.0" top="5.0" />
</padding>
<font>
<Font name="System Bold" size="16.0" />
</font>
</Label>
<HBox alignment="CENTER" spacing="10.0">
<children>
<Button fx:id="addStepButton" mnemonicParsing="false" text="Add" />
<Button fx:id="deleteStepButton" mnemonicParsing="false" text="Delete" />
</children>
</HBox>
</children>
<padding>
<Insets left="10.0" right="10.0" />
</padding>
</HBox>
<ListView fx:id="recipeStepListView" prefHeight="200.0" prefWidth="200.0" />
</children>
</VBox>
</children>
</AnchorPane>

View file

@ -0,0 +1,26 @@
add.ingredient.title=Add Ingredient
add.recipe.title=Create recipe
add.step.title=Add Step
add.ingredient.label=Ingredient
add.recipe.label=Recipe Name
add.step.label=Step
button.ok=Ok
button.cancel=Cancel
menu.label.recipes=Recipes
menu.label.ingredients=Ingredients
menu.label.preparation=Preparation
menu.button.add.recipe=Add Recipe
menu.button.add.ingredient=Add Ingredient
menu.button.add.step=Add Step
menu.button.remove.recipe=Remove Recipe
menu.button.remove.ingredient=Remove Ingredient
menu.button.remove.step=Remove Step
menu.button.edit=Edit
menu.button.clone=Clone
menu.button.print=Print recipe

View file

@ -0,0 +1,26 @@
add.ingredient.title=Add Ingredient
add.recipe.title=Create recipe
add.step.title=Add Step
add.ingredient.label=Ingredient
add.recipe.label=Recipe Name
add.step.label=Step
button.ok=Ok
button.cancel=Cancel
menu.label.recipes=Recipes
menu.label.ingredients=Ingredients
menu.label.preparation=Preparation
menu.button.add.recipe=Add Recipe
menu.button.add.ingredient=Add Ingredient
menu.button.add.step=Add Step
menu.button.remove.recipe=Remove Recipe
menu.button.remove.ingredient=Remove Ingredient
menu.button.remove.step=Remove Step
menu.button.edit=Edit
menu.button.clone=Clone
menu.button.print=Print recipe

View file

@ -0,0 +1,26 @@
add.ingredient.title=Ingredient toevoegen
add.recipe.title=Recept aanmaken
add.step.title=Stap toevoegen
add.ingredient.label=Ingredient
add.recipe.label=Receptnaam
add.step.label=Bereidingsstap
button.ok=Ok
button.cancel=Annuleren
menu.label.recipes=Recepten
menu.label.ingredients=Ingrediënten
menu.label.preparation=Bereiding
menu.button.add.recipe=Recept toevoegen
menu.button.add.ingredient=Ingrediënt toevoegen
menu.button.add.step=Stap toevoegen
menu.button.remove.recipe=Recept verwijderen
menu.button.remove.ingredient=Ingrediënt verwijderen
menu.button.remove.step=Stap verwijderen
menu.button.edit=Bewerken
menu.button.clone=Dupliceren
menu.button.print=Recept afdrukken

View file

@ -0,0 +1,26 @@
add.ingredient.title=Dodaj składnik
add.recipe.title=Utwórz przepis
add.step.title=Dodaj instrukcję
add.ingredient.label=Składnik
add.recipe.label=Nazwa przepisu
add.step.label=Instrukcja
button.ok=Ok
button.cancel=Anuluj
menu.label.recipes=Przepisy
menu.label.ingredients=Składniki
menu.label.preparation=Przygotowanie
menu.button.add.recipe=Dodaj przepis
menu.button.add.ingredient=Dodaj składnik
menu.button.add.step=Dodaj instrukcję
menu.button.remove.recipe=Usuń przepis
menu.button.remove.ingredient=Usuń składnik
menu.button.remove.step=Usuń instrukcję
menu.button.edit=Edytuj
menu.button.clone=Duplikuj
menu.button.print=Drukuj przepis

View file

@ -0,0 +1,106 @@
package client;
import client.utils.ServerUtils;
import commons.Recipe;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ServerUtilsTest {
static ServerUtils dv;
static Recipe testRecipe;
final int fakeId = -1; // If suppose ID's are only positive
@BeforeEach
void setup() throws IOException, InterruptedException {
dv = new ServerUtils();
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
//Making sure there is no recipe in the backend yet
for (Recipe recipe : dv.getRecipes()) {
dv.deleteRecipe(recipe.getId());
}
Recipe r = new Recipe();
r.setName("Tosti");
r.setIngredients(List.of("Bread", "Cheese", "Ham"));
r.setPreparationSteps(List.of("Step 1:", "Step 2"));
testRecipe = dv.addRecipe(r);
}
@Test
void addRecipeTest() throws IOException, InterruptedException {
Recipe r = new Recipe();
r.setName("Eggs on toast");
r.setIngredients(List.of("Bread", "egg", "salt"));
r.setPreparationSteps(List.of("Step 1:", "Step 2"));
testRecipe = dv.addRecipe(r);
}
@Test
void noRecipeToAdd(){
Recipe r = new Recipe();
assertThrows(IOException.class, () -> dv.addRecipe(r));
}
@Test
void getAllRecipesTest() throws IOException, InterruptedException {
List<Recipe> recipes = dv.getRecipes();
assertNotNull(recipes, "The list should not be null");
assertTrue(recipes.size() >= 0, "The list should be 0 (when no recipes), or more");
}
@Test
void findRecipeWithIDTest() throws IOException, InterruptedException {
Recipe r = dv.findId(testRecipe.getId());
assertEquals(testRecipe.getId(), r.getId());
assertEquals("Tosti", r.getName());
assertIterableEquals(List.of("Bread", "Cheese", "Ham"), r.getIngredients());
assertIterableEquals(List.of("Step 1:", "Step 2"), r.getPreparationSteps());
}
@Test
void noRecipeFoundTest() throws IOException, InterruptedException {
assertThrows(IOException.class, () -> dv.findId(fakeId));
}
@Test
void deleteRecipeTest() throws IOException, InterruptedException {
dv.deleteRecipe(testRecipe.getId());
assertThrows(IOException.class, () ->dv.findId(testRecipe.getId()), "The recipe shouldn't exists anymore" );
}
@Test
void noRecipeToDelete() throws IOException, InterruptedException {
assertThrows(IOException.class, () -> dv.deleteRecipe(fakeId));
}
@Test
void cloneRecipeTest() throws IOException, InterruptedException {
Recipe clone = dv.cloneRecipe(testRecipe.getId());
assertNotEquals(clone.getId(), testRecipe.getId(), "The id's should not be equal to each other");
assertNotEquals(clone.getName(),testRecipe.getName(),"The name's should not be the same");
assertEquals(clone.getIngredients(), testRecipe.getIngredients(), "the ingredients should be the same");
assertEquals(clone.getPreparationSteps(), testRecipe.getPreparationSteps(),"The steps should be the same");
}
@Test
void noRecipeToClone(){
assertThrows(IOException.class, () -> dv.cloneRecipe(fakeId));
}
}

View file

@ -15,16 +15,16 @@
*/
package client.scenes;
import client.utils.LocaleManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MainCtrlTest {
private MainCtrl sut;
@BeforeEach
public void setup() {
sut = new MainCtrl();
sut = new MainCtrl(new LocaleManager());
}
@Test

View file

@ -1,46 +1,62 @@
# Code of Conduct CSEP
> Last changed: 2025-11-14 04:20 PM
## Assignment description
- working on our teamwork skills
- coding with other people
- communication skill in a group setting
> Last changed: 2025-11-27 10:30 PM
## 1. Assignment description
- Working on our teamwork skills by helping and collaborating with each other.
- Coding with other people through the use of Git + GitLab tooling.
- Communication skill in a group setting working in a project with 6 people
## Target or ambition level
- so we working towards a 10 but a 8 is fine
- the most important thing is that everyone does their best
## 2. Target or ambition level
- Goal is at least an 8/10, aiming for a 10.
- The most important thing is that everyone does their best
## Planning
- friday - before the ta meeting checking eachother codes and making a planning for the week after
- label the to do lists in gitlab on friday (during planning time)
- check up on eachother thoughout the week
- **Submitter** - Rithvik
## 3. Planning
- Meet in person minimally 2 times a week with preference to Thursday and Friday
- Meet 1 time a week online on monday
- We will adjust expectations and deadlines dynamically depending on what has been actually achieved in the previous week, during one of the aforementioned meetings.
- During a pre-meeting session in the CSE1105 labs on Friday, we will:
- discuss any development blockages that team members encountered during the week.
- go over open MRs.
- plan any task items for the next week.
- consider potential additions to the week's agenda.
- **Submitter of any Brightspace material** - Rithvik Sriram
## Communication
- Discord for our online meeting and sharing info and asking questions
## 4. Communication
- Discord for our online meeting and sharing info and asking questions.
- GitLab to share our code
- Friday lab meetings for in person meetings
- Thursday and Friday lab meetings for in person meetings
### Collaboration outside of the mandatory meetings
- Friday during lab before ta meeting
- Discord
## 5. Collaboration outside of the mandatory meetings
- Thursdays (to code together and help eachother)
- Friday during lab before ta meeting (to make the final changes before merging)
- Discord (for our quick questions about the small things)
- GitLab
## Help and assistance
- We have discord together, so if there is a question or issue it can be asked there and also during the friday meeting
## 6. Help and assistance
- Reserving Discord as a method of communication through which anyone who has an issue or question can talk.
- A person who is having trouble with implementing a specific part of the software is ensured to have another team member available who can assist in debugging/guiding the implementation of that particular feature.
## Work quality
- By having the other 5 members rate the code on gitlab
- Making sure we use the same checkstyle document
## 7. Work quality
- Each Merge Request is required to:
- have at least approval from one person responsible for backend, and one person responsible for frontend.
- concern only one feature/issue, to ensure atomic MRs/commits.
- have generally atomic commits, aka. frequent and small, such that any issues introduced by one specific commit can be triaged quickly.
- Comply to the existing CheckStyle document (enforced automatically via Maven build server).
- have clear comments and JavaDoc written wherever non-trivial functionality is involved.
## Decision-making
- Discuss it with arguments so consensus
## 8. Decision-making
- Important decisions within the group, such as major refactoring or design decisions, will be decided within the group by way of consensus.
- Decide on where and where to meet during the week by way of Discord polling.
## Broken agreements
- Discuss it with them
- If they dont do anything (work, meetings) at all discuss with the ta
## 9. Broken agreements
- If someone breaks the agreements we have agreed upon in this code of conduct we will first of all discuss it with them
- If someone isn't collaborating at all we will make this clear to the TA and hope they can help
- If that doesn't resolve it we will bring it up with the lecturer
- Never will we just exclude someone without taking the proper step
## 10. Problem resolution
- Main way of problem resolution is by reasoning and team discussion in good faith.
- If a team member is not able to, or will be late to, a specific meeting, this should be communicated at least 1 full day before the meeting's commencement.
- For major issues and problems for which the above methods cannot suffice, the responsible chair during the week in which the issue occurred will consult third parties in order of increasing severity:
1. discuss the issue with the TA.
2. communicate with the CSE1105 teaching team e.g. by form of email.
## Problem resolution
- We are all adults here so we can just talk it out by civil discussion
- If your gonna be late, communicate with the rest of the team
- Give a reason if you can't show up for a meeting
- Contact with the TA will be needed when someone doesn't react to any of our messages and doesnt show up at all

42
countlines.bat Normal file
View file

@ -0,0 +1,42 @@
@echo off
setlocal enabledelayedexpansion
for /f "delims=" %%i in ('git config user.email') do set AUTHOR=%%i
echo Checking commit stats for %AUTHOR%...
echo.
echo === Last 7 days (all .java files) ===
git log --author="%AUTHOR%" --since="7 days ago" --pretty=tformat: --numstat | findstr "\.java" > temp_java.txt
set added=0
set removed=0
for /f "tokens=1,2" %%a in (temp_java.txt) do (
set /a added+=%%a
set /a removed+=%%b
)
set /a net=added-removed
echo Lines added in .java: %added%
echo Lines removed in .java: %removed%
echo Net lines: %net%
echo.
echo === All time (excluding *Test.java) ===
git log --author="%AUTHOR%" --pretty=tformat: --numstat -- . ":(exclude)*Test.java" | findstr "\.java" > temp_java.txt
set added=0
set removed=0
for /f "tokens=1,2" %%a in (temp_java.txt) do (
set /a added+=%%a
set /a removed+=%%b
)
set /a net=added-removed
echo Lines added in .java (excluding *Test.java): %added%
echo Lines removed in .java (excluding *Test.java): %removed%
echo Net lines: %net%
del temp_java.txt
endlocal

28
countlines.sh Normal file
View file

@ -0,0 +1,28 @@
#!/bin/bash
AUTHOR=$(git config user.email)
echo -e "Checking commit stats for \033[38:5:2m$AUTHOR\033[0m..."
# Get numstat for commits by author, filter only .java lines
git log --author="$AUTHOR" --since="7 days ago" --pretty=tformat: --numstat \
| awk '
$3 ~ /\.java$/ {
added += $1
removed += $2
}
END {
print "Lines added in .java:", added
print "Lines removed in .java:", removed
print "Net lines:", added - removed
}'
git log --author="$AUTHOR" --pretty=tformat: --numstat -- . ':(exclude)*Test.java' \
| awk '
$3 ~ /\.java$/ {
added += $1
removed += $2
}
END {
print "Lines added in .java (excluding *Test.java):", added
print "Lines removed in .java (excluding *Test.java):", removed
print "Net lines:", added - removed
}'

59
docs/agenda/agenda-03.md Normal file
View file

@ -0,0 +1,59 @@
# Week 3 meeting agenda
| Key | Value |
| ------------ | -------------------------------------------------------- |
| Date | Nov 28th |
| Time | 16:45 |
| Location | DW PC Hall 2 |
| Chair | Steven Liu |
| Minute Taker | Oskar Rasieński |
| Attendees | Mei Chang van der Werff, Natalia Cholewa, Rithvik Sriram |
## Table of contents
1. (1 min) Introduction by chair
2. (1 min) Any additions to the agenda?
3. (1-2 min) TA announcements?
4. (1-2 min) New submission of Code of Conduct + additions?
5. (30 min) **Meeting content**
1. (2 min) Week 3 progress checklist, MRs, and summary
- Have we completed all of basic requirements? If not, what to resolve as soon as possible.
2. (3 min) Socket modelling - what do we (don't) want to update with WebSockets
1. How should we model individual changes, e.g. what happens when the user adds a new recipe? or add an ingredient to a recipe?
3. (3 min) Discussion on implementing nutritional value calculations
4. (1-2 min) Questions and/or suggestions
5. (20 min) **Sprint planning week 4**
1. (2-3 min) Set a provisional deadline for completing parts of the backlog
2. (2 min) Confirm week 4 backend/frontend team formation
3. (5 min) Tasks creation (GitLab) and assignment
4. (5 min) Decide on a TODO list that each team should complete before the next meeting (see below for a provisional proposal)
5. (5 min) Other issues
6. (2 min) Questions
**Max time:** ~35 minutes
## Addendum
### Provisional week 3 tasks checklist
> May have further additions after the pre-meeting session.
**Backend**:
- [ ] Basic recipe definition
- [ ] API endpoints that provide Recipe CRUD functionality
- [ ] Unit tests for the endpoints
- [ ] ...
**Frontend**:
- [ ] A list of recipe names to choose from
- [ ] An interface to display a recipe when it is chosen from the recipe list
- [ ] (Functional) buttons in the interface
- [ ] Abstracted requests sending to the backend
- [ ] ...
## Provisional week 4 list of tasks
**Backend**:
- 4.2 content
- WebSockets service implementation
- 4.3 content
- implementing actual formal/informal ingredients and appropriate unit class definitions
- ... more to add
**Frontend**:
- Make sure the basic frontend functionality are in place and works as expected
- Improving ingredient/steps editing + deletion to work with the new Ingredient and Unit types
- Making a new scene/view + buttons for the Nutrition view
- Make the interface text ResourceBundle based to implement internationalisation
- ... more to add

68
docs/agenda/agenda-04.md Normal file
View file

@ -0,0 +1,68 @@
# Week 3 meeting agenda
| Key | Value |
| ------------ |-----------------------------------------------------------------------------------------|
| Date | Dec 5th |
| Time | 16:45 |
| Location | DW PC Hall 2 |
| Chair | Oskar Rasieński |
| Minute Taker | Rithvik Sriram |
| Attendees | Mei Chang van der Werff, Natalia Cholewa, Rithvik Sriram, Aysegul Aydinlik, Steven Liu |
## Table of contents
1. (1 min) Introduction by chair
2. (1 min) Any additions to the agenda?
3. (1-2 min) TA announcements?
4. (1 min) Buddy check reminder.
5. (1 min) Next meeting thursday after midterms.
6. (33 min) **Meeting content**
1. (5 min) Week 4 progress checklist, MRs, and summary
- Was 4.1 completed?
- Was 4.2 completed?
- Progress 4.3
- Progress 4.6
2. (1-2 min) Questions and/or suggestions
3. (3 min) Showcase
4. (20 min) **Sprint planning week 5-6**
- What needs to be done?
- How do we do it?
- Who does it?
5. (2 min) Designate teams.
6. (1 min) Who is the next chair and minute taker?
7. (2 min) Questions
8. (1 min) Summarize everything
**Max time:** ~40 minutes
## Addendum
### Provisional week 4 tasks checklist
**Backend**:
- [ ] Ingredient and Unit class definitions
- [ ] Integrate new Ingredient class into Recipe
- [ ] Websocket backend
**Frontend**:
- [ ] Integrate everything from last week together
- [ ] Remove example code
- [ ] Print export
- [ ] Edit button
- [ ] Nutritional value view
- [ ] Make ingredients unit based instead of string
- [ ] Websockets client-side
- [ ] Resource bundles for localization
## Provisional week 6 list of tasks
**Backend**:
- 4.4
- Warning when favourite recipes has been deleted by someone else. (Likely through ws)
...
**Frontend**:
- 4.2
- Finish websocket client side.
- 4.3
- Finish any uncompleted tasks.
- 4.4
- Favourite recipes (tied to id, only local)
- Warning when favourite recipes has been deleted by someone else.
- Search bar with a basic query syntax. (Filters existing List, not a new window)
- Favourites category (filter)
...

67
docs/feedback/week-03.md Normal file
View file

@ -0,0 +1,67 @@
# Meeting feedback
**Quick Summary Scale**
Insufficient/Sufficient/Good/Excellent
#### Agenda
Feedback: **Good - Excellent**
- The agenda was uploaded on time.
- The agenda was formatted according to the template.
- The individual points were clear.
- I liked that you included an addendum and a provisional list of tasks for everyone.
- You can slightly adjust the layout of the agenda to be easier to read. For example, you can have sections like Opening, Updates, Feedback, Closing etc - you can use anything you want.
#### Performance of the Previous Minute Taker
Feedback: **Good - Excellent**
- The notes have not been merged into the agenda. Try to have a single document for agenda + minutes.
- The notes cover most of the discussed points.
- The notes are clear. I liked how you distributed them based on discussed topics. Try to have a similar approach but merged with the agenda.
- The notes contain concrete agreements and evryone was assigned to a task.
- All agreements are actionable and realistic.
- A table showing task distribution could be useful.
#### Chair performance
Feedback: **Excellent**
- You did a good job with starting the meeting and checking in with everyone.
- You covered all points from the agenda.
- I liked that you started with a quick summary of how last week went.
- You followed and explained all your points and asked for the opinion of the others.
- I liked that you took the lead and kept to the agenda.
- I also liked that you did a quick summary at the end.
#### Attitude & Relation
Feedback: **Excellent**
- Overall the atmosphere was constructive and positive.
- All ideas were listened to and considered by all team members.
- Everyone was active an dinvolved in the meeting.
- Everyone took ownership of the meeting.
- I liked that you clarified the topics for everyone whenever they asked.
#### Potentially Shippable Produc
Feedback: **Sufficient**
- The team didn't present the current state of the application, however I understand that you worked on it.
- Make sure that next week you have a potentially shippable product.
- For the future meetings you can reserve a few minutes for showing the application.
- Progress has been done compared to last week.
#### Work Contribution/Distribution in the Team
Feedback: **Good**
- You can start by reviewing the action list of last week to get a clear view of what has been completed.
- Most of you reached your goal, make sure that everyone is still on track.
- So far it feels like everyone is contributing equally to the team, and I like that you are willing to help when someone doesn't know something.

View file

@ -0,0 +1,52 @@
# Week 3 meeting minutes
| Key | Value |
| ------------ | -------------------------------------------------------- |
| Date | Nov 28th |
| Time | 16:45 |
| Location | DW PC Hall 2 |
| Chair | Steven Liu |
| Minute Taker | Oskar Rasieński |
| Attendees | Mei Chang van der Werff, Natalia Cholewa, Rithvik Sriram |
## Announcements
- Gitlab will be down on week 5 wednesday until week 6 wednesday.
- Do Buddy check (brightspace).
## Completed tasks:
**Backend**:
- Basic recipe definition
- API endpoints that provide Recipe CRUD functionality
- Unit tests for the endpoints
**Frontend**:
- A list of recipe names to choose from (Interface)
- An interface to display a recipe when it is chosen from the recipe list
- Server utils (Abstracted requests sending to the backend)
## Unfinished tasks:
- Integration of all subsystems.
- print export
## Teams in week 4:
- Natalia and Ayse: Backend
- Oskar, Steven, Rithvik, Mei: Frontend
## TODO in week 4:
- Integration of all subsystems. (4.1)
- print export (4.1)
- Websocket (4.2)
- Start with 4.3, and try to do as much as possible.
## Nutritional Value calculations considerations:
- Don't include informal units in nutritional value calculations. (ignore them and set their value to 0)
- Doesn't really matter whether its server side or client side calculated.
## Remarks:
- When editing recipes we'll replace the whole recipe with the new edited recipe.
- In issues and MRs, track time with /spent and /estimate
- If MR doesn't close issue, say its related to an issue with #issuenr
- Use resource bundles instead of hardcoding text in JFX.
- Put server url in config.
Meeting started: 16:45 and ended: 17:10 (25 mins).