diff --git a/.gitignore b/.gitignore index 2873e189e1..05a3d5ce36 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +*.class +duke.txt +tasks.txt +data.txt diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..bb420086c2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive", + "java.checkstyle.configuration": "${workspaceFolder}/config/checkstyle/checkstyle.xml", + "java.checkstyle.version": "9.3" +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..70f0691218 --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'checkstyle' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + + String javaFxVersion = '17.0.7' + + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClass.set("xavier/Launcher") +} + +shadowJar { + archiveBaseName = "xavier" + archiveClassifier = null + dependsOn("distZip", "distTar") +} + +run { + standardInput = System.in +} + +checkstyle { + toolVersion = '10.2' +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..eb761a9b9a --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..39efb6e4ac --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/docs/README.md b/docs/README.md index 8077118ebe..487f251ac3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,180 @@ -# User Guide +# Xavier User Guide +## Introduction + +![Ui](./Ui.png) + +Xavier is a simple chatbot which helps to manage your daily tasks! + +Download it from [here](https://github.com/shunjieee/ip/releases/download/A-Release/xavier.jar) now! + +--- ## Features +List of features supported: +* Add +* Delete +* List +* Mark +* Unmark +* Find +* Sort +* Exit + +--- +### Adding a task: `todo`, `deadline` and `event` + +There are three types tasks that Xavier supports, to-do, deadline and event. The commands add the respective task to the task list. + +***todo*** + +Formart: +`todo ` + +Example: +`todo wash dishes` + +Expected outcome: +``` +Got it. I've added this task: + [T][ ] wash dishes +Now you have 1 tasks in the list. +``` + +***deadline*** + +Format: +`deadline /by ` + +Example: +`deadline CS2103T Quiz 7 /by 7/3/2024-2359` + +Expected outcome: +``` +Got it. I've added this task: + [D][ ] CS2103T Quiz 7 (by: Mar 07 2024, 23:59) +Now you have 2 tasks in the list. +``` + +***event*** + +Format: +`event /from /to ` + +Example: +`event Taylor Swift Concert /from 3/3/2024-1900 /to 3/3/2024-2200` + +Expected outcome: +``` +Got it. I've added this task: + [E][ ] Taylor Swift Concert (from: Mar 03 2024, 19:00 to: Mar 03 2024, 22:00) +Now you have 3 tasks in the list. +``` + +> [!IMPORTANT] +> The date-time format **MUST** be in **dd/MM/yyyy-HHmm**. + +--- +### Deleting a task: `delete` -### Feature-ABC +Format: +`delete ` -Description of the feature. +Example: +`delete 1` -### Feature-XYZ +Expected outcome: +``` +Noted, I've removed this task: + [T][ ] wash dishes +Now you have 2 tasks in the list. +``` -Description of the feature. +--- +### Listing all the task in the list: `list` -## Usage +Format: +`list` -### `Keyword` - Describe action +Example and expected outcome: +``` +Here are the tasks in your list: +1. [D][ ] CS2103T Quiz 7 (by: Mar 07 2024, 23:59) +2. [E][ ] Taylor Swift Concert (from: Mar 03 2024, 19:00 to: Mar 03 2024, 22:00) +``` -Describe the action and its outcome. +--- +### Marking task as done: `mark` -Example of usage: +Format: +`mark ` -`keyword (optional arguments)` +Example: +`mark 1` Expected outcome: +``` +Nice! I've marked this task as done: + [D][X] CS2103T Quiz 7 (by: Mar 07 2024, 23:59) +``` + +--- +### Unmarking task: `unmark` + +Format: +`unmark ` + +Example: +`mark 2` + +Expected outcome: +``` +OK, I've marked this task as not done yet: + [E][ ] Taylor Swift Concert (from: Mar 03 2024, 19:00 to: Mar 03 2024, 22:00) +``` + +--- +### Finding tasks in the list: `find` + +Format: +`find ` + +Example: +`find Quiz` + +Expected outcome: +``` +Here are the matching tasks in your list: +1. [D][X] CS2103T Quiz 7 (by: Mar 07 2024, 23:59) +``` +> [!NOTE] +> `keyword` is case sensitive. + +--- +### Sorting the list: `sort` + +The list will be sorting with the unmark tasks at the top and marked tasks at the bottom. In each section, the tasks will be sorted alphabetically. + +Format: +`sort` + +Example and expected outcome: +``` +Here are the tasks in your list: +1. [E][ ] Taylor Swift Concert (from: Mar 03 2024, 19:00 to: Mar 03 2024, 22:00) +2. [D][X] CS2103T Quiz 7 (by: Mar 07 2024, 23:59) +``` + +--- +### Exiting the program: `bye` + +The program will save the list in `./data/data.txt` before exiting. -Description of the outcome. +Format: +`bye` +Example and expected outcome **(only CLI)**: ``` -expected output +Saving data ... +Data saved successfully. :) +Bye. Hope to see you again soon! ``` diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..3403c771e6 Binary files /dev/null and b/docs/Ui.png differ diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334cc..0000000000 --- a/src/main/java/Duke.java +++ /dev/null @@ -1,10 +0,0 @@ -public class Duke { - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - } -} diff --git a/src/main/java/command/AddCommand.java b/src/main/java/command/AddCommand.java new file mode 100644 index 0000000000..15a64aa641 --- /dev/null +++ b/src/main/java/command/AddCommand.java @@ -0,0 +1,145 @@ +package command; + +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +import common.DukeException; +import task.Deadline; +import task.Event; +import task.Task; +import task.TaskList; +import task.ToDo; + +/** + * {@inheritDocs} + * Adds a task into a tasklist. + */ +public class AddCommand extends Command { + private String command; + private TaskList taskList; + private StringTokenizer st; + + /** + * Creates an instance of AddCommand. + */ + public AddCommand(String command, TaskList taskList, StringTokenizer st) { + this.command = command; + this.taskList = taskList; + this.st = st; + } + + /** + * {@inheritDocs} + * Adds a task into a tasklist. + * + * @throws DukeException If the command cannot be executed. + */ + @Override + public String execute() throws DukeException { + try { + String taskName = ""; + + switch (command) { + case "todo": + while (st.hasMoreTokens()) { + taskName += st.nextToken() + " "; + } + + if (taskName.equals("")) { + throw new DukeException("Missing task name. :("); + + } else { + ToDo td = new ToDo(taskName.strip()); + taskList.add(td); + assert taskList.contains(td) : taskName + " is not added."; + + return produceResponse(td); + } + + case "deadline": + String deadline = ""; + + while (st.hasMoreTokens()) { + String temp = st.nextToken(); + if (temp.equals("/by")) { + deadline = st.nextToken(); + break; + + } else { + taskName += temp + " "; + } + } + + if (taskName.equals("") || deadline.equals("")) { + throw new DukeException("Missing field(s) / incorrect input(s). :(" + + "\nCheck if you have used the keyword \"/by\""); + + } else { + Deadline d = new Deadline(taskName.strip(), deadline); + taskList.add(d); + assert taskList.contains(d) : taskName + " is not added."; + + return produceResponse(d); + } + + case "event": + String startTime = ""; + String endTime = ""; + + while (st.hasMoreTokens()) { + String temp = st.nextToken(); + + if (temp.equals("/from")) { + startTime = st.nextToken(); + continue; + + } else if (temp.equals("/to")) { + endTime = st.nextToken(); + break; + + } else { + taskName += temp + " "; + } + } + + if (taskName.equals("") || startTime.equals("") || endTime.equals("")) { + throw new DukeException("Missing field(s) / incorrect input(s). :(\n" + + "Check if you have used the keyword \"/from\" and \"/to\""); + + } else { + Event e = new Event(taskName.strip(), startTime, endTime); + taskList.add(e); + assert taskList.contains(e) : taskName + " is not added."; + + return produceResponse(e); + } + + default: + return "Missing field(s) / incorrect input(s). :("; + } + + } catch (NoSuchElementException e) { + System.out.println("Missing field(s) / incorrect input(s). :("); + return "Missing field(s) / incorrect input(s). :("; + } + } + + private String produceResponse(Task t) { + String s = "Got it. I've added this task:\n" + + " " + t.toString() + + "\nNow you have " + taskList.size() + " tasks in the list."; + + System.out.println(s); + return s; + } + + /** + * {@inheritDoc} + * + * @return True if program will exit. + */ + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/command/Command.java b/src/main/java/command/Command.java new file mode 100644 index 0000000000..30b07e970f --- /dev/null +++ b/src/main/java/command/Command.java @@ -0,0 +1,22 @@ +package command; + +import common.DukeException; + +/** + * Represents a command to be executed by the program. + */ +public abstract class Command { + /** + * Executes the command. + * + * @throws DukeException If command cannot be executed. + */ + public abstract String execute() throws DukeException; + + /** + * Returns true if the command executed exits the program. + * + * @return True if program will exit. + */ + public abstract boolean isExit(); +} diff --git a/src/main/java/command/DeleteCommand.java b/src/main/java/command/DeleteCommand.java new file mode 100644 index 0000000000..64a5a82d00 --- /dev/null +++ b/src/main/java/command/DeleteCommand.java @@ -0,0 +1,52 @@ +package command; + +import common.DukeException; +import task.Task; +import task.TaskList; + +/** + * {@inheritDocs} + * Deletes a task from a tasklist. + */ +public class DeleteCommand extends Command { + private TaskList taskList; + private int taskIndex; + + /** + * Creates an instance of DeleteCommand. + */ + public DeleteCommand(TaskList taskList, int taskIndex) { + this.taskList = taskList; + this.taskIndex = taskIndex; + } + + /** + * {@inheritDocs} + * Deletes a task from a tasklist. + * + * @throws DukeException If the command cannot be executed. + */ + @Override + public String execute() { + Task t = taskList.remove(taskIndex); + assert !taskList.contains(t) : t.getTaskName() + " is not removed."; + + + String s = "Noted, I've removed this task:\n" + + " " + t.toString() + + "\nNow you have " + taskList.size() + " tasks in the list."; + + System.out.println(s); + return s; + } + + /** + * {@inheritDoc} + * + * @return True if program will exit. + */ + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/command/ExitCommand.java b/src/main/java/command/ExitCommand.java new file mode 100644 index 0000000000..bc6a455677 --- /dev/null +++ b/src/main/java/command/ExitCommand.java @@ -0,0 +1,29 @@ +package command; + +/** + * {@inheritDocs} + * Exits the program. + */ +public class ExitCommand extends Command { + + /** + * {@inheritDocs} + * Exits the program. + */ + @Override + public String execute() { + String exitMessage = "Bye. Hope to see you again soon!"; + System.out.println(exitMessage); + return exitMessage; + } + + /** + * {@inheritDoc} + * + * @return True if program will exit. + */ + @Override + public boolean isExit() { + return true; + } +} diff --git a/src/main/java/command/FindCommand.java b/src/main/java/command/FindCommand.java new file mode 100644 index 0000000000..bd4c4a903c --- /dev/null +++ b/src/main/java/command/FindCommand.java @@ -0,0 +1,58 @@ +package command; + +import java.util.LinkedList; +import java.util.StringTokenizer; + +import task.Task; +import task.TaskList; + +/** + * {@inheritDocs} + * Finds a task by searching for a keyword. + */ +public class FindCommand extends Command { + private TaskList taskList; + private String keyword; + + /** + * Creates an instance of FindCommand. + */ + public FindCommand(TaskList taskList, StringTokenizer st) { + this.taskList = taskList; + keyword = st.nextToken(); + } + + /** + * {@inheritDocs} + * Finds a task by searching for a keyword. + */ + @Override + public String execute() { + String result = "Here are the matching tasks in your list:\n"; + System.out.print(result); + + LinkedList tl = taskList.getList(); + + int counter = 1; + for (Task t : tl) { + if (t.hasKeyword(keyword)) { + String taskString = counter + ". " + t.toString(); + System.out.println(taskString); + result += taskString + "\n"; + + counter++; + } + } + return result; + } + + /** + * {@inheritDoc} + * + * @return True if program will exit. + */ + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/command/ListCommand.java b/src/main/java/command/ListCommand.java new file mode 100644 index 0000000000..f2a645b51a --- /dev/null +++ b/src/main/java/command/ListCommand.java @@ -0,0 +1,49 @@ +package command; + +import task.Task; +import task.TaskList; + +/** + * {@inheritDocs} + * List all the tasks in the task list. + */ +public class ListCommand extends Command { + private TaskList taskList; + + /** + * Creates an instance of ListCommand. + */ + public ListCommand(TaskList taskList) { + this.taskList = taskList; + } + + /** + * {@inheritDocs} + * List all the tasks in the task list. + */ + @Override + public String execute() { + String result = "Here are the tasks in your list:\n"; + System.out.print(result); + + for (int i = 1; i <= taskList.size(); i++) { + Task t = taskList.get(i); + + String taskString = i + ". " + t.toString(); + System.out.println(taskString); + result += taskString + "\n"; + } + + return result; + } + + /** + * {@inheritDoc} + * + * @return True if program will exit. + */ + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/command/MarkCommand.java b/src/main/java/command/MarkCommand.java new file mode 100644 index 0000000000..6a0f23a135 --- /dev/null +++ b/src/main/java/command/MarkCommand.java @@ -0,0 +1,49 @@ +package command; + +import task.Task; +import task.TaskList; + +/** + * {@inheritDocs} + * Marks a task as done. + */ +public class MarkCommand extends Command { + private TaskList taskList; + private int taskIndex; + + /** + * Creates an instance of MarkCommand. + */ + public MarkCommand(TaskList taskList, int taskIndex) { + this.taskList = taskList; + this.taskIndex = taskIndex; + } + + /** + * {@inheritDocs} + * Marks a task as done. + */ + @Override + public String execute() { + Task t = taskList.get(taskIndex); + t.markAsDone(); + assert t.checkStatus() : t.getTaskName() + " not marked as done."; + + String s = "Nice! I've marked this task as done:\n" + + " " + t.toString(); + + System.out.println(s); + + return s; + } + + /** + * {@inheritDoc} + * + * @return True if program will exit. + */ + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/command/SortCommand.java b/src/main/java/command/SortCommand.java new file mode 100644 index 0000000000..92011fd082 --- /dev/null +++ b/src/main/java/command/SortCommand.java @@ -0,0 +1,58 @@ +package command; + +import java.util.Comparator; + +import task.Task; +import task.TaskList; + +/** + * {@inheritDocs} + * Sort the tasks in the task list. + */ +public class SortCommand extends Command { + private TaskList taskList; + + /** + * Creates an instance of SortCommand. + */ + public SortCommand(TaskList taskList) { + this.taskList = taskList; + } + + /** + * {@inheritDocs} + * List all the tasks in the task list. + */ + @Override + public String execute() { + taskList.getList().sort(new StatusComparator()); + String message = "I have sorted the task list according to the tasks' statuses."; + return message + "\n\n" + new ListCommand(taskList).execute(); + } + + /** + * {@inheritDoc} + * + * @return True if program will exit. + */ + @Override + public boolean isExit() { + return false; + } + + static class StatusComparator implements Comparator { + // undone status are on top of the list + @Override + public int compare(Task task1, Task task2) { + if (!task1.checkStatus() && task2.checkStatus()) { + return -1; + + } else if (task1.checkStatus() && !task2.checkStatus()) { + return 1; + + } else { + return task1.getTaskName().compareToIgnoreCase(task2.getTaskName()); + } + } + } +} diff --git a/src/main/java/command/UnmarkCommand.java b/src/main/java/command/UnmarkCommand.java new file mode 100644 index 0000000000..33f37899ed --- /dev/null +++ b/src/main/java/command/UnmarkCommand.java @@ -0,0 +1,49 @@ +package command; + +import task.Task; +import task.TaskList; + +/** + * {@inheritDocs} + * Marks a task as undone. + */ +public class UnmarkCommand extends Command { + private TaskList taskList; + private int taskIndex; + + /** + * Creates an instance of UnmarkCommand. + */ + public UnmarkCommand(TaskList taskList, int taskIndex) { + this.taskList = taskList; + this.taskIndex = taskIndex; + } + + /** + * {@inheritDocs} + * Marks a task as undone. + */ + @Override + public String execute() { + Task t = taskList.get(taskIndex); + t.unmark(); + assert !t.checkStatus() : t.getTaskName() + " not unmarked."; + + String s = "OK, I've marked this task as not done yet:\n" + + " " + t.toString(); + + System.out.println(s); + + return s; + } + + /** + * {@inheritDoc} + * + * @return True if program will exit. + */ + @Override + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/common/DukeException.java b/src/main/java/common/DukeException.java new file mode 100644 index 0000000000..bee2efb9f6 --- /dev/null +++ b/src/main/java/common/DukeException.java @@ -0,0 +1,15 @@ +package common; + +/** + * Represents an exception specific to this program. + */ +public class DukeException extends Exception { + /** + * Creates an exception with the provided error message. + * + * @param message The error message of the exception. + */ + public DukeException(String message) { + super(message); + } +} diff --git a/src/main/java/common/Parser.java b/src/main/java/common/Parser.java new file mode 100644 index 0000000000..85bb004631 --- /dev/null +++ b/src/main/java/common/Parser.java @@ -0,0 +1,93 @@ +package common; + +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +import command.AddCommand; +import command.Command; +import command.DeleteCommand; +import command.ExitCommand; +import command.FindCommand; +import command.ListCommand; +import command.MarkCommand; +import command.SortCommand; +import command.UnmarkCommand; +import task.TaskList; + +/** + * Parses the user input and creates a command accordingly. + */ +public class Parser { + private StringTokenizer st; + private String command; + private TaskList tasks; + + /** + * Creates an instance of a Parser object to parse the user input. + * + * @param fullCommand The entire user input. + * @param tasks The list of tasks. + */ + public Parser(String fullCommand, TaskList tasks) { + st = new StringTokenizer(fullCommand); + command = st.nextToken().toLowerCase(); + this.tasks = tasks; + } + + /** + * Returns the command object after parsing through the user input. + * + * @return The command object created. + * @throws IndexOutOfBoundException If the user input a number more/less than the number of tasks in the list. + * @throws NumberFormatException If the user input is invalid + * @throws NoSuchElementException If the user input is invalid + * @throws DukeException If there are other unexpected errors + */ + public Command parse() throws IndexOutOfBoundsException, NumberFormatException, + NoSuchElementException, DukeException { + + Command cmd; + switch (command) { + case "list": + cmd = new ListCommand(tasks); + return cmd; + + case "mark": + int indexOfTaskToMark = Integer.parseInt(st.nextToken()); + cmd = new MarkCommand(tasks, indexOfTaskToMark); + return cmd; + + case "unmark": + int indexOfTaskToUnmark = Integer.parseInt(st.nextToken()); + cmd = new UnmarkCommand(tasks, indexOfTaskToUnmark); + return cmd; + + case "delete": + int indexOfTaskToDelete = Integer.parseInt(st.nextToken()); + cmd = new DeleteCommand(tasks, indexOfTaskToDelete); + return cmd; + + case "todo": + case "deadline": + case "event": + cmd = new AddCommand(command, tasks, st); + return cmd; + + case "find": + cmd = new FindCommand(tasks, st); + return cmd; + + case "sort": + cmd = new SortCommand(tasks); + return cmd; + + case "bye": + cmd = new ExitCommand(); + return cmd; + + default: + throw new DukeException("OOPS!! Pls try again, hustler. :)"); + } + } +} + diff --git a/src/main/java/common/Storage.java b/src/main/java/common/Storage.java new file mode 100644 index 0000000000..c0b08bca09 --- /dev/null +++ b/src/main/java/common/Storage.java @@ -0,0 +1,160 @@ +package common; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.LinkedList; +import java.util.Scanner; + +import task.Deadline; +import task.Event; +import task.Task; +import task.TaskList; +import task.ToDo; + +/** + * Loads and saves the list of tasks to a file. + */ +public class Storage { + private static int MAX_ATTEMPT = 2; + private String filepath; + + /** + * Returns an instance of Storage that will load/save to the given filepath. + * + * @param filepath The filepath of the file to load/saves the tasks. + */ + public Storage(String filepath) { + this.filepath = filepath; + } + + /** + * Checks if filepath exist, if not creates one from the current directory. + * Maximum attempt of 2, else the program will exit with error. + */ + public void checkForFilePath() { + int currentAttempt = 0; + File file; + + while (++currentAttempt <= MAX_ATTEMPT) { + try { + Ui.showLine(); + String counter = "Startup Attempt #" + currentAttempt + "/" + MAX_ATTEMPT + ":"; + System.out.println(counter); + + file = new File(filepath); + if (file.createNewFile()) { + String s1 = "tasks.txt does not exist."; + String s2 = "tasks.txt successfully created."; + System.out.println(s1); + System.out.println(s2); + + } else { + String s = "tasks.txt already exist."; + System.out.println(s); + } + break; + + } catch (IOException e) { + System.out.println("IOException occured: " + e.getMessage()); + + File dir = new File("./data"); + boolean isDirectoryCreated = dir.mkdir(); + if (isDirectoryCreated) { + String s = "Directory ./data created."; + System.out.println(s); + } + + } finally { + Ui.showLine(); + } + } + } + + /** + * Returns the list of tasks loaded from the storage file. + * + * @return The list of tasks loaded in a LinkedList. + */ + public LinkedList loadData() { + checkForFilePath(); + LinkedList tasks = new LinkedList<>(); + + try { + Scanner fileScanner = new Scanner(new File(filepath)); + boolean hasError = false; + + while (fileScanner.hasNext()) { + String[] taskFields = fileScanner.nextLine().split(" \\| "); + String taskType = taskFields[0]; + boolean isDone = Integer.parseInt(taskFields[1]) == 1 ? true : false; + + if (taskType.equals("T")) { + ToDo td = new ToDo(isDone, taskFields[2]); + tasks.add(td); + + } else if (taskType.equals("D")) { + try { + Deadline d = new Deadline(isDone, taskFields[2], taskFields[3]); + tasks.add(d); + + } catch (DukeException e) { + System.out.println(e.getMessage() + "\n"); + hasError = true; + } + + } else if (taskType.equals("E")) { + try { + Event e = new Event(isDone, taskFields[2], taskFields[3]); + tasks.add(e); + + } catch (DukeException e) { + System.out.println(e.getMessage() + "\n"); + hasError = true; + } + } + } + fileScanner.close(); + + if (hasError) { + Ui.showLine(); + + } else { + String s = "tasks.txt loaded without error."; + System.out.println(s); + Ui.showLine(); + } + + } catch (FileNotFoundException e) { + System.out.println("FileNotFoundException occured: " + e.getMessage()); + } + return tasks; + } + + /** + * Saves the current list of tasks to the storage file. + * + * @param tasks The list of tasks to save. + */ + public void saveDataAndExit(TaskList tasks) { + try { + System.out.println("Saving data ..."); + + File file = new File(filepath); + FileWriter fw = new FileWriter(file, false); + + LinkedList taskList = tasks.getList(); + for (Task t : taskList) { + fw.write(t.toData()); + fw.write(System.lineSeparator()); + } + + fw.close(); + System.out.println("Data saved successfully. :)"); + + } catch (IOException e) { + System.out.println("Error while saving data: " + e.getMessage()); + } + } +} diff --git a/src/main/java/common/Ui.java b/src/main/java/common/Ui.java new file mode 100644 index 0000000000..f5ea7ef084 --- /dev/null +++ b/src/main/java/common/Ui.java @@ -0,0 +1,37 @@ +package common; + +import java.util.Scanner; + +/** + * Represents the user interface that interacts with the user. + */ +public class Ui { + private Scanner scanner = new Scanner(System.in); + + /** + * Reads the user input. + */ + public String readCommand() { + return scanner.nextLine().strip(); + } + + /** + * Shows the start up message upon successful loading of the program. + */ + public String showWelcome() { + String welcomeMessage = "Good Morning, Hustler! I'm Xavier.\n" + + "What can I do for you?"; + System.out.println(welcomeMessage); + showLine(); + + return welcomeMessage; + } + + /** + * Shows the separating line for different messages. + */ + public static void showLine() { + String line = "________________________________________\n"; + System.out.println(line); + } +} diff --git a/src/main/java/task/Deadline.java b/src/main/java/task/Deadline.java new file mode 100644 index 0000000000..b6fed99be1 --- /dev/null +++ b/src/main/java/task/Deadline.java @@ -0,0 +1,66 @@ +package task; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import common.DukeException; + +/** + * {@inheritDoc} + */ +public class Deadline extends Task { + protected LocalDateTime deadline; + private DateTimeFormatter receivingFormatter = DateTimeFormatter.ofPattern("d/M/yyyy-HHmm"); + private DateTimeFormatter printingFormatter = DateTimeFormatter.ofPattern("MMM dd yyyy, HH:mm"); + + /** + * {@inheritDoc} + */ + public Deadline(String taskName, String deadline) throws DukeException { + super(taskName); + + try { + this.deadline = LocalDateTime.parse(deadline, receivingFormatter); + + } catch (DateTimeParseException e) { + throw new DukeException("Date and Time not in the correct format.\n" + + "Correct format: dd/MM/yyyy-HHmm\n" + + "Received: " + deadline + "\n" + + "\"" + taskName + "\" not added to the list."); + } + } + + /** + * {@inheritDoc} + */ + public Deadline(boolean isDone, String taskName, String deadline) throws DukeException { + super(isDone, taskName); + + try { + this.deadline = LocalDateTime.parse(deadline, printingFormatter); + + } catch (DateTimeParseException e) { + throw new DukeException("Date and Time not in the correct format.\n" + + "Correct format: MMM dd yyyy, HH:mm\n" + + "Received: " + taskName + " | " + deadline + "\n" + + "\"" + taskName + "\" removed from the list."); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + deadline.format(printingFormatter) + ")"; + } + + /** + * {@inheritDoc} + */ + @Override + public String toData() { + return "D | " + super.toData() + " | " + deadline.format(printingFormatter); + } +} diff --git a/src/main/java/task/Event.java b/src/main/java/task/Event.java new file mode 100644 index 0000000000..70fe6c4894 --- /dev/null +++ b/src/main/java/task/Event.java @@ -0,0 +1,81 @@ +package task; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import common.DukeException; + +/** + * {@inheritDoc} + */ +public class Event extends Task { + protected LocalDateTime startTime; + protected LocalDateTime endTime; + private DateTimeFormatter receivingFormatter = DateTimeFormatter.ofPattern("d/M/yyyy-HHmm"); + private DateTimeFormatter printingFormatter = DateTimeFormatter.ofPattern("MMM dd yyyy, HH:mm"); + + /** + * {@inheritDoc} + */ + public Event(String taskName, String startTime, String endTime) throws DukeException { + super(taskName); + + try { + this.startTime = LocalDateTime.parse(startTime, receivingFormatter); + + } catch (DateTimeParseException e) { + throw new DukeException("Start time not in the correct format.\n" + + "Correct format: dd/MM/yyyy-HHmm\n" + + "Received: " + startTime + "\n" + + "\"" + taskName + "\" not added to the list."); + } + + try { + this.endTime = LocalDateTime.parse(endTime, receivingFormatter); + + } catch (DateTimeParseException e) { + throw new DukeException("End time not in the correct format.\n" + + "Correct format: dd/MM/yyyy-HHmm\n" + + "Received: " + endTime + "\n" + + "\"" + taskName + "\" not added to the list."); + } + } + + /** + * {@inheritDoc} + */ + public Event(boolean isDone, String taskName, String time) throws DukeException { + super(isDone, taskName); + + try { + String[] startToEndTime = time.split(" - "); + startTime = LocalDateTime.parse(startToEndTime[0], printingFormatter); + endTime = LocalDateTime.parse(startToEndTime[1], printingFormatter); + + } catch (DateTimeParseException e) { + throw new DukeException("Date and Time not in the correct format.\n" + + "Correct format: MMM dd yyyy, HH:mm\n" + + "Received: " + taskName + " | " + time + "\n" + + "\"" + taskName + "\" removed from the list."); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "[E]" + super.toString() + " (from: " + startTime.format(printingFormatter) + + " to: " + endTime.format(printingFormatter) + ")"; + } + + /** + * {@inheritDoc} + */ + @Override + public String toData() { + return "E | " + super.toData() + " | " + startTime.format(printingFormatter) + + " - " + endTime.format(printingFormatter); + } +} diff --git a/src/main/java/task/Task.java b/src/main/java/task/Task.java new file mode 100644 index 0000000000..63040eb5d1 --- /dev/null +++ b/src/main/java/task/Task.java @@ -0,0 +1,89 @@ +package task; + +/** + * Represents a task to be stored in the list of tasks. + */ +public class Task { + protected String taskName; + protected boolean isDone; + + /** + * Returns an instance of Task. Mark by default as undone. + * + * @param taskName User-defined task name. + */ + public Task(String taskName) { + this.taskName = taskName; + this.isDone = false; + } + + /** + * Returns an instance of Task. Status of task is provided by the user. + * + * @param isDone The status of the task. + * @param taskName User-defined task name. + */ + public Task(boolean isDone, String taskName) { + this.taskName = taskName; + this.isDone = isDone; + } + + /** + * Returns task name + */ + public String getTaskName() { + return taskName; + } + + /** + * Returns task status + * + * @return True, if done. Else, false. + */ + public boolean checkStatus() { + return isDone; + } + + /** + * Returns the status of the task. + * + * @return A string to indicate the status. + */ + public String getStatusIcon() { + return (isDone ? "[X] " : "[ ] "); // mark done task with X + } + + /** + * Sets the status of task as done. + */ + public void markAsDone() { + this.isDone = true; + } + + /** + * Sets the status of task as not done. + */ + public void unmark() { + this.isDone = false; + } + + /** + * Returns the string format of the task for printing to the ui. + */ + @Override + public String toString() { + return getStatusIcon() + taskName; + } + + /** + * Returns the string format of the task for writing to save file. + */ + public String toData() { + return (isDone ? "1" : "0") + " | " + taskName; + } + + public boolean hasKeyword(String keyword) { + return taskName.contains(keyword); + } +} + diff --git a/src/main/java/task/TaskList.java b/src/main/java/task/TaskList.java new file mode 100644 index 0000000000..7df864fef4 --- /dev/null +++ b/src/main/java/task/TaskList.java @@ -0,0 +1,62 @@ +package task; + +import java.util.LinkedList; + +/** + * Represents a list of tasks to be stored. + */ +public class TaskList { + private LinkedList tasks; + + /** + * Creates an instance of TaskList containing tasks. + * + * @param tasks List of tasks to be stored upon instantiating. + */ + public TaskList(LinkedList tasks) { + this.tasks = tasks; + } + + /** + * Returns the number of tasks in the list. + */ + public int size() { + return tasks.size(); + } + + /** + * Get a task from the list. User-input is 1-indexed. + */ + public Task get(int i) { + return tasks.get(i - 1); + } + + /** + * Adds the task into the list. + */ + public void add(Task task) { + tasks.add(task); + } + + /** + * Removes a task from the list. User-input is 1-indexed. + */ + public Task remove(int indexOfTask) { + Task task = tasks.remove(indexOfTask - 1); + return task; + } + + public LinkedList getList() { + return tasks; + } + + /** + * Checks if task is in the list. + * + * @param t The task to be checked. + * @return True if task is in the list. Else, false. + */ + public boolean contains(Task t) { + return tasks.contains(t); + } +} diff --git a/src/main/java/task/ToDo.java b/src/main/java/task/ToDo.java new file mode 100644 index 0000000000..7ffaf32d14 --- /dev/null +++ b/src/main/java/task/ToDo.java @@ -0,0 +1,37 @@ +package task; + +/** + * {@inheritDoc} + */ +public class ToDo extends Task { + + /** + * {@inheritDoc} + */ + public ToDo(String taskName) { + super(taskName); + } + + /** + * {@inheritDoc} + */ + public ToDo(boolean isDone, String taskName) { + super(isDone, taskName); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "[T]" + super.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toData() { + return "T | " + super.toData(); + } +} diff --git a/src/main/java/xavier/DialogBox.java b/src/main/java/xavier/DialogBox.java new file mode 100644 index 0000000000..0f83e86672 --- /dev/null +++ b/src/main/java/xavier/DialogBox.java @@ -0,0 +1,61 @@ +package xavier; +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +/** + * An example of a custom control using FXML. + * This control represents a dialog box consisting of an ImageView to represent the speaker's face and a label + * containing text from the speaker. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + private DialogBox(String text, Image img) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(img); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + } + + public static DialogBox getUserDialog(String text, Image img) { + return new DialogBox(text, img); + } + + public static DialogBox getXavierDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + return db; + } +} diff --git a/src/main/java/xavier/Launcher.java b/src/main/java/xavier/Launcher.java new file mode 100644 index 0000000000..6eecd88e76 --- /dev/null +++ b/src/main/java/xavier/Launcher.java @@ -0,0 +1,11 @@ +package xavier; +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/xavier/Main.java b/src/main/java/xavier/Main.java new file mode 100644 index 0000000000..5e95fe22e8 --- /dev/null +++ b/src/main/java/xavier/Main.java @@ -0,0 +1,52 @@ +package xavier; +import java.io.IOException; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +/** + * A GUI for Duke using FXML. + */ +public class Main extends Application { + + private Xavier xavier = new Xavier("./data/data.txt"); + + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + Scene scene = new Scene(ap); + + stage.setScene(scene); + stage.setTitle("I'm Xavier, serving your's truly :)"); + fxmlLoader.getController().setXavier(xavier); + fxmlLoader.getController().showWelcome(); + stage.show(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Exits the program when the byeCommand is executed. + * + * @param isExit Boolean generated after each command is executed. + */ + public static void exit(boolean isExit) { + if (isExit) { + try { + Thread.sleep(1500); + Platform.exit(); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/xavier/MainWindow.java b/src/main/java/xavier/MainWindow.java new file mode 100644 index 0000000000..81ebd4b225 --- /dev/null +++ b/src/main/java/xavier/MainWindow.java @@ -0,0 +1,62 @@ +package xavier; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +/** + * Controller for MainWindow. Provides the layout for the other controls. + */ +public class MainWindow extends AnchorPane { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private Xavier xavier; + + private Image userImage = new Image(this.getClass().getResourceAsStream("/images/DaUser.png")); + private Image xavierImage = new Image(this.getClass().getResourceAsStream("/images/DaXavier.png")); + + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + } + + public void setXavier(Xavier x) { + xavier = x; + } + + /** + * Creates a dialog box with the welcome message and appends it to the dialog container. + */ + @FXML + public void showWelcome() { + dialogContainer.getChildren().add( + DialogBox.getXavierDialog(xavier.showWelcome(), xavierImage) + ); + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing xavier's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + String response = xavier.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getXavierDialog(response, xavierImage) + ); + userInput.clear(); + Platform.runLater(() -> Main.exit(xavier.getIsExit())); + } +} diff --git a/src/main/java/xavier/Xavier.java b/src/main/java/xavier/Xavier.java new file mode 100644 index 0000000000..efdb6e0926 --- /dev/null +++ b/src/main/java/xavier/Xavier.java @@ -0,0 +1,76 @@ +package xavier; +import java.util.NoSuchElementException; + +import command.Command; +import common.DukeException; +import common.Parser; +import common.Storage; +import common.Ui; +import task.TaskList; + +/** + * The Duke program implements a chatbot, now named NextGenerationJarvis, that keeps track of tasks for the user. + */ +public class Xavier { + private Storage storage; + private TaskList tasks; + private Ui ui; + private boolean isExit; + + /** + * Returns an instance of the program and loads the tasks from the file found at the provided filepath. + * + * @param filepath The filepath of the file to load/save the tasks. + */ + public Xavier(String filepath) { + ui = new Ui(); + storage = new Storage(filepath); + tasks = new TaskList(storage.loadData()); + + } + + public String showWelcome() { + return ui.showWelcome(); + } + + /** + * Returns the response from the program as a String when given the input provided. + * + * @param input The input for the program to parse and execute. + * @return The response from the program. + */ + public String getResponse(String input) { + try { + Command cmd = new Parser(input, tasks).parse(); + isExit = cmd.isExit(); + if (isExit) { + storage.saveDataAndExit(tasks); + return cmd.execute(); + } + return cmd.execute(); + + } catch (IndexOutOfBoundsException e) { + System.out.println("Invalid task number. :("); + return "Invalid task number. :("; + + } catch (NumberFormatException e) { + System.out.println("Input is not an integer. :("); + return "Input is not an integer. :("; + + } catch (NoSuchElementException e) { + System.out.println("Missing task number. :("); + return "Missing task number. :("; + + } catch (DukeException e) { + System.out.println(e.getMessage()); + return e.getMessage(); + + } finally { + Ui.showLine(); + } + } + + public boolean getIsExit() { + return isExit; + } +} diff --git a/src/main/resources/images/DaUser.png b/src/main/resources/images/DaUser.png new file mode 100644 index 0000000000..4872387890 Binary files /dev/null and b/src/main/resources/images/DaUser.png differ diff --git a/src/main/resources/images/DaXavier.png b/src/main/resources/images/DaXavier.png new file mode 100644 index 0000000000..45a16e8725 Binary files /dev/null and b/src/main/resources/images/DaXavier.png differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..bfd5f702f8 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..d9e881aaa7 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + +