diff --git a/client/src/main/java/client/Main.java b/client/src/main/java/client/Main.java index f7c5a00..e722b2d 100644 --- a/client/src/main/java/client/Main.java +++ b/client/src/main/java/client/Main.java @@ -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); } } \ No newline at end of file diff --git a/client/src/main/java/client/MyModule.java b/client/src/main/java/client/MyModule.java index 2f6678b..f0d95ed 100644 --- a/client/src/main/java/client/MyModule.java +++ b/client/src/main/java/client/MyModule.java @@ -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); } } \ No newline at end of file diff --git a/client/src/main/java/client/scenes/AddIngredientCtrl.java b/client/src/main/java/client/scenes/AddIngredientCtrl.java new file mode 100644 index 0000000..70d6924 --- /dev/null +++ b/client/src/main/java/client/scenes/AddIngredientCtrl.java @@ -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; + } + } + +} \ No newline at end of file diff --git a/client/src/main/java/client/scenes/AddQuoteCtrl.java b/client/src/main/java/client/scenes/AddNameCtrl.java similarity index 56% rename from client/src/main/java/client/scenes/AddQuoteCtrl.java rename to client/src/main/java/client/scenes/AddNameCtrl.java index 0dcf39f..73f86e6 100644 --- a/client/src/main/java/client/scenes/AddQuoteCtrl.java +++ b/client/src/main/java/client/scenes/AddNameCtrl.java @@ -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; } } + } \ No newline at end of file diff --git a/client/src/main/java/client/scenes/AddStepsCtrl.java b/client/src/main/java/client/scenes/AddStepsCtrl.java new file mode 100644 index 0000000..abd9f51 --- /dev/null +++ b/client/src/main/java/client/scenes/AddStepsCtrl.java @@ -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; + } + } + +} \ No newline at end of file diff --git a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java new file mode 100644 index 0000000..9257bfc --- /dev/null +++ b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java @@ -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 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 ingredientsListView; + + @FXML + private Button addIngredientButton; + + @FXML + private ListView 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 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(); + + } + +} + + + + diff --git a/client/src/main/java/client/scenes/MainCtrl.java b/client/src/main/java/client/scenes/MainCtrl.java index f231fb6..0ad2f49 100644 --- a/client/src/main/java/client/scenes/MainCtrl.java +++ b/client/src/main/java/client/scenes/MainCtrl.java @@ -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 addName, + Pair foodpal, + Pair addIngredient, + Pair addStep) throws IOException, InterruptedException { - public void initialize(Stage primaryStage, Pair overview, - Pair 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); + } + }); + } } \ No newline at end of file diff --git a/client/src/main/java/client/scenes/QuoteOverviewCtrl.java b/client/src/main/java/client/scenes/QuoteOverviewCtrl.java deleted file mode 100644 index d97ebaf..0000000 --- a/client/src/main/java/client/scenes/QuoteOverviewCtrl.java +++ /dev/null @@ -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 data; - - @FXML - private TableView table; - @FXML - private TableColumn colFirstName; - @FXML - private TableColumn colLastName; - @FXML - private TableColumn 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); - } -} \ No newline at end of file diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCell.java b/client/src/main/java/client/scenes/recipe/IngredientListCell.java new file mode 100644 index 0000000..ac317d8 --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/IngredientListCell.java @@ -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 { + // 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); + } +} diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java new file mode 100644 index 0000000..74193e6 --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/IngredientListCtrl.java @@ -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 { + /** + *

+ * The list of ingredients currently displayed. + *
+ * As the ingredient list in {@link Recipe} is immutable, + * this copies that list upon initialization to this mutable list. + *
+ * Please use the {@link #setUpdateCallback(Function)} function to listen for + * changes and update the recipe accordingly. + *

+ */ + private ObservableList ingredients; + + /** + * A callback function that is called when the ingredient list is updated. + */ + private Function, Void> updateCallback; + + @FXML + ListView 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 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, 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 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(); + } +} diff --git a/client/src/main/java/client/scenes/recipe/RecipeStepListCell.java b/client/src/main/java/client/scenes/recipe/RecipeStepListCell.java new file mode 100644 index 0000000..6d6e992 --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/RecipeStepListCell.java @@ -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 { + /** + * 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); + } +} diff --git a/client/src/main/java/client/scenes/recipe/RecipeStepListCtrl.java b/client/src/main/java/client/scenes/recipe/RecipeStepListCtrl.java new file mode 100644 index 0000000..7ed9c36 --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/RecipeStepListCtrl.java @@ -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 { + /** + *

+ * The list of recipe steps currently displayed. + *
+ * As the step list in {@link Recipe} is immutable, + * this copies that list upon initialization to this mutable list. + *
+ * Please use the {@link #setUpdateCallback(Function)} function to listen for + * changes and update the recipe accordingly. + *

+ */ + private ObservableList steps; + + /** + * A callback function that is called when the step list is updated. + */ + private Function, Void> updateCallback; + + @FXML + ListView 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 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, 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 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(); + } +} diff --git a/client/src/main/java/client/utils/LocaleAware.java b/client/src/main/java/client/utils/LocaleAware.java new file mode 100644 index 0000000..c5bf9d6 --- /dev/null +++ b/client/src/main/java/client/utils/LocaleAware.java @@ -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() {} +} diff --git a/client/src/main/java/client/utils/LocaleManager.java b/client/src/main/java/client/utils/LocaleManager.java new file mode 100644 index 0000000..edf342a --- /dev/null +++ b/client/src/main/java/client/utils/LocaleManager.java @@ -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 currentLocale = new SimpleObjectProperty<>(Locale.ENGLISH); + private final ObjectProperty 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 getBundleProperty() { + return currentBundle; + } +} \ No newline at end of file diff --git a/client/src/main/java/client/utils/ServerUtils.java b/client/src/main/java/client/utils/ServerUtils.java index 483eed1..7bf5de9 100644 --- a/client/src/main/java/client/utils/ServerUtils.java +++ b/client/src/main/java/client/utils/ServerUtils.java @@ -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 getRecipes() throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(SERVER + "/recipes")) + .GET() + .build(); + HttpResponse 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>() { + });// JSON string-> List (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 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 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 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 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 getQuotes() { - return ClientBuilder.newClient(new ClientConfig()) // - .target(SERVER).path("api/quotes") // - .request(APPLICATION_JSON) // - .get(new GenericType>() {}); - } + /** + * 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 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 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 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 preparationSteps = new ArrayList<>(recipe.getPreparationSteps()); + preparationSteps.add(preparationStep); + recipe.setPreparationSteps(preparationSteps); + + updateRecipe(recipe); + } } \ No newline at end of file diff --git a/client/src/main/java/client/utils/ServerUtilsExample.java b/client/src/main/java/client/utils/ServerUtilsExample.java new file mode 100644 index 0000000..bc4a187 --- /dev/null +++ b/client/src/main/java/client/utils/ServerUtilsExample.java @@ -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 getQuotes() { + return ClientBuilder.newClient(new ClientConfig()) // + .target(SERVER).path("api/quotes") // + .request(APPLICATION_JSON) // + .get(new GenericType>() {}); + } + + 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; + } +} \ No newline at end of file diff --git a/client/src/main/resources/client/scenes/AddIngredient.fxml b/client/src/main/resources/client/scenes/AddIngredient.fxml new file mode 100644 index 0000000..5aaddde --- /dev/null +++ b/client/src/main/resources/client/scenes/AddIngredient.fxml @@ -0,0 +1,15 @@ + + + + + + + + + +