diff --git a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java index 727d1bc..56f6c6e 100644 --- a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java +++ b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java @@ -7,9 +7,7 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import client.exception.UpdateException; -import client.scenes.recipe.IngredientListCtrl; -import client.scenes.recipe.RecipeStepListCtrl; +import client.scenes.recipe.RecipeDetailCtrl; import client.utils.Config; import client.utils.ConfigService; @@ -30,24 +28,18 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; -import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; -import javafx.scene.input.KeyCode; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; -import javafx.scene.text.Font; public class FoodpalApplicationCtrl implements LocaleAware { private final ServerUtils server; private final WebSocketUtils webSocketUtils; private final LocaleManager localeManager; - private final IngredientListCtrl ingredientListCtrl; - private final RecipeStepListCtrl stepListCtrl; - private SearchBarCtrl searchBarCtrl; + @FXML + private RecipeDetailCtrl recipeDetailController; - public VBox detailsScreen; - public HBox editableTitleArea; + @FXML + private SearchBarCtrl searchBarController; // everything in the left lane @FXML @@ -65,25 +57,10 @@ public class FoodpalApplicationCtrl implements LocaleAware { @FXML public Button cloneRecipeButton; - @FXML - private Button favouriteButton; - // stores the users favourites and load and saves them private Config config; private final ConfigService configService; - - // === CENTER: RECIPE DETAILS === - - @FXML - private Button editRecipeTitleButton; - - @FXML - public Button removeRecipeButton2; - - @FXML - public Button printRecipeButton; - @FXML private ToggleButton favouritesOnlyToggle; @@ -95,20 +72,17 @@ public class FoodpalApplicationCtrl implements LocaleAware { ServerUtils server, WebSocketUtils webSocketUtils, LocaleManager localeManager, - IngredientListCtrl ingredientListCtrl, - RecipeStepListCtrl stepListCtrl, ConfigService configService ) { this.server = server; this.webSocketUtils = webSocketUtils; this.localeManager = localeManager; - this.ingredientListCtrl = ingredientListCtrl; - this.stepListCtrl = stepListCtrl; this.configService = configService; initializeWebSocket(); } + private void initRecipeList() { // Show recipe name in the list recipeList.setCellFactory(list -> new ListCell<>() { @@ -126,20 +100,14 @@ public class FoodpalApplicationCtrl implements LocaleAware { // When your selection changes, update details in the panel recipeList.getSelectionModel().selectedItemProperty().addListener( (obs, oldRecipe, newRecipe) -> { - showRecipeDetails(newRecipe); - updateFavouriteButton(newRecipe); + this.recipeDetailController.setCurrentlyViewedRecipe(newRecipe); } ); } - @Inject - void setSearchBarCtrl(SearchBarCtrl searchBarCtrl) { - this.searchBarCtrl = searchBarCtrl; - } - private void initializeSearchBar() { // Refresh on search bar change - this.searchBarCtrl.setOnSearch(recipes -> { + this.searchBarController.setOnSearch(recipes -> { // Don't lose selection on refresh Recipe currentlySelected = recipeList.getSelectionModel().getSelectedItem(); int newIndex = -1; @@ -178,42 +146,15 @@ public class FoodpalApplicationCtrl implements LocaleAware { .findFirst() .orElse(null); - showRecipeDetails(recipeInList); + this.recipeDetailController.setCurrentlyViewedRecipe(recipeInList); }); // runLater as it's on another non-FX thread. }); }); } - private void initStepsIngredientsList() { - // Initialize callback for ingredient list updates - this.ingredientListCtrl.setUpdateCallback(newList -> { - Recipe selectedRecipe = recipeList.getSelectionModel().getSelectedItem(); - if (selectedRecipe == null) { // edge case error for NPE. - throw new NullPointerException("Null recipe whereas ingredients are edited"); - } - selectedRecipe.setIngredients(newList); - try { // propagate changes to server - server.updateRecipe(selectedRecipe); - } catch (IOException | InterruptedException e) { - throw new UpdateException("Unable to update recipe to server for " + selectedRecipe); - } - }); - this.stepListCtrl.setUpdateCallback(newList -> { - Recipe selectedRecipe = recipeList.getSelectionModel().getSelectedItem(); - if (selectedRecipe == null) { // edge case error for NPE. - throw new NullPointerException("Null recipe whereas ingredients are edited"); - } - selectedRecipe.setPreparationSteps(newList); - try { // propagate changes to server - server.updateRecipe(selectedRecipe); - } catch (IOException | InterruptedException e) { - throw new UpdateException("Unable to update recipe to server for " + selectedRecipe); - } - }); - } + @Override public void initializeComponents() { config = configService.getConfig(); - initStepsIngredientsList(); initRecipeList(); // Double-click to go to detail screen @@ -223,25 +164,18 @@ public class FoodpalApplicationCtrl implements LocaleAware { openSelectedRecipe(); } }); + this.initializeSearchBar(); refresh(); - updateFavouriteButton(recipeList.getSelectionModel().getSelectedItem()); - } - private void showName(String name) { - final int NAME_FONT_SIZE = 20; - editableTitleArea.getChildren().clear(); - Label nameLabel = new Label(name); - nameLabel.setFont(new Font("System Bold", NAME_FONT_SIZE)); - editableTitleArea.getChildren().add(nameLabel); + + this.recipeDetailController.setCurrentlyViewedRecipe(recipeList.getSelectionModel().getSelectedItem()); } + @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")); } @@ -250,21 +184,12 @@ public class FoodpalApplicationCtrl implements LocaleAware { return localeManager; } - private void showRecipeDetails(Recipe recipe) { - if (recipe == null) { - return; - } - showName(recipe.getName()); - ingredientListCtrl.refetchFromRecipe(recipe); - stepListCtrl.refetchFromRecipe(recipe); - } - // Button handlers @FXML public void refresh() { List recipes; try { - recipes = server.getRecipesFiltered(searchBarCtrl.getFilter()); + recipes = server.getRecipesFiltered(searchBarController.getFilter()); } catch (IOException | InterruptedException e) { recipes = Collections.emptyList(); System.err.println("Failed to load recipes: " + e.getMessage()); @@ -303,11 +228,24 @@ public class FoodpalApplicationCtrl implements LocaleAware { printError("Error occurred when adding recipe!"); } // Calls edit after the recipe has been created, seamless name editing - editRecipeTitle(); + // editRecipeTitle(); } @FXML - private void removeSelectedRecipe() throws IOException, InterruptedException { + private void openSelectedRecipe() { + Recipe selected = recipeList.getSelectionModel().getSelectedItem(); + if (selected == null) { + return; + } + + this.recipeDetailController.setVisible(true); + } + + /** + * Removes the selected recipe from the server and refreshes the recipe list. + */ + @FXML + public void removeSelectedRecipe() throws IOException, InterruptedException { Recipe selected = recipeList.getSelectionModel().getSelectedItem(); if (selected == null) { return; @@ -321,52 +259,6 @@ public class FoodpalApplicationCtrl implements LocaleAware { refresh(); } - @FXML - private void openSelectedRecipe() { - Recipe selected = recipeList.getSelectionModel().getSelectedItem(); - if (selected == null) { - return; - } - detailsScreen.visibleProperty().set(true); - } - - /** - * Revised edit recipe control flow, deprecates the use of a separate AddNameCtrl. - * This is automagically called when a new recipe is created, making for a more seamless UX. - */ - @FXML - private void editRecipeTitle() { - editableTitleArea.getChildren().clear(); - TextField edit = new TextField(); - edit.setOnKeyPressed(event -> { - if (event.getCode() != KeyCode.ENTER) { - return; - } - String newName = edit.getText(); - Recipe selected = recipeList.getSelectionModel().getSelectedItem(); - if (selected == null) { - // edge case, prob won't happen but best to handle it later - throw new NullPointerException("No recipe selected while name was changed!"); - } - selected.setName(newName); - try { - server.updateRecipe(selected); - refresh(); - //recipeList.getSelectionModel().select(selected); - } catch (IOException | InterruptedException e) { - // throw a nice blanket UpdateException - throw new UpdateException("Error occurred when updating recipe name!"); - } - showName(edit.getText()); - }); - editableTitleArea.getChildren().add(edit); - edit.requestFocus(); - } - @FXML - private void makePrintable() { - System.out.println("Recipe printed"); - } - /** * Clones a recipe, when clicking on the button "clone". */ @@ -381,48 +273,19 @@ public class FoodpalApplicationCtrl implements LocaleAware { recipeList.getSelectionModel().select(cloned); } - private void updateFavouriteButton(Recipe recipe) { - if (recipe == null) { - favouriteButton.setDisable(true); - favouriteButton.setText("☆"); - return; - } - - favouriteButton.setDisable(false); - favouriteButton.setText( - config.isFavourite(recipe.getId()) ? "★" : "☆" - ); - } - - @FXML - private void toggleFavourite() { - Recipe selected = recipeList.getSelectionModel().getSelectedItem(); - if (selected == null) { - return; - } - - long id = selected.getId(); - - if (config.isFavourite(id)) { - config.removeFavourite(id); - } else { - config.addFavourite(id); - } - - configService.save(); - - //instant ui update - applyRecipeFilterAndKeepSelection(); - recipeList.refresh(); - updateFavouriteButton(selected); - } - @FXML private void toggleFavouritesView() { applyRecipeFilterAndKeepSelection(); } - private void applyRecipeFilterAndKeepSelection() { + /** + * Applies the recipe filter (favourites only or all) while keeping the current selection if possible. + * If the previously selected recipe is no longer visible after applying the filter, + * the first recipe in the filtered list will be selected instead. + * If there are no recipes after filtering, no selection will be made. + * + */ + public void applyRecipeFilterAndKeepSelection() { Recipe selected = recipeList.getSelectionModel().getSelectedItem(); Long selectedId = selected == null ? null : selected.getId(); @@ -452,8 +315,8 @@ public class FoodpalApplicationCtrl implements LocaleAware { recipeList.getSelectionModel().selectFirst(); } - updateFavouriteButton(recipeList.getSelectionModel().getSelectedItem()); - detailsScreen.visibleProperty().set(!recipeList.getItems().isEmpty()); + this.recipeDetailController.refreshFavouriteButton(); + this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty()); } } diff --git a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java new file mode 100644 index 0000000..3d6441f --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java @@ -0,0 +1,296 @@ +package client.scenes.recipe; + +import client.exception.UpdateException; +import client.scenes.FoodpalApplicationCtrl; +import client.utils.Config; +import client.utils.ConfigService; +import client.utils.LocaleAware; +import client.utils.LocaleManager; +import client.utils.ServerUtils; +import com.google.inject.Inject; +import commons.Recipe; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + +import java.io.IOException; +import java.util.Optional; + +/** + * Controller for the recipe detail view. + * Manages displaying and editing recipe details such as name, ingredients, and steps. + */ +public class RecipeDetailCtrl implements LocaleAware { + private final LocaleManager localeManager; + private final ServerUtils server; + private final FoodpalApplicationCtrl appCtrl; + private final ConfigService configService; + + @FXML + private IngredientListCtrl ingredientListController; + + @FXML + private RecipeStepListCtrl stepListController; + + private Recipe recipe; + + @Inject + public RecipeDetailCtrl(LocaleManager localeManager, + ServerUtils server, + FoodpalApplicationCtrl appCtrl, + ConfigService configService) { + this.localeManager = localeManager; + this.server = server; + this.appCtrl = appCtrl; + this.configService = configService; + } + + @FXML + private VBox detailsScreen; + + @FXML + private HBox editableTitleArea; + + @FXML + private Button editRecipeTitleButton; + + @FXML + private Button removeRecipeButton; + + @FXML + private Button printRecipeButton; + + @FXML + private Button favouriteButton; + + private ListView getParentRecipeList() { + return this.appCtrl.recipeList; + } + + /** + * Convenience method for a frequently needed operation to retrieve the currently + * selected recipe. + * + * @return The currently selected recipe in the parent recipe list. + */ + private Optional getSelectedRecipe() { + return Optional.ofNullable( + this.getParentRecipeList().getSelectionModel().getSelectedItem() + ); + } + + /** + * Convenience method for a frequently needed operation to retrieve the global + * application configuration. + * + * @return The application configuration. + */ + private Config getConfig() { + return this.configService.getConfig(); + } + + /** + * Refreshes the recipe list from the server. + * + * @throws IOException Upon invalid recipe response. + * @throws InterruptedException Upon request interruption. + * + * @see FoodpalApplicationCtrl#refresh() + */ + private void refresh() throws IOException, InterruptedException { + this.appCtrl.refresh(); + } + + /** + * Toggles the visibility of the recipe details screen. + * + * @param visible Whether the details screen should be visible. + */ + public void setVisible(boolean visible) { + this.detailsScreen.setVisible(visible); + } + + /** + * Sets the currently viewed recipe in the detail view. + * + * @param recipe The recipe to view. + */ + public void setCurrentlyViewedRecipe(Recipe recipe) { + if (recipe == null) { + return; + } + + this.recipe = recipe; + + this.showName(recipe.getName()); + this.ingredientListController.refetchFromRecipe(recipe); + this.stepListController.refetchFromRecipe(recipe); + this.refreshFavouriteButton(); + } + + /** + * Initializes the ingredient and step list controllers with update callbacks. + */ + private void initStepsIngredientsList() { + // Initialize callback for ingredient list updates + this.ingredientListController.setUpdateCallback(newList -> { + var maybeSelected = this.getSelectedRecipe(); + if (maybeSelected.isEmpty()) { // Safely handle edge case + throw new NullPointerException("Null recipe whereas ingredients are edited"); + } + + Recipe selectedRecipe = maybeSelected.get(); + selectedRecipe.setIngredients(newList); + try { // propagate changes to server + server.updateRecipe(selectedRecipe); + } catch (IOException | InterruptedException e) { + throw new UpdateException("Unable to update recipe to server for " + selectedRecipe); + } + }); + + // Ditto, for step list updates + this.stepListController.setUpdateCallback(newList -> { + var maybeSelected = this.getSelectedRecipe(); + if (maybeSelected.isEmpty()) { // Safely handle edge case + throw new NullPointerException("Null recipe whereas ingredients are edited"); + } + + Recipe selectedRecipe = maybeSelected.get(); + selectedRecipe.setPreparationSteps(newList); + try { // propagate changes to server + server.updateRecipe(selectedRecipe); + } catch (IOException | InterruptedException e) { + throw new UpdateException("Unable to update recipe to server for " + selectedRecipe); + } + }); + } + + /** + * Revised edit recipe control flow, deprecates the use of a separate AddNameCtrl. + * This is automagically called when a new recipe is created, making for a more seamless UX. + */ + @FXML + private void editRecipeTitle() { + this.editableTitleArea.getChildren().clear(); + TextField edit = new TextField(); + + edit.setOnKeyPressed(event -> { + if (event.getCode() != KeyCode.ENTER) { + return; + } + + String newName = edit.getText(); + this.recipe.setName(newName); + + try { + server.updateRecipe(this.recipe); + this.refresh(); + } catch (IOException | InterruptedException e) { + // throw a nice blanket UpdateException + throw new UpdateException("Error occurred when updating recipe name!"); + } + + this.showName(edit.getText()); + }); + + editableTitleArea.getChildren().add(edit); + edit.requestFocus(); + } + + /** + * Remove the selected recipe from the server and refresh the recipe list. + * Internally calls {@link FoodpalApplicationCtrl#removeSelectedRecipe()}. + */ + @FXML + private void removeSelectedRecipe() throws IOException, InterruptedException { + this.appCtrl.removeSelectedRecipe(); + } + + @FXML + /** + * Print the currently viewed recipe. + */ + private void printRecipe() { + // TODO: actually make it print? + System.out.println("Recipe printed"); + } + + /** + * Refreshes the favourite button state based on whether the currently viewed recipe + * is marked as a favourite in the application configuration. + */ + public void refreshFavouriteButton() { + if (recipe == null) { + favouriteButton.setDisable(true); + favouriteButton.setText("☆"); + return; + } + + favouriteButton.setDisable(false); + favouriteButton.setText( + this.getConfig().isFavourite(recipe.getId()) ? "★" : "☆" + ); + } + + /** + * Toggles the favourite status of the currently viewed recipe in the application configuration + * and writes the changes to disk. + */ + @FXML + private void toggleFavourite() { + long id = this.recipe.getId(); + + Config config = this.getConfig(); + if (config.isFavourite(id)) { + config.removeFavourite(id); + } else { + config.addFavourite(id); + } + + configService.save(); + + //instant ui update + appCtrl.applyRecipeFilterAndKeepSelection(); + this.getParentRecipeList().refresh(); + refreshFavouriteButton(); + } + + /** + * Updates the title area to show the given name. + * + * @param name The name to show. + */ + private void showName(String name) { + final int NAME_FONT_SIZE = 20; + + editableTitleArea.getChildren().clear(); + + Label nameLabel = new Label(name); + nameLabel.setFont(new Font("System Bold", NAME_FONT_SIZE)); + + editableTitleArea.getChildren().add(nameLabel); + } + + @Override + public void updateText() { + editRecipeTitleButton.setText(getLocaleString("menu.button.edit")); + removeRecipeButton.setText(getLocaleString("menu.button.remove.recipe")); + printRecipeButton.setText(getLocaleString("menu.button.print")); + } + + @Override + public LocaleManager getLocaleManager() { + return this.localeManager; + } + + @Override + public void initializeComponents() { + initStepsIngredientsList(); + } +} diff --git a/client/src/main/resources/client/scenes/FoodpalApplication.fxml b/client/src/main/resources/client/scenes/FoodpalApplication.fxml index 6b8750b..310e78d 100644 --- a/client/src/main/resources/client/scenes/FoodpalApplication.fxml +++ b/client/src/main/resources/client/scenes/FoodpalApplication.fxml @@ -46,7 +46,7 @@ - + @@ -61,27 +61,7 @@
- - - - - - - - - -
diff --git a/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml b/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml new file mode 100644 index 0000000..ebd3a61 --- /dev/null +++ b/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + +