370 lines
11 KiB
Java
370 lines
11 KiB
Java
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.PrintExportService;
|
|
import client.utils.server.ServerUtils;
|
|
import client.utils.WebSocketDataService;
|
|
import com.google.inject.Inject;
|
|
import commons.Recipe;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.nio.file.Path;
|
|
import java.util.Optional;
|
|
import java.util.function.BiConsumer;
|
|
import java.util.function.Consumer;
|
|
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.control.TextInputDialog;
|
|
import javafx.scene.control.ComboBox;
|
|
import javafx.scene.input.KeyCode;
|
|
import javafx.scene.layout.HBox;
|
|
import javafx.scene.layout.VBox;
|
|
import javafx.scene.text.Font;
|
|
import javafx.stage.DirectoryChooser;
|
|
|
|
/**
|
|
* Controller for the recipe detail view.
|
|
* Manages displaying and editing recipe details such as name, ingredients, and
|
|
* steps.
|
|
*/
|
|
public class RecipeDetailCtrl implements LocaleAware {
|
|
private final LocaleManager localeManager;
|
|
private final ServerUtils server;
|
|
private final FoodpalApplicationCtrl appCtrl;
|
|
private final ConfigService configService;
|
|
private final WebSocketDataService<Long, Recipe> webSocketDataService;
|
|
|
|
@FXML
|
|
private IngredientListCtrl ingredientListController;
|
|
|
|
@FXML
|
|
private RecipeStepListCtrl stepListController;
|
|
|
|
private Recipe recipe;
|
|
|
|
@Inject
|
|
public RecipeDetailCtrl(
|
|
LocaleManager localeManager,
|
|
ServerUtils server,
|
|
FoodpalApplicationCtrl appCtrl,
|
|
ConfigService configService,
|
|
WebSocketDataService<Long, Recipe> webSocketDataService) {
|
|
this.localeManager = localeManager;
|
|
this.server = server;
|
|
this.appCtrl = appCtrl;
|
|
this.configService = configService;
|
|
this.webSocketDataService = webSocketDataService;
|
|
}
|
|
|
|
@FXML
|
|
private VBox detailsScreen;
|
|
|
|
@FXML
|
|
private HBox editableTitleArea;
|
|
|
|
@FXML
|
|
private Button editRecipeTitleButton;
|
|
|
|
@FXML
|
|
private Button removeRecipeButton;
|
|
|
|
@FXML
|
|
private Button printRecipeButton;
|
|
|
|
@FXML
|
|
private Button favouriteButton;
|
|
|
|
@FXML
|
|
private ComboBox<String> langSelector;
|
|
|
|
private ListView<Recipe> 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<Recipe> 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.langSelector.setValue(recipe.getLocale());
|
|
this.refreshFavouriteButton();
|
|
}
|
|
|
|
/**
|
|
* Create a callback that takes in the selected recipe and a helper type
|
|
* and updates the recipe based on that.
|
|
* Also, posts the update to the server.
|
|
*
|
|
* @param recipeConsumer The helper function to use when updating the recipe -
|
|
* how to update it
|
|
* @return The created callback to use in a setUpdateCallback or related
|
|
* function
|
|
*/
|
|
private <T> Consumer<T> createUpdateRecipeCallback(BiConsumer<Recipe, T> recipeConsumer) {
|
|
return recipes -> {
|
|
Recipe selectedRecipe = this.getSelectedRecipe().orElseThrow(
|
|
() -> new NullPointerException(
|
|
"Null recipe whereas ingredients are edited"));
|
|
|
|
recipeConsumer.accept(selectedRecipe, recipes);
|
|
|
|
try { // propagate changes to server
|
|
server.updateRecipe(selectedRecipe);
|
|
webSocketDataService.add(selectedRecipe.getId(), recipe -> {
|
|
int idx = getParentRecipeList().getItems().indexOf(selectedRecipe);
|
|
getParentRecipeList().getItems().set(idx, recipe);
|
|
});
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new UpdateException("Unable to update recipe to server for " +
|
|
selectedRecipe);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initializes the ingredient and step list controllers with update callbacks.
|
|
*/
|
|
private void initStepsIngredientsList() {
|
|
// code does NOT get nicer than this :pray:
|
|
|
|
// Initialize callback for ingredient list updates
|
|
this.ingredientListController.setUpdateCallback(
|
|
this.createUpdateRecipeCallback(Recipe::setIngredients));
|
|
// Ditto, for step list updates
|
|
this.stepListController.setUpdateCallback(
|
|
this.createUpdateRecipeCallback(Recipe::setPreparationSteps));
|
|
}
|
|
|
|
/**
|
|
* Revised edit recipe control flow, deprecates the use of a separate
|
|
* AddNameCtrl. This is automagically called when a new recipe is created,
|
|
* making for a more seamless UX.
|
|
* Public for reference by ApplicationCtrl.
|
|
*/
|
|
@FXML
|
|
public 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();
|
|
}
|
|
|
|
/**
|
|
* 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()) ? "★"
|
|
: "☆");
|
|
}
|
|
|
|
/**
|
|
* Gives the User a download file prompt and lets the
|
|
* user change the name of the downloaded file and
|
|
* uses printExportService to create a downloadable file.
|
|
*/
|
|
@FXML
|
|
private void printRecipe() {
|
|
if (recipe == null) {
|
|
return; // Do nothing if no recipe selected
|
|
}
|
|
|
|
// Open directory chooser
|
|
DirectoryChooser directoryChooser = new DirectoryChooser();
|
|
directoryChooser.setTitle("Select Folder to Save Recipe");
|
|
|
|
File selectedDirectory = directoryChooser.showDialog(
|
|
printRecipeButton.getScene().getWindow());
|
|
|
|
if (selectedDirectory == null) {
|
|
return; // User cancelled
|
|
}
|
|
|
|
// Ask for filename
|
|
TextInputDialog dialog = new TextInputDialog(recipe.getName() + ".txt");
|
|
dialog.setTitle("Save Recipe");
|
|
dialog.setHeaderText("Enter filename for the recipe");
|
|
dialog.setContentText("Filename:");
|
|
|
|
Optional<String> result = dialog.showAndWait();
|
|
|
|
if (result.isPresent()) {
|
|
String filename = result.get();
|
|
if (!filename.endsWith(".txt")) {
|
|
filename = filename + ".txt";
|
|
}
|
|
|
|
// Use PrintExportService methods
|
|
String recipeText = PrintExportService.buildRecipeText(recipe);
|
|
Path dirPath = selectedDirectory.toPath();
|
|
PrintExportService.exportToFile(recipeText, dirPath, filename);
|
|
}
|
|
}
|
|
/**
|
|
* 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
|
|
refreshFavouriteButton();
|
|
appCtrl.refresh();
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Switch the recipe's language.
|
|
*/
|
|
@FXML
|
|
void changeLanguage() {
|
|
recipe.setLocale(this.langSelector.getValue());
|
|
|
|
try {
|
|
server.updateRecipe(this.recipe);
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new UpdateException("Error occurred when updating recipe locale!");
|
|
}
|
|
}
|
|
|
|
@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();
|
|
|
|
langSelector.getItems().addAll("en", "nl", "pl");
|
|
}
|
|
}
|