From a736bcdb350c2b3800461ad2cf02698a4b532743 Mon Sep 17 00:00:00 2001 From: Nicholas Ang Date: Fri, 13 Apr 2018 05:31:51 +0800 Subject: [PATCH] [v1.5rc] Collated Code (#150) * updated dg * more updates * changed to DG * done changing to sentences for implementation part * user guide * updated UG * user Guide * user guide * updated all images * changed caution * changed note * warning * danger * bulb * warning * userguide done * increase width * introduction PPP * formatting done * testing * collated code * deleted excess files --- collated/functional/RyanAngJY.md | 130 ++++- collated/functional/hoangduong1607.md | 177 +++++- collated/functional/kokonguyen191.md | 745 +++++++++++++++++++++++++- collated/test/RyanAngJY.md | 69 ++- collated/test/hoangduong1607.md | 152 +++++- collated/test/kokonguyen191.md | 244 ++++++++- docs/DeveloperGuide.adoc | 2 +- docs/RyanAng.adoc | 66 --- docs/UserGuide.adoc | 8 + docs/team/NicholasAng.adoc | 88 +++ 10 files changed, 1533 insertions(+), 148 deletions(-) delete mode 100644 docs/RyanAng.adoc create mode 100644 docs/team/NicholasAng.adoc diff --git a/collated/functional/RyanAngJY.md b/collated/functional/RyanAngJY.md index 051ca049fada..6f2e09daeace 100644 --- a/collated/functional/RyanAngJY.md +++ b/collated/functional/RyanAngJY.md @@ -24,6 +24,28 @@ public class ShareRecipeEvent extends BaseEvent { } } ``` +###### \java\seedu\recipe\commons\util\FileUtil.java +``` java + /** + * Checks if a given file is an image file. + * + * @return true if a given file is a valid image file. + */ + public static boolean isImageFile(File file) { + if (!isFileExists(file) || file.isDirectory()) { + return false; + } else { + try { + if (ImageIO.read(file) == null) { + return false; + } + } catch (IOException exception) { + System.out.println("Error reading file"); + } + return true; + } + } +``` ###### \java\seedu\recipe\logic\commands\ShareCommand.java ``` java /** @@ -184,6 +206,8 @@ import java.io.File; import java.net.URL; import seedu.recipe.MainApp; +import seedu.recipe.commons.util.FileUtil; +import seedu.recipe.storage.ImageDownloader; /** * Represents a Recipe's image in the address book. @@ -193,10 +217,14 @@ public class Image { public static final String NULL_IMAGE_REFERENCE = "-"; public static final String FILE_PREFIX = "file:"; - public static final String MESSAGE_IMAGE_CONSTRAINTS = "Image path should be valid"; + public static final String IMAGE_STORAGE_FOLDER = "data/images/"; + public static final String MESSAGE_IMAGE_CONSTRAINTS = "Image path should be valid," + + " file should be a valid image file"; public static final URL VALID_IMAGE = MainApp.class.getResource("/images/clock.png"); public static final String VALID_IMAGE_PATH = VALID_IMAGE.toExternalForm().substring(5); - public final String value; + + private String value; + private String imageName; /** * Constructs a {@code Image}. @@ -206,25 +234,44 @@ public class Image { public Image(String imagePath) { requireNonNull(imagePath); checkArgument(isValidImage(imagePath), MESSAGE_IMAGE_CONSTRAINTS); + if (ImageDownloader.isValidImageUrl(imagePath)) { + imagePath = ImageDownloader.downloadImage(imagePath); + } this.value = imagePath; + setImageName(); } /** - * Returns true if a given string is a valid file path, or no file path has been assigned + * Sets the name of the image file */ - public static boolean isValidImage(String testImagePath) { - if (testImagePath.equals(NULL_IMAGE_REFERENCE)) { - return true; + public void setImageName() { + if (this.value.equals(NULL_IMAGE_REFERENCE)) { + imageName = NULL_IMAGE_REFERENCE; + } else { + this.imageName = new File(this.value).getName(); } - File image = new File(testImagePath); - if (image.exists() && !image.isDirectory()) { - return true; + } + + public String getImageName() { + return imageName; + } + +``` +###### \java\seedu\recipe\model\recipe\Image.java +``` java + + /** + * Sets image path to follow internal image storage folder + */ + public void setImageToInternalReference() { + if (!imageName.equals(NULL_IMAGE_REFERENCE)) { + this.value = IMAGE_STORAGE_FOLDER + imageName; } - return false; } public String getUsablePath() { - return FILE_PREFIX + value; + File imagePath = new File(this.value); + return FILE_PREFIX + imagePath.getAbsolutePath(); } @Override @@ -392,6 +439,63 @@ public class HtmlFormatter { } } ``` +###### \java\seedu\recipe\storage\ImageStorage.java +``` java +package seedu.recipe.storage; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import seedu.recipe.commons.core.LogsCenter; +import seedu.recipe.commons.util.FileUtil; +import seedu.recipe.model.ReadOnlyRecipeBook; +import seedu.recipe.model.recipe.Image; + +/** + * A class to save RecipeBook image data stored on the hard disk. + */ +public class ImageStorage { + public static final String IMAGE_FOLDER = "images/"; + private static final String RECIPE_BOOK_FILENAME = "recipebook.xml"; + private static final String WARNING_UNABLE_TO_SAVE_IMAGE = "Image cannot be saved."; + + /** + * Saves all image files into the images folder of the application + * + * @param filePath location of the image. Cannot be null + */ + public static void saveAllImageFiles(ReadOnlyRecipeBook recipeBook, String filePath) throws IOException { + String imageFolderPath = filePath.replaceAll(RECIPE_BOOK_FILENAME, IMAGE_FOLDER); + File imageFolder = new File(imageFolderPath); + FileUtil.createDirs(imageFolder); + + for (int i = 0; i < recipeBook.getRecipeList().size(); i++) { + Image recipeImage = recipeBook.getRecipeList().get(i).getImage(); + saveImageFile(recipeImage.toString(), imageFolderPath); + recipeImage.setImageToInternalReference(); + } + } + + /** + * Saves an image file into the images folder of the application + * + * @param imagePath location of the image. Cannot be null + * @param imageFolderPath location of the image. Cannot be null + */ + public static void saveImageFile(String imagePath, String imageFolderPath) { + try { + File imageToSave = new File(imagePath); + File pathToNewImage = new File(imageFolderPath + imageToSave.getName()); + Files.copy(imageToSave.toPath(), pathToNewImage.toPath(), REPLACE_EXISTING); + } catch (IOException e) { + LogsCenter.getLogger(ImageStorage.class).warning(WARNING_UNABLE_TO_SAVE_IMAGE); + } + } +} +``` ###### \java\seedu\recipe\storage\XmlAdaptedRecipe.java ``` java if (this.url == null) { @@ -406,18 +510,20 @@ public class HtmlFormatter { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Image.class.getSimpleName())); } if (!Image.isValidImage(this.image)) { - throw new IllegalValueException(Image.MESSAGE_IMAGE_CONSTRAINTS); + this.image = Image.NULL_IMAGE_REFERENCE; } final Image image = new Image(this.image); ``` ###### \java\seedu\recipe\ui\BrowserPanel.java ``` java + /** * Loads the text recipe onto the browser */ private void loadLocalRecipe(Recipe recipe) { browser.getEngine().loadContent(HtmlFormatter.getHtmlFormat(recipe)); } + ``` ###### \java\seedu\recipe\ui\BrowserPanel.java ``` java diff --git a/collated/functional/hoangduong1607.md b/collated/functional/hoangduong1607.md index 1422cb03ddf3..eedc19912d1b 100644 --- a/collated/functional/hoangduong1607.md +++ b/collated/functional/hoangduong1607.md @@ -3,6 +3,10 @@ ``` java package seedu.recipe.logic.commands; +import static java.util.Objects.requireNonNull; +import static seedu.recipe.logic.parser.CliSyntax.PREFIX_GROUP_NAME; +import static seedu.recipe.logic.parser.CliSyntax.PREFIX_INDEX; + import java.util.List; import java.util.Set; @@ -15,25 +19,34 @@ import seedu.recipe.model.recipe.Recipe; /** * Groups selected recipes. */ -public class GroupCommand extends Command { +public class GroupCommand extends UndoableCommand { public static final String COMMAND_WORD = "group"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Groups the recipes identified by the indices numbers used in the last recipe listing.\n" - + "Parameters: GROUP_NAME INDEX [INDEX] (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " Best 1 3"; + + "Parameters: " + + PREFIX_GROUP_NAME + "GROUP_NAME " + + PREFIX_INDEX + "INDEX " + + "[" + PREFIX_INDEX + "INDEX] (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_GROUP_NAME + "Best " + + PREFIX_INDEX + "1 " + + PREFIX_INDEX + "3 "; public static final String MESSAGE_SUCCESS = "Created New Recipe Group: %s"; private GroupName groupName; private Set targetIndices; public GroupCommand(GroupName groupName, Set targetIndices) { + requireNonNull(groupName); + requireNonNull(targetIndices); this.targetIndices = targetIndices; this.groupName = groupName; } @Override - public CommandResult execute() throws CommandException { + public CommandResult executeUndoableCommand() throws CommandException { + requireNonNull(model); List lastShownList = model.getFilteredRecipeList(); for (Index index : targetIndices) { @@ -73,7 +86,7 @@ public class ViewGroupCommand extends Command { + ": Views recipes in a group.\n" + "Parameters: GROUP_NAME\n" + "Example: " + COMMAND_WORD + " Best"; - public static final String MESSAGE_FAILURE = "Recipe group not found!"; + public static final String MESSAGE_GROUP_NOT_FOUND = "Recipe group not found!"; public static final String MESSAGE_SUCCESS = "Listed all recipes in [%s]"; private GroupPredicate groupPredicate; @@ -87,7 +100,15 @@ public class ViewGroupCommand extends Command { @Override public CommandResult execute() { model.updateFilteredRecipeList(groupPredicate); - return new CommandResult(String.format(MESSAGE_SUCCESS, groupName)); + + String commandResult; + if (model.getFilteredRecipeList().size() > 0) { + commandResult = String.format(MESSAGE_SUCCESS, groupName); + } else { + commandResult = MESSAGE_GROUP_NOT_FOUND; + } + + return new CommandResult(commandResult); } @Override @@ -286,6 +307,17 @@ public class GroupPredicate implements Predicate { commandTextArea.positionCaret(nextFieldPosition); } + /** + * Moves caret to the next field in input text. + * If no field is found after current position, continue from beginning of input text. + */ + private void moveToPrevField() { + int currentCaretPosition = commandTextArea.getCaretPosition(); + int prevFieldPosition = autoCompletionUtil.getPrevFieldPosition(commandTextArea.getText(), + currentCaretPosition); + commandTextArea.positionCaret(prevFieldPosition); + } + /** * Automatically fills command box with text generated by auto-completion */ @@ -316,8 +348,8 @@ public class GroupPredicate implements Predicate { package seedu.recipe.ui; import static seedu.recipe.ui.util.AutoCompletionUtil.APPLICATION_COMMANDS; -import static seedu.recipe.ui.util.AutoCompletionUtil.APPLICATION_KEYWORDS; import static seedu.recipe.ui.util.AutoCompletionUtil.MAX_SUGGESTIONS; +import static seedu.recipe.ui.util.AutoCompletionUtil.getPrefixesForCommand; import java.util.ArrayList; import java.util.Collections; @@ -334,6 +366,8 @@ import seedu.recipe.ui.util.TextInputProcessorUtil; */ public class SuggestionsPopUp extends ContextMenu { + private static final int MARGIN = 5; + private CommandBox commandBox; private TextArea commandTextArea; private TextInputProcessorUtil textInputProcessor; @@ -363,8 +397,18 @@ public class SuggestionsPopUp extends ContextMenu { textInputProcessor.setFont(commandTextArea.getFont()); String lastWord = textInputProcessor.getLastWord(); // finds suggestions and displays - ArrayList suggestionList = new ArrayList<>(APPLICATION_KEYWORDS); - suggestionList.addAll(APPLICATION_COMMANDS); + ArrayList suggestionList = new ArrayList<>(); + + String firstWord = textInputProcessor.getFirstWord(); + if (APPLICATION_COMMANDS.contains(firstWord)) { + if (firstWord.equals(lastWord)) { + suggestionList.add(firstWord); + } else { + suggestionList.addAll(getPrefixesForCommand().get(firstWord)); + } + } else { + suggestionList.addAll(APPLICATION_COMMANDS); + } findSuggestions(lastWord, suggestionList); @@ -406,7 +450,8 @@ public class SuggestionsPopUp extends ContextMenu { */ double findDisplayPositionY(double caretPositionY) { return Math.min(-commandTextArea.getHeight() + commandTextArea.getInsets().getTop() - + commandTextArea.getInsets().getBottom() + caretPositionY, -commandTextArea.getInsets().getBottom()); + + commandTextArea.getInsets().getBottom() + caretPositionY, -commandTextArea.getInsets().getBottom()) + + MARGIN; } /** @@ -434,33 +479,42 @@ import seedu.recipe.logic.parser.CliSyntax; */ public class AutoCompletionUtil { public static final ArrayList APPLICATION_COMMANDS = new ArrayList<>(Arrays.asList("add", "clear", "delete", - "edit", "exit", "find", "group", "help", "history", "list", "redo", "select", "share", "tag", "undo", - "upload", "view_group")); + "edit", "exit", "find", "group", "help", "history", "ingredient", "list", "parse", "redo", "search", + "select", "share", "tag", "theme", "token", "undo", "upload", "view_group")); public static final ArrayList APPLICATION_KEYWORDS = new ArrayList<>(Arrays.asList( CliSyntax.PREFIX_NAME.toString(), CliSyntax.PREFIX_INGREDIENT.toString(), - CliSyntax.PREFIX_INSTRUCTION.toString(), CliSyntax.PREFIX_PREPARATION_TIME.toString(), - CliSyntax.PREFIX_TAG.toString(), CliSyntax.PREFIX_URL.toString(), CliSyntax.PREFIX_INDEX.toString(), - CliSyntax.PREFIX_GROUP_NAME.toString())); - public static final int MAX_SUGGESTIONS = 8; + CliSyntax.PREFIX_INSTRUCTION.toString(), CliSyntax.PREFIX_COOKING_TIME.toString(), + CliSyntax.PREFIX_PREPARATION_TIME.toString(), CliSyntax.PREFIX_CALORIES.toString(), + CliSyntax.PREFIX_SERVINGS.toString(), CliSyntax.PREFIX_TAG.toString(), CliSyntax.PREFIX_URL.toString(), + CliSyntax.PREFIX_IMG.toString(), CliSyntax.PREFIX_GROUP_NAME.toString(), + CliSyntax.PREFIX_INDEX.toString())); + public static final int MAX_SUGGESTIONS = 4; public static final char LF = '\n'; public static final char WHITESPACE = ' '; public static final char END_FIELD = '/'; - private HashMap> prefixesForCommand; + private static HashMap> prefixesForCommand; public AutoCompletionUtil() { + initializePrefixesForCommandsOffline(); + } + + /** + * Creates a list of all prefixes associated with each command + */ + private void initializePrefixesForCommandsOffline() { prefixesForCommand = new HashMap<>(); ArrayList addPrefixes = new ArrayList<>(Arrays.asList(CliSyntax.PREFIX_NAME.toString(), CliSyntax.PREFIX_INGREDIENT.toString(), CliSyntax.PREFIX_INSTRUCTION.toString(), - CliSyntax.PREFIX_PREPARATION_TIME.toString(), CliSyntax.PREFIX_TAG.toString(), - CliSyntax.PREFIX_URL.toString())); + CliSyntax.PREFIX_COOKING_TIME.toString(), CliSyntax.PREFIX_PREPARATION_TIME.toString(), + CliSyntax.PREFIX_CALORIES.toString(), CliSyntax.PREFIX_SERVINGS.toString(), + CliSyntax.PREFIX_URL.toString(), CliSyntax.PREFIX_IMG.toString(), CliSyntax.PREFIX_TAG.toString())); prefixesForCommand.put("add", addPrefixes); ArrayList editPrefixes = new ArrayList<>(Arrays.asList(CliSyntax.PREFIX_NAME.toString(), CliSyntax.PREFIX_INGREDIENT.toString(), CliSyntax.PREFIX_INSTRUCTION.toString(), - CliSyntax.PREFIX_PREPARATION_TIME.toString(), CliSyntax.PREFIX_TAG.toString(), - CliSyntax.PREFIX_URL.toString())); + CliSyntax.PREFIX_TAG.toString(), CliSyntax.PREFIX_URL.toString())); prefixesForCommand.put("edit", editPrefixes); ArrayList groupPrefixes = new ArrayList<>(Arrays.asList(CliSyntax.PREFIX_GROUP_NAME.toString(), @@ -505,13 +559,51 @@ public class AutoCompletionUtil { int wrapAroundPosition = (i + currentCaretPosition) % inputText.length(); if (inputText.charAt(wrapAroundPosition) == END_FIELD) { - nextFieldCaretPosition = wrapAroundPosition + 1; - break; + TextInputProcessorUtil textInputProcessor = new TextInputProcessorUtil(); + textInputProcessor.setContent(inputText.substring(0, wrapAroundPosition + 1)); + + if (APPLICATION_KEYWORDS.contains(textInputProcessor.getLastWord())) { + nextFieldCaretPosition = wrapAroundPosition + 1; + break; + } } } return nextFieldCaretPosition; } + + /** + * Finds position of previous field. + * Returns current position of caret if no field is found + */ + public int getPrevFieldPosition(String inputText, int currentCaretPosition) { + int prevFieldCaretPosition = currentCaretPosition; + + // skips current field (if any) + for (int i = 2; i < inputText.length(); i++) { + int wrapAroundPosition = currentCaretPosition - i; + if (wrapAroundPosition < 0) { + wrapAroundPosition += inputText.length(); + } + wrapAroundPosition %= inputText.length(); + + if (inputText.charAt(wrapAroundPosition) == END_FIELD) { + TextInputProcessorUtil textInputProcessor = new TextInputProcessorUtil(); + textInputProcessor.setContent(inputText.substring(0, wrapAroundPosition + 1)); + + if (APPLICATION_KEYWORDS.contains(textInputProcessor.getLastWord())) { + prevFieldCaretPosition = wrapAroundPosition + 1; + break; + } + } + } + + return prevFieldCaretPosition; + } + + public static HashMap> getPrefixesForCommand() { + return (HashMap>) prefixesForCommand.clone(); + } } ``` ###### \java\seedu\recipe\ui\util\TextInputProcessorUtil.java @@ -529,14 +621,13 @@ public class TextInputProcessorUtil { private static final String EMPTY_STRING = ""; private static final char LF = '\n'; private static final char SPACE = ' '; + private static final int LINE_HEIGHT = 26; private String content; private Font font; - private Text text; public TextInputProcessorUtil() { content = new String(); - text = new Text(); } /** @@ -552,11 +643,24 @@ public class TextInputProcessorUtil { * Gets Y-coordinate of caret */ public double getCaretPositionY() { - return text.prefHeight(-1); + return getRow() * LINE_HEIGHT; + } + + /** + * Gets row index (1-indexed) of caret + */ + public int getRow() { + int row = 1; + for (int i = 0; i < content.length(); i++) { + if (content.charAt(i) == LF) { + row++; + } + } + return row; } /** - * Gets last word from {@code content} + * Gets last word (character(s) between the last whitespace and end of string) from {@code content} */ public String getLastWord() { String lastWord = EMPTY_STRING; @@ -571,6 +675,23 @@ public class TextInputProcessorUtil { return lastWord; } + /** + * Gets first word from {@code content} + */ + public String getFirstWord() { + String firstWord = EMPTY_STRING; + + String trimmedContent = content.trim(); + for (int i = 0; i < trimmedContent.length(); i++) { + if (isWordSeparator(trimmedContent.charAt(i))) { + break; + } + firstWord = firstWord + trimmedContent.charAt(i); + } + + return firstWord; + } + /** * Checks whether {@code inputChar} is a word separator */ @@ -629,8 +750,6 @@ public class TextInputProcessorUtil { */ public void setContent(String inputText) { content = inputText; - text.setText(inputText); - text.setFont(font); } /** diff --git a/collated/functional/kokonguyen191.md b/collated/functional/kokonguyen191.md index 5a77a9ad7f4a..7ccadc02cfc4 100644 --- a/collated/functional/kokonguyen191.md +++ b/collated/functional/kokonguyen191.md @@ -58,6 +58,16 @@ public class WebParseRequestEvent extends BaseEvent { } ``` +###### \java\seedu\recipe\commons\util\FileUtil.java +``` java + /** + * Returns true if {@code testPath} is a valid file path and points to an image. + */ + public static boolean isImageFile(String testPath) { + File file = new File(testPath); + return isImageFile(file); + } +``` ###### \java\seedu\recipe\logic\commands\ChangeThemeCommand.java ``` java package seedu.recipe.logic.commands; @@ -209,7 +219,7 @@ import com.restfb.json.Json; import com.restfb.json.JsonObject; /** - * Handle a query to recipes.wikia.com + * Handles a query to recipes.wikia.com */ public class WikiaQueryHandler implements WikiaQuery { @@ -730,6 +740,38 @@ public class CookingTime { } } +``` +###### \java\seedu\recipe\model\recipe\Image.java +``` java + + /** + * Returns true if a given string is a valid file path, or no file path has been assigned + */ + public static boolean isValidImage(String testImageInput) { + if (testImageInput.equals(NULL_IMAGE_REFERENCE)) { + return true; + } else { + boolean isValidImageStringInput = isValidImageStringInput(testImageInput); + boolean isValidImagePath = FileUtil.isImageFile(testImageInput); + boolean isValidImageUrl = ImageDownloader.isValidImageUrl(testImageInput); + + boolean isValidImage = isValidImageStringInput && (isValidImagePath || isValidImageUrl); + + return isValidImage; + } + } + + /** + * Returns true if the input is a valid input syntax-wise + */ + private static boolean isValidImageStringInput(String testString) { + String trimmedTestImagePath = testString.trim(); + if (trimmedTestImagePath.equals("")) { + return false; + } + return true; + } + ``` ###### \java\seedu\recipe\model\recipe\PreparationTime.java ``` java @@ -843,6 +885,501 @@ public class Servings { } ``` +###### \java\seedu\recipe\model\util\SampleDataUtil.java +``` java + public static Recipe[] getSampleRecipes() { + return new Recipe[] { + new Recipe( + new Name("Mee Goreng"), + new Ingredient( + "green chillies, red chili paste, hot chili sauce," + + " tomato sauce, tomatoes, potatoes, mutton," + + " onion, bean sprouts, cabbage, yellow noodle," + + " oil noodle, eggs, msg, salt, sugar"), + new Instruction( + "Heat oil and fry onion well, add minced " + + "mutton, tomatoes, potatoes and cabbage." + + "Next, throw in noodles and bean sprouts and " + + "fry for a short while.Throw in green" + + " chillies, red chile and fry briefly." + + "In the center of the wok, heat oil, and put" + + " in the eggs, scramble and mix with " + + "noodles thoroughly.Season with msg, " + + "salt, sugar, tomato sauce and chile " + + "sauce.Served with sliced cucumber and tomato sauce." + + "Best eaten with mama teh!! enjoy!."), + new CookingTime("5m"), + new PreparationTime("10m"), + new Calories("750"), + new Servings("1"), + new Url("http://recipes.wikia.com/wiki/Mee_Goreng?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/images/c/ca/" + + "Meegoreng1.jpg/revision/latest/scale-to-width-down/" + + "340?cb=20080516004609"), + getTagSet("Potato", "HokkienNoodle", "MungBeanSprout", "Cabbage", "Mutton", + "SingaporeanAppetizers", "FreshChilePepper", "Tomato", "Cucumber") + ), + new Recipe( + new Name("Hainanese Chicken Rice"), + new Ingredient( + "ginger, garlic, cinnamon, cloves, star anise," + + " chicken broth, pandan leaves, salt, " + + "light soy sauce, sesame oil, cucumber, " + + "tomatoes, coriander, lettuce, pineapple," + + " fresh chillies, ginger, garlic, vinegar" + + ", fish sauce, sugar, sweet soy sauce"), + new Instruction( + "Boil water with spring Onion, ginger and pandan l" + + "eaves, put in Chicken and cook till done, do not over cook." + + "Briefly dip in cold water and set aside to cool. " + + "Keep broth heated.Wash rice and drain. Fin" + + "ely shred ginger and garlic, fry in oil wit" + + "h cloves, cinammon and star anise till frag" + + "rant, add in rice and fry for several minutes." + + "Transfer into rice cooker, add chicken broth, pin" + + "ch of salt, pandan leaves and start cooking" + + ".Put all chili sauce ingredient in a mixer and grind till fine." + + "Slice and arrange tomatoes and cucumbers on a big p" + + "late, cut Chicken into small pieces and put" + + " on top. Splash some light soy sauce and se" + + "same oil over, throw a bunch of coriander on top." + + "Next, Put broth in a bowl with lettuce, get ready c" + + "hili sauce and sweet soy sauce. Serve rice " + + "on a plate with spoon and fork."), + new CookingTime("7m"), + new PreparationTime("15m"), + new Calories("750"), + new Servings("2"), + new Url("http://recipes.wikia.com/wiki/Hainanese_Chicken_Rice?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/images" + + "/d/d3/Chickenrice2.jpg/revision/latest/scale-to-width-down" + + "/340?cb=20080516004325"), + getTagSet("MainDishPoultry", "ScrewPineLeaf", "Lettuce", "SingaporeanMeat", "Chicken", + "Pineapple", "Cucumber", "Rice") + ), + new Recipe( + new Name("Breakfast Pizza"), + new Ingredient("bacon, sausage, green onions, green pepper, eggs, cheddar"), + new Instruction("Mix dough according to package." + + "While dough is rising cook bacon and sausage." + + "Slice vegetables.Cook scrambled eggs in the same skillet as the meat." + + "Spread dough thinly on pizza pan." + + "Cook for 5 minutes at 450°F remove from oven and add meat," + + " eggs, veggies, and cheese." + + "Return to oven until cheese is melted.Enjoy!"), + new CookingTime("5m"), + new PreparationTime("15m"), + new Calories("1000"), + new Servings("3"), + new Url("http://recipes.wikia.com/wiki/Breakfast_Pizza?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/" + + "images/3/31/164_1breakfast_pizza.jpg/revision/late" + + "st/scale-to-width-down/340?cb=20130610170317"), + getTagSet("Pizza", "Breakfast", "Sausage", "Brunch", "Egg", "Cheddar", "Shallot", + "ReadyMadeDough", "GreenBellPepper", "Bacon") + ), + new Recipe( + new Name("Veggie Taco"), + new Ingredient( + "tortilla, refried beans, cheddar, avocado, lettuce, cucumber," + + " tomato, radishes, scallions"), + new Instruction("Spread refried beans onto tortilla." + + "Place on paper plate." + + "Microwave on high 30–45 seconds until beans are hot.Sprinkle with cheese." + + "Fold tortilla in half and top with avocado and chopped salad vegetables." + + "Serve with salsa."), + new CookingTime("1m"), + new PreparationTime("4m"), + new Calories("600"), + new Servings("1"), + new Url("http://recipes.wikia.com/wiki/Veggie_Taco?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes" + + "/images/0/0b/Veggie_Taco.jpg/revision/latest/scal" + + "e-to-width-down/340?cb=20080516004531"), + getTagSet("Lettuce", "RefriedBean", "Cheddar", "Avocado", "Radish", "MexicanVegetarian", + "Tomato", "Cucumber", "Taco") + ), + new Recipe( + new Name("Pho Bo"), + new Ingredient( + "rice noodles, bean sprouts, shallots, coriander, beef, beef stock, " + + "consommé, fresh ginger, cinnamon, coriander seeds, star anis" + + "e, caster sugar, salt, black pepper, fish sauce"), + new Instruction("Boil stock, add the ginger, cinnamon, coriander seeds and s" + + "tar anise.After 15 minutes, add the sugar, salt, peppe" + + "r and fish sauce.Cook the noodles in water, make them al dente." + + "Add bean sprouts, shallots and coriander."), + new CookingTime("15m"), + new PreparationTime("20m"), + new Calories("900"), + new Servings("2"), + new Url("http://recipes.wikia.com/wiki/Pho_Bo?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/images/e" + + "/e1/Pho_bo.jpg/revision/latest/scale-to-width-down/340?c" + + "b=20080516004830"), + getTagSet("VietnameseSoups", "RiceNoodle", "StarAnise", "BeanSprout", "VietnameseNoodle", + "Beef", "BeefStockAndBroth") + ), + new Recipe( + new Name("Hiyashi Chuka"), + new Ingredient("water, rice wine vinegar, soy sauce, sugar, oil, water, sug" + + "ar, soy sauce, rice wine vinegar, sesame seeds, sesa" + + "me oil, Chinese egg noodles, chuka soba, ramen, eggs" + + ", ham, chicken breasts, cucumbers, carrots, bean spro" + + "uts, tomatoes, ginger, shoga"), + new Instruction("All ingredients should be as cold as possible for ma" + + "ximum body-chilling benefit." + + "Divide chilled noodles among serving plates." + + "Add toppings of your choice." + + "My personal favorite is ham, omelette, cucumber, ca" + + "rrot, bean sprouts and ginger." + + "Add dressing of your choice just before eating."), + new CookingTime("10m"), + new PreparationTime("10m"), + new Calories("700"), + new Servings("2"), + new Url("http://recipes.wikia.com/wiki/Hiyashi_Chuka?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/i" + + "mages/4/4d/Hiyashi_Chuka_2.jpg/revision/latest/scal" + + "e-to-width-down/340?cb=20080516004300"), + getTagSet("ChineseEggNoodle", "Carrot", "Egg", "Ham", "RiceVinegar", "Ramen", "BeanSprout", + "JapaneseSalads", "ChickenBreast", "Cucumber") + ), + new Recipe( + new Name("Bulgogi I"), + new Ingredient( + "beef sirloin, soy sauce, water, scallions, garlic, soy s" + + "auce, sesame oil, black bean paste, Shaoxing wine," + + " sugar, cayenne pepper, ginger, sugar, sesame seed" + + ", oil, Tabasco, salt, garlic, sesame seed, scallions, oil"), + new Instruction("Cut beef into very thin strips and pound to flatten; the" + + "n cut into medium size squares." + + "Combine all the other ingredients." + + "The marinade, as the name of the dish implies, should b" + + "e quite fiery." + + "Mix meat and marinade and set aside for 4 to 5 hours, o" + + "r longer if refrigerated." + + "Broil very quickly over hot charcoal, dip in Bulgogi sau" + + "ce and serve immediately with white rice."), + new CookingTime("15m"), + new PreparationTime("6h"), + new Calories("1500"), + new Servings("6"), + new Url("http://recipes.wikia.com/wiki/Bulgogi_I?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/" + + "images/3/32/Cooking.jpg/revision/latest/scale-to-widt" + + "h-down/340?cb=20050413221745"), + getTagSet("BeefSirloin", "KoreanMeat") + ), + new Recipe( + new Name("Rassolnik"), + new Ingredient( + "veal, beef, kidneys, chicken, giblets, carrot, parsley ro" + + "ot, celery root, onion, salt, salt, black pepper" + + "corns, bay leaves, potatoes, long-grain rice, c" + + "ucumbers, sour cream, parsley"), + new Instruction("While the kidneys are soaking, cut the carrot, parsley " + + "and celery roots, and onion into julienne strips." + + "In 4-quart pot, bring 2 quarters of water to a boil." + + "Add the kidneys , julienned vegetables, 1 teaspoon salt, a" + + "nd the peppercorns and bay leaves, and bring to a boil again." + + "Lower the heat and simmer, partially covered, for 30 minutes." + + "Meanwhile, peel the potatoes and cut into 1-inch cubes." + + "Strain the stock, discarding the vegetables." + + "Cut the kidneys into ¼-inch slices and return to the stoc" + + "k, adding the potatoes and rice." + + "Cook slowly, partially covered, for 20 minutes, then add " + + "the pickles and simmer 5 minutes more." + + "Turn off the heat, cover completely, and allow the flavor" + + "s to mingle for 5 minutes." + + "Blend the sour cream with 1 cup of soup and stir it back i" + + "nto the pot, then taste the seasoning."), + new CookingTime("45m"), + new PreparationTime("25m"), + new Calories("900"), + new Servings("4"), + new Url("http://recipes.wikia.com/wiki/Rassolnik?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/image" + + "s/3/3f/460.jpg/revision/latest/scale-to-width-down/" + + "340?cb=20080516004855"), + getTagSet("Celeriac", "SourCream", "Potato", "RussianSoups", "Carrot", "RussianMeat", "Pickle", + "LongGrainRice", "Giblet", "ParsleyRoot", "Kidney") + ), + new Recipe( + new Name("Sausage Rolls"), + new Ingredient("shortcrust pastry, sausage, plain flour, milk"), + new Instruction("Roll the pastry out thinly into a rectangle, then cut it " + + "lengthwise into 2 strips." + + "Divide the sausage meat into 2 pieces; dust with flour an" + + "d form into 2 rolls the length of the pastry." + + "Lay a roll of sausage meat down the center of each strip;" + + " just brush down the edges of the pastry with a little milk." + + "Fold one side of the pastry over the sausage meat and pres" + + "s the two edges firmly together." + + "Seal the long edges together by flaking." + + "Brush the length of the two rolls with milk; then cut each" + + " into slices 4 cm (1 inch) to 5 cm (2 inches) long." + + "Place on a baking sheet and bake in a moderately hot oven " + + "(200°C / 400°F / Gas 6) for 15 minutes; to cook " + + "the meat thoroughly, reduce the temperature to mode" + + "ate (180°C / 350°F / Gas 4) and cook for a further 15 minutes." + + "Cover and brown the top of the dish under a hot grill. Ser" + + "ve straight from the pan."), + new CookingTime("30m"), + new PreparationTime("15m"), + new Calories("900"), + new Servings("4"), + new Url("http://recipes.wikia.com/wiki/Sausage_Rolls?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/images/8" + + "/8b/Sausage_rolls.jpg/revision/latest/scale-to-width" + + "-down/340?cb=20130725232626"), + getTagSet("Sausage", "British", "AsianAppetizers", "Appetizer", "AustralianAppetizers", + "EuropeanAppetizers", "European", "World", "Asian", "Meat", "Oceanian", "SideDishMeat", + "SideDish", "Australian", "MeatAppetizer", "BritishAppetizers", "OceanianAppetizers", + "SavoryPastryAppetizer") + ), + new Recipe( + new Name("Traditional Banoffee Pie"), + new Ingredient("butter, brown sugar, condensed milk, bananas"), + new Instruction("Have a baking tin, bowl, non-stick pan and wooden spoon ready." + + "Make sure the digestive biscuits are crushed to breadcrumbs." + + "Tip all the breadcrumbs into the baking tin, then " + + "use a spoon to create a pie shell across th" + + "e bottom and around the sides of the tin." + + "Chill this in your fridge/freezer for ten minutes a" + + "nd continue to the caramel." + + "Melt the butter and sugar into a non-stick pan over low heat." + + "Stir this continuously until all the sugar has dissolved." + + "Add the condensed milk and bring this to a boil fo" + + "r about a minute. Stir this until a thick golden caramel forms." + + "Spread the caramel over the now firm base, and the" + + "n leave to chill for an hour." + + "After the hour chilling, remove the pie from the " + + "tin carefully and place it on your serving plate." + + "Slice the bananas into small chunks." + + "Create a layer of bananas on top of the caramel." + + "Spread the whipped cream on top so it covers the layer of bananas."), + new CookingTime("3h"), + new PreparationTime("15m"), + new Calories("800"), + new Servings("6"), + new Url("http://recipes.wikia.com/wiki/Traditional_Banoffee_Pie?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/images/b/b2" + + "/Banoffeepiewithpecan_86926_16x9.jpg/revision/latest/" + + "scale-to-width-down/340?cb=20130711152627"), + getTagSet("BritishDesserts", "British", "Pie", "Dessert") + ), + new Recipe( + new Name("Traditional Welsh Rarebit"), + new Ingredient("-"), + new Instruction("Put the cheese, flour, mustard, Worcestershire sauce, " + + "butter and pepper into a saucepan." + + "Mix well and then add the beer or milk to moisten. Be " + + "careful not to make it too wet as you'll never " + + "get it to stick to the bread." + + "Stir the mixture over a low heat until it's melted." + + "Once your mixture has the consistency of a thick paste," + + " remove it from the heat and allow to cool slightly." + + "While the cheese mixture is cooling, take four slices o" + + "f bread and toast them on one side only." + + "Once done, divide the mixture between the four slices of" + + " toast. Pop this back under the grill to brown." + + "Serve immediately. This dish makes a great lunchtime sna" + + "ck, or for a more substantial meal, try serving " + + "it alongside a bowl of leek and potato soup."), + new CookingTime("25m"), + new PreparationTime("30m"), + new Calories("1000"), + new Servings("4"), + new Url("http://recipes.wikia.com/wiki/Traditional_Welsh_Rarebit?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipes/images/9/9b/" + + "WelshRarebit4_big.jpg/revision/latest/" + + "scale-to-width-down/340?cb=20110728151258"), + getTagSet("British", "Welsh", "Snack", "Lunch") + ), + new Recipe( + new Name("Egg Curry"), + new Ingredient( + "eggs, onions, tomatoes, tomato, garlic, fresh ginger, garlic, cumin, chili powder, " + + "turmeric, coriander powder, cumin, salt, yoghurt, coriander leaves, oil"), + new Instruction( + "Add cumin seeds in hot oil till it begins to sizzle. Add ginger-garli" + + "c paste and Onion paste, and fry for 3 – 5 minutes till slightly browned." + + "Add salt, chili powder, coriander powder, cumin powder and tur" + + "meric powder and cook for another minute till fragrant. Add" + + " tomatoe paste and let cook for a few minutes till all th" + + "e spices blend in." + + "Add in the youghurt and stir constantly to avoid getting lumps." + + "Put in the boiled egg halves abd cover cook for another 5 minutes." + + "Garnish with chopped coriander leaves and serve " + + "warm with fresh rotis or rice."), + new CookingTime("15m"), + new PreparationTime("45m"), + new Calories("900"), + new Servings("4"), + new Url("http://recipes.wikia.com/wiki/Egg_Curry?useskin=wikiamobile"), + new Image( + "https://vignette.wikia.nocookie.net/recipe" + + "s/images/1/1b/Egg_Curry.jpg/revision/latest/scal" + + "e-to-width-down/340?cb=20080516004839"), + getTagSet("Curry", "Egg", "HookedOnHeat", "IndianVegetarian", "ChiliPowder", "Tomato", "Rice") + ) + }; + } + +``` +###### \java\seedu\recipe\storage\ImageDownloader.java +``` java +package seedu.recipe.storage; + +import static java.util.Objects.requireNonNull; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.imageio.ImageIO; +import javax.xml.bind.annotation.adapters.HexBinaryAdapter; + +import seedu.recipe.commons.core.LogsCenter; +import seedu.recipe.commons.util.FileUtil; +import seedu.recipe.model.recipe.Image; + +/** + * A class that downloads images and saves them to the images folder + */ +public class ImageDownloader { + + public static final String DOWNLOADED_IMAGE_FORMAT = "jpg"; + + /** + * Returns true if {@code testUrl} is valid and links to an image + */ + public static boolean isValidImageUrl(String testUrl) { + URL imageUrl; + try { + imageUrl = new URL(testUrl); + } catch (MalformedURLException e) { + return false; + } + + BufferedImage image; + try { + image = ImageIO.read(imageUrl); + } catch (IOException ioe) { + LogsCenter.getLogger(Image.class).warning("Cannot get image from " + + testUrl + ". It is likely the app is not connected to the Internet."); + return false; + } + + if (image != null) { + return true; + } else { + return false; + } + } + + /** + * Downloads an iamge from {@code imageUrlString} to the images folder + */ + public static String downloadImage(String imageUrlString) { + assert isValidImageUrl(imageUrlString); + + try { + byte[] imageData = getImageData(imageUrlString); + String md5Checksum = calculateMd5Checksum(imageData); + String filePath = getImageFilePathFromImageName(md5Checksum); + File file = prepareImageFile(filePath); + if (file != null) { + writeDataToFile(imageData, file); + } + return filePath; + } catch (IOException ioe) { + throw new AssertionError( + "Something wrong happened when the app was trying to " + + "download image data from " + imageUrlString + + ". This should not happen.", ioe); + } catch (NoSuchAlgorithmException nsaee) { + throw new AssertionError( + "Something wrong happened when the app was trying to " + + "calculate the MD5 checksum for the iamge from " + imageUrlString + + ". This should not happen.", nsaee); + } + } + + /** + * Gets a byte array from the {@code imageUrlSring} + */ + private static byte[] getImageData(String imageUrlString) throws IOException { + URL imageUrl = new URL(imageUrlString); + BufferedImage image = ImageIO.read(imageUrl); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(image, DOWNLOADED_IMAGE_FORMAT, byteArrayOutputStream); + byteArrayOutputStream.flush(); + byte[] data = byteArrayOutputStream.toByteArray(); + byteArrayOutputStream.close(); + return data; + } + + /** + * Returns the MD5 checksum String value of given {@code data} + */ + private static String calculateMd5Checksum(byte[] data) throws NoSuchAlgorithmException { + requireNonNull(data); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(data); + // Adapted from https://stackoverflow.com/questions/5470219/get-md5-string-from-message-digest + HexBinaryAdapter hexBinaryAdapter = new HexBinaryAdapter(); + return hexBinaryAdapter.marshal(md5.digest()); + } + + private static String getImageFilePathFromImageName(String imageName) { + return Image.IMAGE_STORAGE_FOLDER + imageName + "." + DOWNLOADED_IMAGE_FORMAT; + } + + /** + * Checks if {@code filePath} exists or not. If not, create a file at {@code filePath} as well as any parent + * directory if necessary, then returns the File object. + */ + private static File prepareImageFile(String filePath) throws IOException { + File file = new File(filePath); + if (FileUtil.createFile(file)) { + return file; + } else { + return null; + } + } + + /** + * Writes given {@code data} to {@code file} + */ + private static void writeDataToFile(byte[] data, File file) throws IOException { + requireNonNull(data); + requireNonNull(file); + FileOutputStream fileOutputStream = new FileOutputStream(file); + fileOutputStream.write(data); + fileOutputStream.flush(); + fileOutputStream.close(); + } +} +``` ###### \java\seedu\recipe\storage\XmlAdaptedRecipe.java ``` java if (this.ingredient == null) { @@ -901,6 +1438,56 @@ public class Servings { ``` ###### \java\seedu\recipe\ui\BrowserPanel.java +``` java + + /** + * Loads a default HTML file with a background that matches the general theme. + * + * @param isDarkTheme true if the app is using dark theme + */ + public void loadDefaultPage(boolean isDarkTheme) { + if (!isLoaded()) { + URL defaultPage; + if (isDarkTheme) { + defaultPage = MainApp.class.getResource(FXML_FILE_FOLDER + DEFAULT_PAGE_GIRL); + } else { + defaultPage = MainApp.class.getResource(FXML_FILE_FOLDER + DEFAULT_PAGE_LIGHT); + } + loadPage(defaultPage.toExternalForm()); + logger.info("BrowserPanel is empty, changed theme and reloaded BrowserPanel."); + } else { + logger.info("BrowserPanel is not empty, changed theme without reloading BrowserPanel."); + } + } + + /** + * Returns true if BrowserPanel is loaded with a page that is neither null nor default + */ + private boolean isLoaded() { + URL lightTheme = MainApp.class.getResource(FXML_FILE_FOLDER + DEFAULT_PAGE_LIGHT); + URL darkTheme = MainApp.class.getResource(FXML_FILE_FOLDER + DEFAULT_PAGE_GIRL); + + String loadedUrlString = browser.getEngine().getLocation(); + if (loadedUrlString == null) { + return false; + } else { + URL loadedUrl = null; + try { + loadedUrl = new URL(loadedUrlString); + } catch (MalformedURLException murle) { + throw new AssertionError("Something wrong happened when the app is trying to read the " + + "url loaded inside BrowserPanel. This should not happen.", murle); + } + boolean isLightThemeLoaded = loadedUrl.equals(lightTheme); + boolean isDarkThemeLoaded = loadedUrl.equals(darkTheme); + + boolean isBlankPageLoaded = isLightThemeLoaded || isDarkThemeLoaded; + + return !isBlankPageLoaded; + } + } +``` +###### \java\seedu\recipe\ui\BrowserPanel.java ``` java @Subscribe private void handleInternetSearchRequestEvent(InternetSearchRequestEvent event) { @@ -958,6 +1545,24 @@ public class Servings { } ``` +###### \java\seedu\recipe\ui\MainWindow.java +``` java + /** + * Toggles the main window theme + */ + private void loadStyle(boolean darkTheme) { + Scene scene = primaryStage.getScene(); + scene.getStylesheets().clear(); + if (darkTheme) { + scene.getStylesheets().add(MainApp.class.getResource(FXML_FILE_FOLDER + GIRL_THEME_CSS).toExternalForm()); + } else { + scene.getStylesheets().add(MainApp.class.getResource(FXML_FILE_FOLDER + LIGHT_THEME_CSS).toExternalForm()); + } + scene.getStylesheets().add(MainApp.class.getResource(FXML_FILE_FOLDER + EXTENSIONS_CSS).toExternalForm()); + primaryStage.setScene(scene); + primaryStage.show(); + } +``` ###### \java\seedu\recipe\ui\parser\MobileWikiaParser.java ``` java package seedu.recipe.ui.parser; @@ -991,6 +1596,11 @@ public class MobileWikiaParser extends WikiaParser { contentText = this.document.selectFirst(".article-content.mw-content"); } + @Override + protected void getCategories() { + categories = document.select(".mw-content.collapsible-menu.ember-view ul li"); + } + @Override public String getName() { return document.selectFirst(".wiki-page-header__title").text(); @@ -1028,6 +1638,7 @@ import static seedu.recipe.logic.parser.CliSyntax.PREFIX_IMG; import static seedu.recipe.logic.parser.CliSyntax.PREFIX_INGREDIENT; import static seedu.recipe.logic.parser.CliSyntax.PREFIX_INSTRUCTION; import static seedu.recipe.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.recipe.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.recipe.logic.parser.CliSyntax.PREFIX_URL; import org.jsoup.nodes.Document; @@ -1050,31 +1661,41 @@ public abstract class WebParser { String instruction = getInstruction(); String imageUrl = getImageUrl(); String url = getUrl(); + String[] tags = getTags(); if (!name.equals("")) { StringBuilder commandBuilder = new StringBuilder(); - commandBuilder.append(COMMAND_WORD); - commandBuilder.append(LF); - commandBuilder.append(PREFIX_NAME); - commandBuilder.append(name); + commandBuilder.append(COMMAND_WORD) + .append(LF) + .append(PREFIX_NAME) + .append(name); if (!ingredient.equals("")) { - commandBuilder.append(LF); - commandBuilder.append(PREFIX_INGREDIENT); - commandBuilder.append(ingredient); + commandBuilder.append(LF) + .append(PREFIX_INGREDIENT) + .append(ingredient); } if (!instruction.equals("")) { - commandBuilder.append(LF); - commandBuilder.append(PREFIX_INSTRUCTION); - commandBuilder.append(instruction); + commandBuilder.append(LF) + .append(PREFIX_INSTRUCTION) + .append(instruction); } if (!imageUrl.equals("")) { + commandBuilder.append(LF) + .append(PREFIX_IMG) + .append(imageUrl); + } + commandBuilder.append(LF) + .append(PREFIX_URL) + .append(url); + if (tags.length > 0) { commandBuilder.append(LF); - commandBuilder.append(PREFIX_IMG); - commandBuilder.append(imageUrl); + for (String tag : tags) { + commandBuilder + .append(PREFIX_TAG) + .append(tag) + .append(" "); + } } - commandBuilder.append(LF); - commandBuilder.append(PREFIX_URL); - commandBuilder.append(url); return commandBuilder.toString(); } return null; @@ -1089,6 +1710,8 @@ public abstract class WebParser { public abstract String getImageUrl(); public abstract String getUrl(); + + public abstract String[] getTags(); } ``` ###### \java\seedu\recipe\ui\parser\WebParserHandler.java @@ -1142,7 +1765,9 @@ public class WebParserHandler { String domain = uri.getHost(); switch (domain) { case WikiaParser.DOMAIN: - // Try to pre-parse first + /* + Try to pre-parse to see if the page is mobile wikia or wikia. + */ org.jsoup.nodes.Document jsoupDocument = Jsoup.parse(documentString); if (jsoupDocument.getElementById("mw-content-text") == null) { return new MobileWikiaParser(jsoupDocument); @@ -1166,8 +1791,11 @@ package seedu.recipe.ui.parser; import static java.util.Objects.requireNonNull; +import java.util.Arrays; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -1184,6 +1812,7 @@ public class WikiaParser extends WebParser { protected Document document; protected Element contentText; + protected Elements categories; /** * Constructs from a Jsoup Document. @@ -1192,6 +1821,7 @@ public class WikiaParser extends WebParser { requireNonNull(document); this.document = document; getMainBody(); + getCategories(); } /** @@ -1201,13 +1831,21 @@ public class WikiaParser extends WebParser { requireNonNull(html); this.document = Jsoup.parse(html, url); getMainBody(); + getCategories(); } /** * Assigns {@code contentText} to the Element that contains the article body. */ protected void getMainBody() { - contentText = this.document.getElementById("mw-content-text"); + contentText = document.getElementById("mw-content-text"); + } + + /** + * Assigns {@code categories} to the ElementS that contains the categories. + */ + protected void getCategories() { + categories = document.getElementsByClass("category"); } @Override @@ -1217,10 +1855,22 @@ public class WikiaParser extends WebParser { @Override public String getIngredient() { + Elements[] arrayOfElementsOfIngredients = getElementsOfIngredient(); + return getIngredientString(arrayOfElementsOfIngredients); + } + + /** + * Returns an array of size 2 of Elements. The first contains all Element with class "a", which contain a link. + * The second contains all Element with class "li", which contain the whole ingredient line. + */ + private Elements[] getElementsOfIngredient() { Elements elements = contentText.select("h2,ul"); Iterator eleIte = elements.iterator(); - while (eleIte.hasNext() && !eleIte.next().text().startsWith("Ingredient")) { - // Do nothing and just go to the line that starts with "Ingredient" + while (eleIte.hasNext()) { + String nextText = eleIte.next().text(); + if (nextText.startsWith("Ingredient")) { + break; + } } Elements elementsWithIngredientWithLink = new Elements(); @@ -1234,6 +1884,16 @@ public class WikiaParser extends WebParser { elementsWithIngredientWithLink.addAll(nextElement.select("a")); } + return new Elements[] {elementsWithIngredientWithLink, elementsWithIngredient}; + } + + /** + * Returns a Ingredient string that can be used in an add command from the list of ingredients. + */ + private String getIngredientString(Elements[] arrayOfElementsOfIngredients) { + Elements elementsWithIngredientWithLink = arrayOfElementsOfIngredients[0]; + Elements elementsWithIngredient = arrayOfElementsOfIngredients[1]; + List ingredientList; if (elementsWithIngredientWithLink.isEmpty()) { ingredientList = elementsWithIngredient.eachText(); @@ -1246,6 +1906,24 @@ public class WikiaParser extends WebParser { @Override public String getInstruction() { Elements elementsWithInstruction = contentText.select("ol li"); + if (elementsWithInstruction.isEmpty()) { + Elements elements = contentText.select("h2,p,h3"); + Iterator eleIte = elements.iterator(); + while (eleIte.hasNext()) { + String nextText = eleIte.next().text(); + if (nextText.startsWith("Directions")) { + break; + } + } + while (eleIte.hasNext()) { + Element nextElement = eleIte.next(); + if (nextElement.tagName().equals("h3")) { + break; + } else { + elementsWithInstruction.add(nextElement); + } + } + } List instructionList = elementsWithInstruction.eachText(); return String.join("\n", instructionList); } @@ -1260,6 +1938,33 @@ public class WikiaParser extends WebParser { } } + @Override + public String[] getTags() { + LinkedList tags = new LinkedList<>(); + + for (Element category : categories) { + String rawText = category.text(); + tags.add(trimTag(rawText)); + } + + return tags.toArray(new String[tags.size()]); + } + + /** + * Trims a tag, removes generic keywords to make tag shorter + */ + protected String trimTag(String tag) { + if (tag.endsWith("ishes")) { + tag = tag.replace("Dishes", "").replace("dishes", ""); + } + if (tag.endsWith("ecipes")) { + tag = tag.replace("Recipes", "").replace("recipes", ""); + } + return Arrays.stream(tag.split(" ")) + .map(word -> Character.toTitleCase(word.charAt(0)) + word.substring(1)) + .collect(Collectors.joining()); + } + @Override public String getUrl() { return document.selectFirst("[rel=\"canonical\"]").attr("href"); diff --git a/collated/test/RyanAngJY.md b/collated/test/RyanAngJY.md index a78380123227..c3985831d751 100644 --- a/collated/test/RyanAngJY.md +++ b/collated/test/RyanAngJY.md @@ -1,4 +1,26 @@ # RyanAngJY +###### \java\seedu\recipe\commons\util\FileUtilTest.java +``` java + @Test + public void isImageFile() { + // file is not an image file + File file = new File(MainApp.class.getResource("/view/DarkTheme.css").toExternalForm() + .substring(5)); + assertFalse(FileUtil.isImageFile(file)); + + // file is directory + file = new File(MainApp.class.getResource("/view").toExternalForm().substring(5)); + assertFalse(FileUtil.isImageFile(file)); + + // file is null pointer + File nullFile = null; + Assert.assertThrows(NullPointerException.class, () -> FileUtil.isImageFile(nullFile)); + + // valid image file + file = new File(Image.VALID_IMAGE_PATH); + assertTrue(FileUtil.isImageFile(file)); + } +``` ###### \java\seedu\recipe\logic\parser\AddCommandParserTest.java ``` java // multiple urls - last url accepted @@ -42,6 +64,10 @@ import org.junit.Test; import seedu.recipe.testutil.Assert; public class ImageTest { + private static final String INVALID_IMAGE_URL = "http://google.com"; + private static final String NOT_AN_IMAGE_PATH = "build.gradle"; + private static final String VALID_IMAGE_URL = "https://i.imgur.com/FhRsgCK.jpg"; + @Test public void constructor_null_throwsNullPointerException() { Assert.assertThrows(NullPointerException.class, () -> new Image(null)); @@ -58,12 +84,30 @@ public class ImageTest { // blank image assertFalse(Image.isValidImage("")); // empty string assertFalse(Image.isValidImage(" ")); // spaces only + assertFalse(Image.isValidImage("\t\n\t\r\n")); // invalid image assertFalse(Image.isValidImage("estsed")); //random string - // valid image + // valid image path + assertTrue(Image.isValidImage(Image.NULL_IMAGE_REFERENCE)); assertTrue(Image.isValidImage(Image.VALID_IMAGE_PATH)); + // invalid image path + assertFalse(Image.isValidImage("ZZZ://ZZZ!!@@#")); + // not an image + assertFalse(Image.isValidImage(NOT_AN_IMAGE_PATH)); + + // valid image url + assertTrue(Image.isValidImage(VALID_IMAGE_URL)); + // invalid image url + assertFalse(Image.isValidImage(INVALID_IMAGE_URL)); + } + + @Test + public void setImageToInternalReference() { + Image imageStub = new Image(Image.VALID_IMAGE_PATH); + imageStub.setImageToInternalReference(); + assertTrue(imageStub.toString().equals(Image.IMAGE_STORAGE_FOLDER + imageStub.getImageName())); } } ``` @@ -133,15 +177,6 @@ public class UrlTest { String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Url.class.getSimpleName()); Assert.assertThrows(IllegalValueException.class, expectedMessage, recipe::toModelType); } - @Test - public void toModelType_invalidImage_throwsIllegalValueException() { - XmlAdaptedRecipe recipe = - new XmlAdaptedRecipe(VALID_NAME, VALID_INGREDIENT, VALID_INSTRUCTION, VALID_COOKING_TIME, - VALID_PREPARATION_TIME, VALID_CALORIES, VALID_SERVINGS, VALID_URL, - INVALID_IMAGE, VALID_TAGS); - String expectedMessage = Image.MESSAGE_IMAGE_CONSTRAINTS; - Assert.assertThrows(IllegalValueException.class, expectedMessage, recipe::toModelType); - } @Test public void toModelType_nullImage_throwsIllegalValueException() { @@ -152,6 +187,20 @@ public class UrlTest { String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Image.class.getSimpleName()); Assert.assertThrows(IllegalValueException.class, expectedMessage, recipe::toModelType); } + + + @Test + public void toModelType_invalidImage_setsImageToNullReference() { + Recipe recipe = null; + try { + recipe = new XmlAdaptedRecipe(VALID_NAME, VALID_INGREDIENT, VALID_INSTRUCTION, VALID_COOKING_TIME, + VALID_PREPARATION_TIME, VALID_CALORIES, VALID_SERVINGS, VALID_URL, + INVALID_IMAGE, VALID_TAGS).toModelType(); + } catch (Exception e) { + System.out.println("Unable to create recipe"); + } + assertTrue(recipe.getImage().toString().equals(Image.NULL_IMAGE_REFERENCE)); + } ``` ###### \java\seedu\recipe\storage\XmlAdaptedRecipeTest.java ``` java diff --git a/collated/test/hoangduong1607.md b/collated/test/hoangduong1607.md index ed5011a052f6..357480576bf5 100644 --- a/collated/test/hoangduong1607.md +++ b/collated/test/hoangduong1607.md @@ -1,6 +1,7 @@ # hoangduong1607 ###### \java\guitests\guihandles\CommandBoxHandle.java ``` java + /** * Inserts the given string to text at current caret position */ @@ -62,7 +63,7 @@ public class ViewGroupCommandParserTest { guiRobot.push(KeyCode.DOWN); guiRobot.push(KeyCode.DOWN); guiRobot.push(KeyCode.ENTER); - assertInput(SECOND_SUGGESTION); + assertInput(ADD_COMMAND_WITH_PREFIX_NAME + WHITESPACE + SECOND_FIELD_OF_ADD_COMMAND); } @Test @@ -75,8 +76,66 @@ public class ViewGroupCommandParserTest { guiRobot.push(KeyboardShortcutsMapping.NEXT_FIELD); commandBoxHandle.insertText(RECIPE_NAME); assertInput(AUTO_COMPLETION_FOR_ADD_COMMAND_WITH_RECIPE_NAME); + + guiRobot.push(KeyboardShortcutsMapping.PREV_FIELD); + guiRobot.push(KeyboardShortcutsMapping.PREV_FIELD); + commandBoxHandle.insertText(TAG); + assertInput(AUTO_COMPLETION_FOR_ADD_COMMAND_WITH_RECIPE_NAME_AND_TAG); + } + +``` +###### \java\seedu\recipe\ui\testutil\TextInputProcessorUtilTest.java +``` java +package seedu.recipe.ui.testutil; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import seedu.recipe.ui.util.TextInputProcessorUtil; + +public class TextInputProcessorUtilTest { + + private static final char WHITESPACE = ' '; + private static final char LF = '\n'; + private static final String EMPTY_STRING = ""; + + private static final String FIRST_WORD = "HELLO"; + private static final String SECOND_WORD = "WORLD"; + private static final String THIRD_WORD = "MY"; + private static final String FOURTH_WORD = "NAME"; + private static final String FIFTH_WORD = "IS"; + private static final String FIRST_SINGLE_LINE_SENTENCE = FIRST_WORD + WHITESPACE + SECOND_WORD; + private static final String SECOND_SINGLE_LINE_SENTENCE = WHITESPACE + THIRD_WORD + WHITESPACE + FOURTH_WORD + + WHITESPACE + FIFTH_WORD + WHITESPACE; + private static final String MULTIPLE_LINES_SENTENCE = FIRST_SINGLE_LINE_SENTENCE + LF + SECOND_SINGLE_LINE_SENTENCE; + + private static TextInputProcessorUtil textInputProcessor = new TextInputProcessorUtil(); + + @Test + public void getLastWord_success() { + textInputProcessor.setContent(FIRST_SINGLE_LINE_SENTENCE); + assertEquals(SECOND_WORD, textInputProcessor.getLastWord()); + + textInputProcessor.setContent(MULTIPLE_LINES_SENTENCE); + assertEquals(EMPTY_STRING, textInputProcessor.getLastWord()); + } + + @Test + public void getFirstWord_success() { + textInputProcessor.setContent(FIRST_SINGLE_LINE_SENTENCE); + assertEquals(FIRST_WORD, textInputProcessor.getFirstWord()); + + textInputProcessor.setContent(MULTIPLE_LINES_SENTENCE); + assertEquals(FIRST_WORD, textInputProcessor.getFirstWord()); } + @Test + public void getLastLine_success() { + textInputProcessor.setContent(MULTIPLE_LINES_SENTENCE); + assertEquals(SECOND_SINGLE_LINE_SENTENCE, textInputProcessor.getLastLine()); + } +} ``` ###### \java\systemtests\GroupCommandSystemTest.java ``` java @@ -151,3 +210,94 @@ public class GroupCommandSystemTest extends RecipeBookSystemTest { } } ``` +###### \java\systemtests\ViewGroupCommandSystemTest.java +``` java +package systemtests; + +import static seedu.recipe.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import seedu.recipe.commons.core.index.Index; +import seedu.recipe.logic.commands.GroupCommand; +import seedu.recipe.logic.commands.ViewGroupCommand; +import seedu.recipe.logic.parser.CliSyntax; +import seedu.recipe.model.Model; +import seedu.recipe.model.recipe.Recipe; + +public class ViewGroupCommandSystemTest extends RecipeBookSystemTest { + + private static final String GROUP_THAT_EXISTS = "My best"; + private static final String GROUP_THAT_DOES_NOT_EXIST = "Best"; + private static final String WHITESPACE = " "; + private static final String FIRST_INDEX = "1"; + private static final String SECOND_INDEX = "2"; + private static final List EMPTY_LIST = new ArrayList<>(); + + @Test + public void group() throws Exception { + Model expectedModel = getModel(); + + String groupCommand = GroupCommand.COMMAND_WORD + WHITESPACE + CliSyntax.PREFIX_GROUP_NAME + GROUP_THAT_EXISTS + + WHITESPACE + CliSyntax.PREFIX_INDEX + FIRST_INDEX + WHITESPACE + CliSyntax.PREFIX_INDEX + + SECOND_INDEX; + String expectedResultMessage = String.format(GroupCommand.MESSAGE_SUCCESS, GROUP_THAT_EXISTS); + assertCommandSuccess(groupCommand, expectedResultMessage, expectedModel); + + /* Case: view a group that exists -> show recipe(s) in the group */ + String viewGroupCommand = ViewGroupCommand.COMMAND_WORD + WHITESPACE + GROUP_THAT_EXISTS; + expectedResultMessage = String.format(ViewGroupCommand.MESSAGE_SUCCESS, GROUP_THAT_EXISTS); + expectedModel = getModel(); + ModelHelper.setFilteredList(expectedModel, + expectedModel.getFilteredRecipeList() + .get(Index.fromOneBased(Integer.valueOf(FIRST_INDEX)).getZeroBased()), + expectedModel.getFilteredRecipeList() + .get(Index.fromOneBased(Integer.valueOf(SECOND_INDEX)).getZeroBased())); + assertCommandSuccess(viewGroupCommand, expectedResultMessage, expectedModel); + + + /*Case: view a group that does not exist -> show empty list and inform user that the group does not exist */ + viewGroupCommand = ViewGroupCommand.COMMAND_WORD + WHITESPACE + GROUP_THAT_DOES_NOT_EXIST; + expectedResultMessage = ViewGroupCommand.MESSAGE_GROUP_NOT_FOUND; + expectedModel = getModel(); + ModelHelper.setFilteredList(expectedModel, EMPTY_LIST); + assertCommandSuccess(viewGroupCommand, expectedResultMessage, expectedModel); + + /* Case: mixed case command word -> rejected */ + assertCommandFailure("View_Group", MESSAGE_UNKNOWN_COMMAND); + } + + /** + * Performs the same verification as {@code assertCommandSuccess(String)} except that the result box displays + * {@code expectedResultMessage} and the model related components equal to {@code expectedModel}. + */ + private void assertCommandSuccess(String command, String expectedResultMessage, Model expectedModel) { + executeCommand(command); + assertApplicationDisplaysExpected("", expectedResultMessage, expectedModel); + assertCommandBoxShowsDefaultStyle(); + } + + /** + * Executes {@code command} and verifies that the command box displays {@code command}, the result display + * box displays {@code expectedResultMessage} and the model related components equal to the current model. + * These verifications are done by + * {@code RecipeBookSystemTest#assertApplicationDisplaysExpected(String, String, Model)}.
+ * Also verifies that the browser url, selected card and status bar remain unchanged, and the command box has the + * error style. + * + * @see RecipeBookSystemTest#assertApplicationDisplaysExpected(String, String, Model) + */ + private void assertCommandFailure(String command, String expectedResultMessage) { + Model expectedModel = getModel(); + + executeCommand(command); + assertApplicationDisplaysExpected(command, expectedResultMessage, expectedModel); + assertSelectedCardUnchanged(); + assertCommandBoxShowsErrorStyle(); + assertStatusBarUnchanged(); + } +} +``` diff --git a/collated/test/kokonguyen191.md b/collated/test/kokonguyen191.md index 67176187306e..8de2041bd02b 100644 --- a/collated/test/kokonguyen191.md +++ b/collated/test/kokonguyen191.md @@ -1,6 +1,7 @@ # kokonguyen191 ###### \java\guitests\guihandles\CommandBoxHandle.java ``` java + /** * Appends the given string to text already existing in the Command box */ @@ -9,6 +10,14 @@ guiRobot.pauseForHuman(); } + /** + * Submits whatever is in the CommandBox + */ + public void submitCommand() { + click(); + guiRobot.type(KeyCode.ENTER); + } + ``` ###### \java\guitests\guihandles\MainMenuHandle.java ``` java @@ -582,6 +591,93 @@ public class ServingsTest { } } ``` +###### \java\seedu\recipe\storage\ImageDownloaderTest.java +``` java +package seedu.recipe.storage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.CRC32; + +import org.junit.After; +import org.junit.Test; + +import seedu.recipe.model.recipe.Image; +import seedu.recipe.testutil.Assert; + +public class ImageDownloaderTest { + + private static final String INVALID_IMAGE_URL = "http://google.com"; + private static final String VALID_IMAGE_URL = "https://i.imgur.com/FhRsgCK.jpg"; + private static final String VALID_IMAGE_MD5 = "2A78C63135CCB8BCECEF189FE0CD834C"; + private static final String VALID_IMAGE_PATH = + Image.IMAGE_STORAGE_FOLDER + VALID_IMAGE_MD5 + "." + ImageDownloader.DOWNLOADED_IMAGE_FORMAT; + private static final long VALID_IMAGE_CRC = 2184062566L; + + @Test + public void isValidImageUrl() throws Exception { + // not an image url + assertFalse(ImageDownloader.isValidImageUrl(null)); + assertFalse(ImageDownloader.isValidImageUrl("\t\n\t\r\n")); + assertFalse(ImageDownloader.isValidImageUrl("ZZZ://ZZZ!!@@#")); + assertFalse(ImageDownloader.isValidImageUrl(Image.VALID_IMAGE_PATH)); + assertFalse(ImageDownloader.isValidImageUrl(Image.NULL_IMAGE_REFERENCE)); + + // invalid image url + assertFalse(ImageDownloader.isValidImageUrl(INVALID_IMAGE_URL)); + + // valid image url + assertTrue(ImageDownloader.isValidImageUrl(VALID_IMAGE_URL)); + } + + @Test + public void downloadImage_invalidUrl_throwsAssertionError() { + Assert.assertThrows(AssertionError.class, () -> ImageDownloader.downloadImage(INVALID_IMAGE_URL)); + } + + @Test + public void downloadImage_validUrl_returnsImageName() throws Exception { + // First download + String fileName = ImageDownloader.downloadImage(VALID_IMAGE_URL); + File file = new File(fileName); + assertTrue(file.exists()); + assertImageCrc(file, VALID_IMAGE_CRC); + assertEquals(VALID_IMAGE_PATH, fileName); + + // Re-download will still return file name + assertEquals(VALID_IMAGE_PATH, ImageDownloader.downloadImage(VALID_IMAGE_URL)); + } + + @After + public void cleanUp() { + File file = new File(VALID_IMAGE_PATH); + file.delete(); + } + + /** + * Asserts that {@code image} has CRC {@code crcValue} + */ + private void assertImageCrc(File image, long crcValue) throws IOException { + InputStream in = new FileInputStream(image); + CRC32 crc32 = new CRC32(); + byte[] buffer = new byte[1000]; + int bytes; + while ((bytes = in.read(buffer)) != -1) { + crc32.update(buffer, 0, bytes); + } + long crc = crc32.getValue(); + in.close(); + + assertEquals(VALID_IMAGE_CRC, crc); + } +} +``` ###### \java\seedu\recipe\storage\XmlAdaptedRecipeTest.java ``` java @Test @@ -806,8 +902,14 @@ import static seedu.recipe.logic.parser.CliSyntax.PREFIX_IMG; import static seedu.recipe.logic.parser.CliSyntax.PREFIX_INGREDIENT; import static seedu.recipe.logic.parser.CliSyntax.PREFIX_INSTRUCTION; import static seedu.recipe.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.recipe.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.recipe.logic.parser.CliSyntax.PREFIX_URL; +import java.util.Arrays; +import java.util.stream.Collectors; + +import seedu.recipe.model.recipe.Recipe; + /** * A utility class containing a list of {@code Recipe} objects parsed from Wikia to be used in tests. */ @@ -839,6 +941,19 @@ public class WikiaRecipes { "http://recipes.wikia.com/wiki/Hainanese_Chicken_Rice?useskin=wikiamobile"; public static final String MOBILE_CHICKEN_RICE_IMAGE_URL = "https://vignette.wikia.nocookie.net/recipes/images/d/d3" + "/Chickenrice2.jpg/revision/latest/scale-to-width-down/340?cb=20080516004325"; + public static final String[] CHICKEN_TAGS = {"SingaporeanMeat", "ScrewPineLeaf", + "Chicken", "Cucumber", "Lettuce", "MainDishPoultry", "Pineapple", "Rice"}; + public static final Recipe HAINANESE_CHICKEN_RICE = new RecipeBuilder() + .withName(CHICKEN_NAME) + .withIngredient(CHICKEN_INGREDIENT) + .withInstruction(CHICKEN_INSTRUCTION) + .withCookingTime("-") + .withPreparationTime("-") + .withCalories("-") + .withServings("-") + .withUrl(WIKIA_RECIPE_URL_CHICKEN) + .withImage("-") + .withTags(CHICKEN_TAGS).build(); public static final String UGANDAN_NAME = "Ugandan Chicken Stew"; public static final String UGANDAN_INGREDIENT = "chicken, oil, onion, tomatoes, potatoes, salt, pepper"; @@ -849,6 +964,19 @@ public class WikiaRecipes { public static final String WIKIA_RECIPE_URL_UGANDAN = "http://recipes.wikia.com/wiki/Ugandan_Chicken_Stew"; public static final String MOBILE_WIKIA_RECIPE_URL_UGANDAN = "http://recipes.wikia.com/wiki/Ugandan_Chicken_Stew?useskin=wikiamobile"; + public static final String[] UGANDAN_TAGS = {"UgandanMeat", "Potato", "MainDishPoultry", "Tomato", "Stew", + "Chicken", "RecipesThatNeedPhotos"}; + public static final Recipe UGANDAN_CHICKEN_STEW = new RecipeBuilder() + .withName(UGANDAN_NAME) + .withIngredient(UGANDAN_INGREDIENT) + .withInstruction(UGANDAN_INSTRUCTION) + .withCookingTime("-") + .withPreparationTime("-") + .withCalories("-") + .withServings("-") + .withUrl(MOBILE_WIKIA_RECIPE_URL_UGANDAN) + .withImage("-") + .withTags(UGANDAN_TAGS).build(); public static final String WIKIA_RECIPE_URL_BEEF = "http://recipes.wikia.com/wiki/Beef_Tenderloin_with_Madeira_Sauce"; @@ -860,20 +988,31 @@ public class WikiaRecipes { public static final String WIKIA_CHICKEN_ADD_COMMAND = COMMAND_WORD + LF + PREFIX_NAME + CHICKEN_NAME + LF + PREFIX_INGREDIENT + CHICKEN_INGREDIENT + LF + PREFIX_INSTRUCTION + CHICKEN_INSTRUCTION + LF + PREFIX_IMG + CHICKEN_RICE_IMAGE_URL + LF - + PREFIX_URL + WIKIA_RECIPE_URL_CHICKEN; + + PREFIX_URL + WIKIA_RECIPE_URL_CHICKEN + LF + joinTags(CHICKEN_TAGS); public static final String WIKIA_UGANDAN_ADD_COMMAND = COMMAND_WORD + LF + PREFIX_NAME + UGANDAN_NAME + LF + PREFIX_INGREDIENT + UGANDAN_INGREDIENT + LF - + PREFIX_INSTRUCTION + UGANDAN_INSTRUCTION + LF + PREFIX_URL + WIKIA_RECIPE_URL_UGANDAN; + + PREFIX_INSTRUCTION + UGANDAN_INSTRUCTION + LF + PREFIX_URL + WIKIA_RECIPE_URL_UGANDAN + + LF + joinTags(UGANDAN_TAGS); public static final String MOBILE_WIKIA_CHICKEN_ADD_COMMAND = COMMAND_WORD + LF + PREFIX_NAME + CHICKEN_NAME + LF + PREFIX_INGREDIENT + CHICKEN_INGREDIENT + LF + PREFIX_INSTRUCTION + CHICKEN_INSTRUCTION + LF + PREFIX_IMG + MOBILE_CHICKEN_RICE_IMAGE_URL + LF - + PREFIX_URL + MOBILE_WIKIA_RECIPE_URL_CHICKEN; + + PREFIX_URL + MOBILE_WIKIA_RECIPE_URL_CHICKEN + LF + joinTags(CHICKEN_TAGS); public static final String MOBILE_WIKIA_UGANDAN_ADD_COMMAND = COMMAND_WORD + LF + PREFIX_NAME + UGANDAN_NAME + LF + PREFIX_INGREDIENT + UGANDAN_INGREDIENT + LF - + PREFIX_INSTRUCTION + UGANDAN_INSTRUCTION + LF + PREFIX_URL + MOBILE_WIKIA_RECIPE_URL_UGANDAN; + + PREFIX_INSTRUCTION + UGANDAN_INSTRUCTION + LF + PREFIX_URL + MOBILE_WIKIA_RECIPE_URL_UGANDAN + + LF + joinTags(UGANDAN_TAGS); private WikiaRecipes() { } // prevents instantiation + + /** + * Takes in an array of tag strings and returns a string that can be passed to an add or edit command. + */ + private static String joinTags(String[] tags) { + return Arrays.stream(tags) + .map(tag -> PREFIX_TAG + tag + " ") + .collect(Collectors.joining()); + } } ``` ###### \java\seedu\recipe\ui\CommandBoxTest.java @@ -888,6 +1027,7 @@ public class WikiaRecipes { ``` ###### \java\seedu\recipe\ui\CommandBoxTest.java ``` java + /** * Checks that the input in the {@code commandBox} equals to {@code expectedCommand}. */ @@ -900,10 +1040,12 @@ public class WikiaRecipes { ``` java package seedu.recipe.ui.parser; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static seedu.recipe.testutil.WikiaRecipes.CHICKEN_INGREDIENT; import static seedu.recipe.testutil.WikiaRecipes.CHICKEN_INSTRUCTION; import static seedu.recipe.testutil.WikiaRecipes.CHICKEN_NAME; +import static seedu.recipe.testutil.WikiaRecipes.CHICKEN_TAGS; import static seedu.recipe.testutil.WikiaRecipes.MOBILE_CHICKEN_RICE_IMAGE_URL; import static seedu.recipe.testutil.WikiaRecipes.MOBILE_WIKIA_CHICKEN_ADD_COMMAND; import static seedu.recipe.testutil.WikiaRecipes.MOBILE_WIKIA_RECIPE_URL_CHICKEN; @@ -912,6 +1054,7 @@ import static seedu.recipe.testutil.WikiaRecipes.MOBILE_WIKIA_UGANDAN_ADD_COMMAN import static seedu.recipe.testutil.WikiaRecipes.UGANDAN_INGREDIENT; import static seedu.recipe.testutil.WikiaRecipes.UGANDAN_INSTRUCTION; import static seedu.recipe.testutil.WikiaRecipes.UGANDAN_NAME; +import static seedu.recipe.testutil.WikiaRecipes.UGANDAN_TAGS; import java.io.IOException; @@ -982,6 +1125,12 @@ public class MobileWikiaParserTest extends GuiUnitTest { assertEquals(wikiaParserUgandan.getUrl(), MOBILE_WIKIA_RECIPE_URL_UGANDAN); } + @Test + public void getTags_validRecipes_returnsResult() throws Exception { + assertArrayEquals(wikiaParserChicken.getTags(), CHICKEN_TAGS); + assertArrayEquals(wikiaParserUgandan.getTags(), UGANDAN_TAGS); + } + @Test public void parseRecipe_validRecipe_returnsValidCommand() throws Exception { assertEquals(wikiaParserChicken.parseRecipe(), MOBILE_WIKIA_CHICKEN_ADD_COMMAND); @@ -1076,15 +1225,18 @@ public class WebParserHandlerTest extends GuiUnitTest { ``` java package seedu.recipe.ui.parser; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static seedu.recipe.testutil.WikiaRecipes.BEEF_INGREDIENT; import static seedu.recipe.testutil.WikiaRecipes.CHICKEN_INGREDIENT; import static seedu.recipe.testutil.WikiaRecipes.CHICKEN_INSTRUCTION; import static seedu.recipe.testutil.WikiaRecipes.CHICKEN_NAME; import static seedu.recipe.testutil.WikiaRecipes.CHICKEN_RICE_IMAGE_URL; +import static seedu.recipe.testutil.WikiaRecipes.CHICKEN_TAGS; import static seedu.recipe.testutil.WikiaRecipes.UGANDAN_INGREDIENT; import static seedu.recipe.testutil.WikiaRecipes.UGANDAN_INSTRUCTION; import static seedu.recipe.testutil.WikiaRecipes.UGANDAN_NAME; +import static seedu.recipe.testutil.WikiaRecipes.UGANDAN_TAGS; import static seedu.recipe.testutil.WikiaRecipes.WIKIA_CHICKEN_ADD_COMMAND; import static seedu.recipe.testutil.WikiaRecipes.WIKIA_NOT_RECIPE; import static seedu.recipe.testutil.WikiaRecipes.WIKIA_RECIPE_URL_BEEF; @@ -1166,6 +1318,12 @@ public class WikiaParserTest extends GuiUnitTest { assertEquals(wikiaParserUgandan.getUrl(), WIKIA_RECIPE_URL_UGANDAN); } + @Test + public void getTags_validRecipes_returnsResult() throws Exception { + assertArrayEquals(wikiaParserChicken.getTags(), CHICKEN_TAGS); + assertArrayEquals(wikiaParserUgandan.getTags(), UGANDAN_TAGS); + } + @Test public void parseRecipe_validRecipe_returnsValidCommand() throws Exception { String a = WIKIA_CHICKEN_ADD_COMMAND; @@ -1210,7 +1368,9 @@ package systemtests; import static org.junit.Assert.assertTrue; import static seedu.recipe.TestApp.APP_TITLE; -import static GIRL_THEME_CSS; +import static seedu.recipe.testutil.TypicalIndexes.INDEX_FIRST_RECIPE; +import static seedu.recipe.testutil.TypicalRecipes.getTypicalRecipeBook; +import static seedu.recipe.ui.MainWindow.GIRL_THEME_CSS; import static seedu.recipe.ui.MainWindow.LIGHT_THEME_CSS; import static seedu.recipe.ui.UiPart.FXML_FILE_FOLDER; @@ -1219,6 +1379,10 @@ import org.junit.Test; import guitests.GuiRobot; import seedu.recipe.MainApp; import seedu.recipe.logic.commands.ChangeThemeCommand; +import seedu.recipe.logic.commands.SelectCommand; +import seedu.recipe.model.Model; +import seedu.recipe.model.ModelManager; +import seedu.recipe.model.UserPrefs; public class ChangeThemeSystemTest extends RecipeBookSystemTest { private static final String ERROR_MESSAGE = "ATTENTION!!!! : On some computers, this test may fail when run on " @@ -1226,6 +1390,8 @@ public class ChangeThemeSystemTest extends RecipeBookSystemTest { + "that this is a bug with TestFX library that we are using. If this test fails, you have to run your " + "tests on headless mode. See UsingGradle.adoc on how to do so."; + private Model model = new ModelManager(getTypicalRecipeBook(), new UserPrefs()); + private final GuiRobot guiRobot = new GuiRobot(); @Test @@ -1261,6 +1427,11 @@ public class ChangeThemeSystemTest extends RecipeBookSystemTest { executeCommand(ChangeThemeCommand.COMMAND_WORD); assertLightTheme(); + + String command = SelectCommand.COMMAND_WORD + " " + INDEX_FIRST_RECIPE.getOneBased(); + executeCommand(command); + executeCommand(ChangeThemeCommand.COMMAND_WORD); + assertDarkTheme(); } /** @@ -1268,7 +1439,7 @@ public class ChangeThemeSystemTest extends RecipeBookSystemTest { */ private void assertDarkTheme() { assertTrue(ERROR_MESSAGE, guiRobot.getStage(APP_TITLE).getScene().getStylesheets().get(0) - .equals(MainApp.class.getResource(FXML_FILE_FOLDER + DARK_THEME_CSS).toExternalForm())); + .equals(MainApp.class.getResource(FXML_FILE_FOLDER + GIRL_THEME_CSS).toExternalForm())); guiRobot.pauseForHuman(); } @@ -1286,38 +1457,94 @@ public class ChangeThemeSystemTest extends RecipeBookSystemTest { ``` java package systemtests; +import static org.junit.Assert.assertEquals; import static seedu.recipe.testutil.TypicalIndexes.INDEX_FIRST_RECIPE; import static seedu.recipe.testutil.TypicalIndexes.INDEX_SECOND_RECIPE; +import static seedu.recipe.testutil.TypicalIndexes.INDEX_THIRD_RECIPE; import static seedu.recipe.testutil.TypicalRecipes.getTypicalRecipeBook; +import static seedu.recipe.testutil.TypicalRecipes.getTypicalRecipes; +import static seedu.recipe.testutil.WikiaRecipes.HAINANESE_CHICKEN_RICE; import static seedu.recipe.testutil.WikiaRecipes.MOBILE_WIKIA_UGANDAN_ADD_COMMAND; +import static seedu.recipe.testutil.WikiaRecipes.UGANDAN_CHICKEN_STEW; import static seedu.recipe.testutil.WikiaRecipes.WIKIA_CHICKEN_ADD_COMMAND; +import java.io.File; +import java.util.List; + +import org.junit.After; import org.junit.Test; import seedu.recipe.logic.commands.ParseCommand; import seedu.recipe.logic.commands.SelectCommand; +import seedu.recipe.logic.commands.UndoCommand; import seedu.recipe.model.Model; import seedu.recipe.model.ModelManager; import seedu.recipe.model.UserPrefs; +import seedu.recipe.model.recipe.Image; +import seedu.recipe.model.recipe.Recipe; +import seedu.recipe.storage.ImageDownloader; +import seedu.recipe.testutil.RecipeBuilder; /** - * A system test class for the search command, which contains interaction with other UI components. + * A system test class for the parse command, which contains interaction with other UI components. */ public class ParseCommandSystemTest extends RecipeBookSystemTest { + private static final String HAINANESE_CHICKEN_RICE_IMAGE_PATH = + Image.IMAGE_STORAGE_FOLDER + "7F474E50D9E9F21A980A30B4D54308AD." + + ImageDownloader.DOWNLOADED_IMAGE_FORMAT; + private Model model = new ModelManager(getTypicalRecipeBook(), new UserPrefs()); + private List recipes = getTypicalRecipes(); @Test public void parse() { + // First add String command = SelectCommand.COMMAND_WORD + " " + INDEX_FIRST_RECIPE.getOneBased(); executeCommand(command); + assertCommandSuccess(WIKIA_CHICKEN_ADD_COMMAND); + getCommandBox().submitCommand(); + + Recipe testRecipe = new RecipeBuilder(HAINANESE_CHICKEN_RICE).withImage( + HAINANESE_CHICKEN_RICE_IMAGE_PATH).build(); + recipes.add(testRecipe); + assertEquals(recipes, getModel().getFilteredRecipeList()); + + // Remove recipe + command = UndoCommand.COMMAND_WORD; + executeCommand(command); + recipes.remove(testRecipe); + assertEquals(recipes, getModel().getFilteredRecipeList()); + + // Then add again + command = SelectCommand.COMMAND_WORD + " " + INDEX_FIRST_RECIPE.getOneBased(); + executeCommand(command); assertCommandSuccess(WIKIA_CHICKEN_ADD_COMMAND); + getCommandBox().submitCommand(); + + recipes.add(testRecipe); + assertEquals(recipes, getModel().getFilteredRecipeList()); + // Try to parse and add another recipe command = SelectCommand.COMMAND_WORD + " " + INDEX_SECOND_RECIPE.getOneBased(); executeCommand(command); - assertCommandSuccess(MOBILE_WIKIA_UGANDAN_ADD_COMMAND); + getCommandBox().submitCommand(); + recipes.add(UGANDAN_CHICKEN_STEW); + assertEquals(recipes, getModel().getFilteredRecipeList()); + + + // Not supported site + command = SelectCommand.COMMAND_WORD + " " + INDEX_THIRD_RECIPE.getOneBased(); + executeCommand(command); + assertCommandSuccess(""); + } + + @After + public void cleanUp() { + File file = new File(HAINANESE_CHICKEN_RICE_IMAGE_PATH); + file.delete(); } /** @@ -1325,7 +1552,6 @@ public class ParseCommandSystemTest extends RecipeBookSystemTest { * and the current content of the CommandBox is {@code content} */ private void assertCommandSuccess(String content) { - executeCommand(ParseCommand.COMMAND_WORD); assertStatusBarUnchanged(); assertCommandBoxContent(content); diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 30e77b918ef2..5e7cb2a167f8 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -453,8 +453,8 @@ private void loadStyle(boolean darkTheme) { ** Cons: There is no default page to display so we have to find another approach to make `_BrowserPanel_` display something as a placeholder, *without* using a default page. // end::change-theme[] -// tag::cloud-storage[] === Cloud storage support +// tag::cloud-storage[] ==== Current implementation A new command `_UploadCommand_` has been added to allow users to upload data in the recipe book onto the cloud. `_UploadCommandParser_` takes in and checks the `_String_` filename, adds an XML extension to it before saving it to `_CloudStorageUtil_` and parsing it to `_UploadCommand_`. diff --git a/docs/RyanAng.adoc b/docs/RyanAng.adoc deleted file mode 100644 index dfc350885b79..000000000000 --- a/docs/RyanAng.adoc +++ /dev/null @@ -1,66 +0,0 @@ -= Ryan Ang - Project Portfolio -:imagesDir: images -:stylesDir: stylesheets - -== PROJECT: ReciRecipé - -Welcome to my portfolio page for CS2103T Academic Year 2018 Semester 2 project - ReciRecipé. - -== Project Overview - -include::./UserGuide.adoc[tag=applicationDescription] - -* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ - -[[code-contributions]] -== Summary of contributions - -=== Enhancement: Unique Tag Colors -==== Why? -Users love visually aesthetic applications! But on a more serious note, different tag colors help users differentiate their tags at at glance. - -This feature has been implemented in version 1.1, in PR link:https://github.com/CS2103JAN2018-F09-B2/main/pull/5[#5]. - -include::./DeveloperGuide.adoc[tag=unique-tag-colors] - -=== Enhancement: Embedded URL -==== Why? -A user may want to save an online recipe, or perhaps even a YouTube video, into ReciRecipé. - -This feature has been implemented in version 1.1, in PR link:https://github.com/CS2103JAN2018-F09-B2/main/pull/35[#35]. - -include::./DeveloperGuide.adoc[tag=embedded-url] - -=== New feature: Facebook Compatibility -==== Why? -In this modern day and age, users like to share their lives with their friends. Why not share recipes too? - -This feature has been implemented in version 1.3, in PR link:https://github.com/CS2103JAN2018-F09-B2/main/pull/74[#74]. - -include::./UserGuide.adoc[leveloffset=1,tag=share-command] - -include::./DeveloperGuide.adoc[tag=facebook-compatibility] - -=== New feature: Image Support -==== Why? -Users want to know how awesome their dish looks like once they have cooked it. - -This feature has been implemented in version 1.4, in PR link:https://github.com/CS2103JAN2018-F09-B2/main/pull/86[#86]. - -include::./DeveloperGuide.adoc[tag=inserting-an-image] - -== Other contributions - -Listed below are other contributions I have made to the ReciRecipé project: - -* UI Design: -** Redesigned the layout and appearance of the original UI. - -* Project Management: -** Set up team organisation and repository. -** Managed issues on GitHub. -** Reviewed teammates' pull requests via GitHub. - -* Other Documentation Contributions: -** Contributed to User Stories and Use Cases. - diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 4a639de24a7c..ba388d948468 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -287,6 +287,7 @@ Format: `history` Pressing the kbd:[↑] and kbd:[↓] arrows while holding down kbd:[Ctrl] will display the previous and next input respectively in the command box. ==== +// tag::ingredientCommand[] === *`ingredient`* : Locating recipes by ingredients Finds recipes whose ingredients contain all of the given keywords. @@ -306,6 +307,7 @@ Examples: Returns recipe with ingredient _Chicken_. * *`ingredient chicken fish pasta`* + Returns only recipes that contain all `_chicken_`, `_fish_`, and `_pasta_` as ingredients. +// end::ingredientCommand[] === *`list`* : Listing all recipes @@ -453,6 +455,7 @@ image::FacebookFeedDialog.jpg[width="600"] The User will be redirected back to his/her Facebook feed once the post has been made. //end::share-command[] +// tag::tagCommand[] === *`tag`* : Locating recipes by tags Finds recipe whose tags contain any of the given keywords. @@ -472,6 +475,7 @@ Examples: Returns all recipes with the tag `_drink_`. * *`tag drink food`* + Returns any recipe with the tag `_drink_` or `_food_`. +// end::tagCommand[] //tag::change-theme[] === *`theme`* : Changing theme @@ -491,6 +495,7 @@ image::ChangeTheme.PNG[width="750"] You can change theme from the menu bar or by pressing F2 too! //end::change-theme[] +// tag::tokenCommand[] [[token]] === *`token`* : Exchanging authorization code for access token @@ -510,6 +515,7 @@ image::AuthorizationCode.PNG[width="750"] * This command only accepts valid authorization codes. * If the authorization code is invalid, an error will be thrown indicating that the application was unable to upload the file. **** +// end::tokenCommand[] === *`undo`* : Undoing previous command @@ -541,6 +547,7 @@ Reverses the *`clear`* command. + *`undo`* + Reverses the *`delete 1`* command. + +// tag::uploadCommand[] === *`upload`* : Uploading all recipes Uploads the xml file containing data of all recipes to Dropbox, giving it the specified filename. @@ -568,6 +575,7 @@ Data file will be uploaded to Dropbox with the name RecipeBook.xml. + *`add`*, *`delete`* or *`edit`*. + *`upload RecipeBook`* + Updated data file will be uploaded to Dropbox with the name RecipeBook(1).xml. +// end::uploadCommand[] // tag::view-group-command[] === *`view_group`* : Viewing groups of recipes diff --git a/docs/team/NicholasAng.adoc b/docs/team/NicholasAng.adoc new file mode 100644 index 000000000000..79562ceb2a0d --- /dev/null +++ b/docs/team/NicholasAng.adoc @@ -0,0 +1,88 @@ += Nicholas Ang - Project Portfolio +:imagesDir: ../images +:stylesDir: ../stylesheets +:sectnums: + +== PROJECT: ReciRecipé + +Welcome to my portfolio page for CS2103T Academic Year 2018 Semester 2 project - ReciRecipé. + +== Overview + +include::./UserGuide.adoc[tag=applicationDescription] + +== Summary of contributions + +* *Major enhancement*: Added the ability for cloud storage of the data in ReciRecipé. +** What it does: It allows the user to upload all the recipes they have saved in the application onto their own Dropbox account. +** Justification: Saving all of their painstakingly searched, added or crafted recipes solely on their own hard drives is very risky as it could be lost very easily. Furthermore, if they accidentally cleared and closed the app, all the data will disappear too. +This function allows them to retrieve their recipes any time they want from any computer, as long as they have ReciRecipé. +** Highlights: This enhancement involves authentication and authorization from Dropbox. It required an in-depth understanding of the Dropbox APIs and how to handle authorization codes and access tokens. + +* *Minor enhancement*: Added a new command `tag` that allowed user to search for recipes with the specified tagname(s). [OR search] +* *Minor enhancement*: Added a new command `ingredient` that allowed user to search for recipes with the specified ingredient(s). [AND search] + +* *Code contributed*: [https://github.com/CS2103JAN2018-F09-B2/main/blob/master/collated/functional/nicholasangcx.md[Functional code]] [https://github.com/CS2103JAN2018-F09-B2/main/blob/master/collated/test/nicholasangcx.md[Test code]] + +* *Other contributions*: + +** Project management: +*** Managed releases `v1.3` - `v1.4` (2 releases) on GitHub. +*** Managed some issues on GitHub. + +** Documentation: +*** Fixed the overall language of the DG and changed the formats to be consistent (i.e. all in proper sentences). [https://github.com/CS2103JAN2018-F09-B2/main/pull/135[PR #135]] +*** Fixed language issues/formatting in the UG and updated the images to fit the new UI theme. [https://github.com/CS2103JAN2018-F09-B2/main/pull/138[PR #138]][https://github.com/CS2103JAN2018-F09-B2/main/pull/139[PR #139]] + +[[code-contributions]] +== Code Contributions +|=== +|_Given below are the code enhancements I have made to the project and also sections I contributed to the Developer Guide based on those enhancements. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +=== Major Enhancement: Cloud Storage +This feature has been implemented in version 1.3, in link:https://github.com/CS2103JAN2018-F09-B2/main/pull/79[PR #79] and link:https://github.com/CS2103JAN2018-F09-B2/main/pull/88[PR #88]. +Quite a bit of additional work was required as I had to change my original implementation due to Dropbox not supporting login through a Webview. A system browser was used in the end and a lot of the underlying implementation code had to be changed. + +==== Why? +Saving all of their painstakingly searched, added or crafted recipes solely on their own hard drives is very risky as it could be lost very easily. Furthermore, if they accidentally cleared and closed the app, all the data will disappear too. + +include::./DeveloperGuide.adoc[tag=cloud-storage] + +=== Minor Enhancement: Tag Search +This feature has been implemented in version 1.1, in link:https://github.com/CS2103JAN2018-F09-B2/main/pull/33[PR #33]. +This feature allows the user to search for recipes based on the tags allocated to them. + +==== Why? +This allows the user to organise their recipes more efficiently and can obtain the ones they are interested in faster. For example, tagging their frequently used recipes with favourites, a simple search of favourites would list all those recipes immediately. + +include::../DeveloperGuide.adoc[tag=tagSearch] + +=== Minor Enhancement: Ingredient Search +This feature has been implemented in version 1.4, in link:https://github.com/CS2103JAN2018-F09-B2/main/pull/98[PR #98]. This feature allows the user to search for recipes based on the ingredients specified within them. + +==== Why? +This allows the user to maximise the ingredients they currently have, searching for recipes that contain ALL the ingredients that they have searched for. + +include::../DeveloperGuide.adoc[tag=ingredientSearch] + +== Contributions to User Guide +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +=== Usage +Listed here are the commands I have added, in alphabetical order, in order to make my enhancements work, and how I explained the usage of these commands. + +include::./UserGuide.adoc[tag=ingredientCommand] +include::./UserGuide.adoc[tag=tagCommand] +include::./UserGuide.adoc[tag=tokenCommand] +include::./UserGuide.adoc[tag=uploadCommand] + + + + + + + +