diff --git a/build.gradle b/build.gradle index c361f9811e..c64ad741ef 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'application' id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'checkstyle' } repositories { @@ -11,6 +12,20 @@ repositories { 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 { @@ -28,14 +43,20 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("serenity.gui.Launcher") } shadowJar { - archiveBaseName = "duke" + archiveBaseName = "serenity" archiveClassifier = null + archiveFileName = 'serenity.jar' +} + +checkstyle { + toolVersion = '10.2' } run{ standardInput = System.in + enableAssertions = true; } diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..acac1a8e28 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..135ea49ee0 --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/data/serenity.txt b/data/serenity.txt new file mode 100644 index 0000000000..110d834386 --- /dev/null +++ b/data/serenity.txt @@ -0,0 +1,4 @@ +D | 1 | return book | 30 Aug 2024 +T | 1 | read book +E | 0 | attend meeting | 3pm | 5pm +D | 0 | write essay | 22 Sep 2024 diff --git a/docs/README.md b/docs/README.md index 47b9f984f7..b34d6c010c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,126 @@ -# Duke User Guide +# Serenity User Guide -// Update the title above to match the actual product name +![Ui](./Ui.png) -// Product screenshot goes here +Serenity is a chatbot that helps you to manage your tasks in a simple and efficient manner. + +## Adding a todo task + +Adds a todo task to the task list. + +Format: `todo DESCRIPTION` + +Examples: +- `todo read book` +- `todo watch movie` + +## Adding a deadline task + +Adds a deadline task to the task list. + +Format: `deadline DESCRIPTION /by DATE` + +- The `DATE` should be in the format `DD/MM/YYYY` + +Examples: +- `deadline return book /by 19/09/2024` +- `deadline write essay /by 20/10/2025` + +## Adding an event task + +Adds a deadline task to the task list. + +Format: `deadline DESCRIPTION /from START /to END` + +Examples: +- `event project meeting /from Monday 2pm /to 4pm` +- `event hackathon /from Tuesday /to Friday` + +## Listing all tasks + +Shows a list of all tasks in the task list. + +Format: `list` + +## Marking a task + +Marks the task at the given index as done. + +Format: `mark INDEX` + +- The index refers to the index number shown in the task list +- The index must be a positive integer +- The index cannot be greater than the number of tasks in the task list + +Example: +- `mark 1` + +## Unmarking a task + +Unmarks the task at the given index as done. + +Format: `unmark INDEX` + +- The index refers to the index number shown in the task list +- The index must be a positive integer +- The index cannot be greater than the number of tasks in the task list + +Example: +- `unmark 1` + +## Finding a task + +Finds all tasks in the task list which contains the given keyword. + +Format: `find KEYWORD` + +- The search is case-sensitive e.g. `book` will not match `Book` +- Only description is searched + +Example: +- `find book` +- `find write` + +## Deleting a task +Deletes the task at the given index. + +Format: `delete INDEX` + +- The index refers to te index number shown in the task list +- The index must be a positive integer +- The index cannot be greater than the number of tasks in the task list + +Example: +- `delete 1` + +## Updating a task + +Updates the task at the given index + +Format: `update INDEX /update TASK_TYPE DESCRIPTION` + +- The task type is either `todo`, `deadline`, or `event` +- The description required is dependent on the task type + +Examples: +- `update 1 /update todo read mystery book` +- `update 2 /update deadline return book /by 22/09/2024` +- `update 3 /update event project meeting /from Monday 3pm /to 5pm` + +## Exiting chatbot + +Displays goodbye message and exits chatbot. + +Format: `bye` -// Product intro goes here -## Adding deadlines -// Describe the action and its outcome. -// Give examples of usage -Example: `keyword (optional arguments)` -// A description of the expected outcome goes here -``` -expected output -``` -## Feature ABC -// Feature details -## Feature XYZ -// Feature details \ No newline at end of file diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..298f8604a6 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/serenity/Parser.java b/src/main/java/serenity/Parser.java new file mode 100644 index 0000000000..6d3a810d34 --- /dev/null +++ b/src/main/java/serenity/Parser.java @@ -0,0 +1,134 @@ +package serenity; + +import java.io.IOException; + +import serenity.task.Task; +import serenity.task.TaskList; + + +/** + * Represents a parser that parses user inputs and executes the actions corresponding to the command. + */ +public class Parser { + + /** + * Parses user input, carries out corresponding actions and + * displays corresponding message. + * + * @param input User's input. + * @param tasks List of tasks. + * @param ui User interface. + * @param storage Data storage. + * @throws SerenityException If user input is invalid and task cannot be created. + * @throws IOException If there is an issue writing to file that stores data. + */ + public static void parse(String input, TaskList tasks, Ui ui, Storage storage) + throws SerenityException, IOException { + String[] parts = input.split(" ", 2); + String command = parts[0].strip(); + String message; + Task t; + + switch (command) { + case "bye": + ui.showGoodbye(); + break; + case "list": + ui.showTaskList(tasks); + break; + case "todo", "deadline", "event": + t = TaskList.createTask(input); + message = tasks.addTask(t); + ui.showMessage(message); + storage.saveTask(t); + break; + case "mark", "unmark": + message = tasks.changeStatus(input); + ui.showMessage(message); + storage.writeToFile(tasks); + break; + case "delete": + message = tasks.deleteTask(input); + ui.showMessage(message); + storage.writeToFile(tasks); + break; + case "find": + message = tasks.findTask(input); + ui.showMessage(message); + break; + case "update": + message = tasks.updateTask(input); + ui.showMessage(message); + storage.writeToFile(tasks); + break; + default: + ui.showMessage("Error: Invalid task."); + break; + } + + } + + /** + * Parses user input, carries out corresponding actions and + * returns message to be displayed. + * + * @param input User's input + * @param tasks List of tasks. + * @param storage Data storage + * @return Message to be displayed. + */ + public static String parseToString(String input, TaskList tasks, Storage storage) { + String[] parts = input.split(" ", 2); + String command = parts[0].strip(); + String message; + Task t; + + try { + switch (command) { + case "bye": + message = "Goodbye. Hope to see you again soon!"; + break; + case "list": + message = tasks.toString(); + break; + case "todo", "deadline", "event": + t = TaskList.createTask(input); + message = tasks.addTask(t); + storage.saveTask(t); + break; + case "mark", "unmark": + message = tasks.changeStatus(input); + storage.writeToFile(tasks); + break; + case "delete": + message = tasks.deleteTask(input); + storage.writeToFile(tasks); + break; + case "find": + message = tasks.findTask(input); + break; + case "update": + message = tasks.updateTask(input); + storage.writeToFile(tasks); + break; + default: + message = "Error: Invalid task."; + break; + } + } catch (SerenityException | IOException e) { + return e.getMessage(); + } + return message; + } + + /** + * Checks if command is to exit chatbot. + * + * @param input User's input. + * @return True if the command is bye. + */ + public static boolean isExit(String input) { + return input.startsWith("bye"); + } + +} diff --git a/src/main/java/serenity/Serenity.java b/src/main/java/serenity/Serenity.java new file mode 100644 index 0000000000..af97c83872 --- /dev/null +++ b/src/main/java/serenity/Serenity.java @@ -0,0 +1,93 @@ +package serenity; + +import java.io.IOException; + +import serenity.task.TaskList; + +/** + * Represents a chatbot used for task management. + */ +@SuppressWarnings("checkstyle:Regexp") +public class Serenity { + + private static final String DEFAULT_FILE_PATH = "./data/serenity.txt"; + private Storage storage; + private TaskList tasks; + private Ui ui; + + /** + * Constructs a Serenity object. + * + * @param filePath The path of file where data is stored. + */ + public Serenity(String filePath) { + ui = new Ui(); + storage = new Storage(filePath); + try { + tasks = new TaskList(storage.loadFile()); + } catch (SerenityException e) { + ui.showLoadingError(); + tasks = new TaskList(); + } catch (IOException e) { + ui.showMessage(e.getMessage()); + } + } + + /** + * Constructs a Serenity object with default file path. + */ + public Serenity() { + ui = new Ui(); + storage = new Storage(DEFAULT_FILE_PATH); + try { + tasks = new TaskList(storage.loadFile()); + } catch (SerenityException e) { + ui.showLoadingError(); + tasks = new TaskList(); + } catch (IOException e) { + ui.showMessage(e.getMessage()); + } + } + + /** + * Generates response to user's input in chat. + * + * @param input User's input + */ + public String getResponse(String input) { + return Parser.parseToString(input, tasks, storage); + } + + /** + * Generates welcome message. + * + * @return Welcome message. + */ + public String welcome() { + return "Hi, I'm Serenity!\n" + "What can I do for you?"; + } + + + /** + * Runs the Serenity chatbot + */ + public void run() { + + ui.showWelcome(); + boolean isExit = false; + + while (!isExit) { + try { + String input = ui.readCommand(); + Parser.parse(input, tasks, ui, storage); + isExit = Parser.isExit(input); + } catch (SerenityException | IOException e) { + ui.showMessage(e.getMessage()); + } + } + } + public static void main(String[] args) { + new Serenity("./data/serenity.txt").run(); + } + +} diff --git a/src/main/java/serenity/SerenityException.java b/src/main/java/serenity/SerenityException.java new file mode 100644 index 0000000000..b844c01725 --- /dev/null +++ b/src/main/java/serenity/SerenityException.java @@ -0,0 +1,16 @@ +package serenity; + +/** + * Represents a custom exception in the Serenity class. + */ +public class SerenityException extends Exception { + + /** + * Constructs a SerenityException. + * + * @param msg The error message of the exception. + */ + public SerenityException(String msg) { + super(msg); + } +} diff --git a/src/main/java/serenity/Storage.java b/src/main/java/serenity/Storage.java new file mode 100644 index 0000000000..5402159a5b --- /dev/null +++ b/src/main/java/serenity/Storage.java @@ -0,0 +1,139 @@ +package serenity; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Scanner; + +import serenity.task.Deadline; +import serenity.task.Event; +import serenity.task.Task; +import serenity.task.TaskList; +import serenity.task.Todo; + + +/** + * Handles the storing, loading and saving of tasks to data file. + */ +public class Storage { + + protected String filePath; + + /** + * Constructs a Storage object. + * + * @param filePath The path of file where data is stored. + */ + public Storage(String filePath) { + this.filePath = filePath; + } + + /** + * Loads tasks stored in data file to chatbot. + * + * @return ArrayList of tasks loaded from data file. + * @throws IOException If there are issues with creating directory or file + * @throws SerenityException If there are issues with loading a task. + */ + public ArrayList loadFile() throws IOException, SerenityException { + File f = new File(filePath); + File directory = new File(f.getParentFile().getAbsolutePath()); + directory.mkdirs(); + if (!f.exists()) { + Files.createFile(Paths.get(filePath)); + } + + assert f.exists() : "File should exist."; + + Scanner sc = new Scanner(f); + ArrayList tasks = new ArrayList<>(); + while (sc.hasNext()) { + tasks.add(loadTask(sc.nextLine())); + } + sc.close(); + return tasks; + } + + /** + * Converts string representation of task stored in data file to a Task. + * + * @param taskDescription String representation of task stored in data file. + * @return Task created with the task description. + * @throws SerenityException If task description is invalid and task cannot be created. + */ + public Task loadTask(String taskDescription) throws SerenityException { + String[] parts = taskDescription.split("\\|"); + Task t; + + for (int i = 0; i < parts.length; i++) { + parts[i] = parts[i].strip(); + } + + String taskType = parts[0]; + + switch (taskType) { + case "T": + t = new Todo(parts[2]); + break; + case "D": + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd MMM yyyy"); + LocalDate date = LocalDate.parse(parts[3], formatter); + String by = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + t = new Deadline(parts[2], by); + break; + case "E": + t = new Event(parts[2], parts[3], parts[4]); + break; + default: + throw new SerenityException("Error: Task cannot be loaded."); + } + + if (parts[1].equals("1")) { + t.markAsDone(); + } + + return t; + } + + /** + * Writes to data file by appending text to existing content. + * + * @param textToAppend Text to be appended. + * @throws IOException If there are issues accessing file. + */ + public void appendToFile(String textToAppend) throws IOException { + FileWriter fw = new FileWriter(filePath, true); + fw.write(textToAppend + "\n"); + fw.close(); + } + + /** + * Saves task to data file. + * + * @param t Task to be saved to file. + * @throws IOException If there are issues appending text to file. + */ + public void saveTask(Task t) throws IOException { + String textToAppend = t.formatData(); + appendToFile(textToAppend); + } + + /** + * Writes to file by overwriting existing content. + * + * @param tasks Tasks to be saved to file. + * @throws IOException If there are issues writing to file. + */ + public void writeToFile(TaskList tasks) throws IOException { + //overwrite + FileWriter fw = new FileWriter(filePath); + fw.write(tasks.formatData()); + fw.close(); + } + +} diff --git a/src/main/java/serenity/Ui.java b/src/main/java/serenity/Ui.java new file mode 100644 index 0000000000..41b90504c1 --- /dev/null +++ b/src/main/java/serenity/Ui.java @@ -0,0 +1,78 @@ +package serenity; + +import java.util.Scanner; + +import serenity.task.TaskList; + + +/** + * Represents user interface that handles user interactions. + */ +public class Ui { + + private static final String HORIZONTAL_LINE = "__________________________________________"; + protected Scanner sc; + + /** + * Constructs Ui object + */ + public Ui() { + this.sc = new Scanner(System.in); + } + + /** + * Displays welcome message. + */ + public void showWelcome() { + System.out.println(HORIZONTAL_LINE); + System.out.println("Hi, I'm Serenity!\n" + "What can I do for you?"); + System.out.println(HORIZONTAL_LINE); + } + + /** + * Displays goodbye message and close scanner. + */ + public void showGoodbye() { + this.sc.close(); + System.out.println(HORIZONTAL_LINE); + System.out.println("Goodbye. Hope to see you again soon!"); + System.out.println(HORIZONTAL_LINE); + } + + /** + * Gets command from user input. + * @return User's input. + */ + public String readCommand() { + return this.sc.nextLine(); + } + + /** + * Displays message of loading error. + */ + public void showLoadingError() { + System.out.println("Error: File cannot be loaded."); + } + + /** + * Displays message to user. + * + * @param message Message to be displayed. + */ + public void showMessage(String message) { + System.out.println(HORIZONTAL_LINE); + System.out.println(message); + System.out.println(HORIZONTAL_LINE); + } + + /** + * Displays TaskList to user. + * + * @param tasks TaskList to be displayed. + */ + public void showTaskList(TaskList tasks) { + System.out.println(HORIZONTAL_LINE); + System.out.println(tasks.toString()); + System.out.println(HORIZONTAL_LINE); + } +} diff --git a/src/main/java/serenity/gui/DialogBox.java b/src/main/java/serenity/gui/DialogBox.java new file mode 100644 index 0000000000..94a34ccf7c --- /dev/null +++ b/src/main/java/serenity/gui/DialogBox.java @@ -0,0 +1,61 @@ +package serenity.gui; + +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; + +/** + * 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 getSerenityDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + return db; + } +} + diff --git a/src/main/java/serenity/gui/Launcher.java b/src/main/java/serenity/gui/Launcher.java new file mode 100644 index 0000000000..1ec4d42717 --- /dev/null +++ b/src/main/java/serenity/gui/Launcher.java @@ -0,0 +1,12 @@ +package serenity.gui; + +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/serenity/gui/Main.java b/src/main/java/serenity/gui/Main.java new file mode 100644 index 0000000000..e9db461ae2 --- /dev/null +++ b/src/main/java/serenity/gui/Main.java @@ -0,0 +1,37 @@ +package serenity.gui; + +import java.io.IOException; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; +import serenity.Serenity; + +/** + * A GUI for Duke using FXML. + */ +public class Main extends Application { + + private Serenity serenity = new Serenity(); + + @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("Serenity"); + // inject the serenity instance + MainWindow window = fxmlLoader.getController(); + window.setSerenity(serenity); + window.start(); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} + diff --git a/src/main/java/serenity/gui/MainWindow.java b/src/main/java/serenity/gui/MainWindow.java new file mode 100644 index 0000000000..3f2f0d49e8 --- /dev/null +++ b/src/main/java/serenity/gui/MainWindow.java @@ -0,0 +1,66 @@ +package serenity.gui; + +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; +import serenity.Serenity; + +/** + * Controller for the main GUI. + */ +public class MainWindow extends AnchorPane { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private Serenity serenity; + + private Image userImage = new Image(this.getClass().getResourceAsStream("/images/User.png")); + private Image serenityImage = new Image(this.getClass().getResourceAsStream("/images/Serenity.png")); + + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + } + + /** Injects the Serenity instance */ + public void setSerenity(Serenity s) { + serenity = s; + } + + /** + * Starts chatbot with welcome message. + */ + + public void start() { + dialogContainer.getChildren().addAll(DialogBox.getSerenityDialog(serenity.welcome(), serenityImage)); + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing Duke'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 = serenity.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getSerenityDialog(response, serenityImage) + ); + userInput.clear(); + if (input.equals("bye")) { + Platform.exit(); + } + } +} diff --git a/src/main/java/serenity/task/Deadline.java b/src/main/java/serenity/task/Deadline.java new file mode 100644 index 0000000000..88c62b5a2d --- /dev/null +++ b/src/main/java/serenity/task/Deadline.java @@ -0,0 +1,55 @@ +package serenity.task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import serenity.SerenityException; + + + +/** + * Represents a type of Task that has a deadline. + */ +public class Deadline extends Task { + + protected LocalDate date; + + /** + * Constructs a Deadline. + * + * @param description Description of task. + * @param by Deadline of task. + */ + public Deadline(String description, String by) throws SerenityException { + super(description); + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[dd/MM/yyyy]"); + this.date = LocalDate.parse(by, formatter); + } catch (DateTimeParseException e) { + throw new SerenityException("Error: Invalid date. Please enter date in format dd/mm/yyyy."); + } + } + + /** + * Returns string representation of Deadline. + * + * @return String representation. + */ + @Override + public String toString() { + String formatDate = date.format(DateTimeFormatter.ofPattern("dd MMM yyyy")); + return "[D]" + super.toString() + " (by: " + formatDate + ")"; + } + + /** + * Returns string representation of Deadline to save as data. + * + * @return String representation to save as data. + */ + @Override + public String formatData() { + String formatDate = date.format(DateTimeFormatter.ofPattern("dd MMM yyyy")); + return "D | " + super.formatData() + " | " + formatDate; + } +} diff --git a/src/main/java/serenity/task/Event.java b/src/main/java/serenity/task/Event.java new file mode 100644 index 0000000000..78a0678d57 --- /dev/null +++ b/src/main/java/serenity/task/Event.java @@ -0,0 +1,44 @@ +package serenity.task; + +/** + * Represents a type of Task that has start and end timings. + */ +public class Event extends Task { + + protected String start; + protected String end; + + + /** + * Constructs an Event. + * + * @param description Description of task. + * @param start Start time of task. + * @param end End time of task. + */ + public Event(String description, String start, String end) { + super(description); + this.start = start; + this.end = end; + } + + /** + * Returns string representation of Event. + * + * @return String representation. + */ + @Override + public String toString() { + return "[E]" + super.toString() + " (from: " + start + " to: " + end + ")"; + } + + /** + * Returns string representation of Event to save as data. + * + * @return String representation to save as data. + */ + @Override + public String formatData() { + return "E | " + super.formatData() + " | " + this.start + " | " + this.end; + } +} diff --git a/src/main/java/serenity/task/Task.java b/src/main/java/serenity/task/Task.java new file mode 100644 index 0000000000..c89f39a3cd --- /dev/null +++ b/src/main/java/serenity/task/Task.java @@ -0,0 +1,64 @@ +package serenity.task; + +/** + * Represents a general task with a description that can be marked done or undone. + */ +public class Task { + protected String description; + protected boolean isDone; + + /** + * Constructs a Task. + * + * @param description Description of task. + */ + public Task(String description) { + this.description = description; + this.isDone = false; + } + + + /** + * Returns string representation of status of task. + * + * @return String representation of status of task. + */ + public String getStatusIcon() { + return (isDone ? "X" : " "); // mark done task with X + } + + /** + * Marks task as done. + */ + public void markAsDone() { + this.isDone = true; + } + + /** + * Marks task as undone. + */ + public void markAsNotDone() { + this.isDone = false; + } + + /** + * Returns string representation of a Task. + * + * @return String representation. + */ + @Override + public String toString() { + return "[" + this.getStatusIcon() + "] " + this.description; + } + + /** + * Returns string representation of Task to save as data. + * + * @return String representation to save as data. + */ + public String formatData() { + int i = this.isDone ? 1 : 0; + return i + " | " + this.description; + } + +} diff --git a/src/main/java/serenity/task/TaskList.java b/src/main/java/serenity/task/TaskList.java new file mode 100644 index 0000000000..e7e214aca0 --- /dev/null +++ b/src/main/java/serenity/task/TaskList.java @@ -0,0 +1,286 @@ +package serenity.task; + +import java.util.ArrayList; + +import serenity.SerenityException; + +/** + * Represents a list of tasks, where tasks can be added, deleted + * and marked as done or undone. + */ +public class TaskList { + + protected ArrayList tasks; + + /** + * Constructs a TaskList with no existing tasks. + */ + public TaskList() { + this.tasks = new ArrayList<>(); + } + + /** + * Constructs a TaskList with existing tasks. + * + * @param tasks ArrayList of existing tasks. + */ + public TaskList(ArrayList tasks) { + this.tasks = tasks; + } + + /** + * Creates task of specified type with given task description. + * + * @param input Task description specifying type of task. + * @return Task of specified type with given task description. + * @throws SerenityException If type of task is not specified. + */ + public static Task createTask(String input) throws SerenityException { + Task t; + + if (input.startsWith("todo")) { + t = createTodo(input); + } else if (input.startsWith("deadline")) { + t = createDeadline(input); + } else if (input.startsWith("event")) { + t = createEvent(input); + } else { + throw new SerenityException("Error: Type of task is invalid."); + } + + return t; + } + + /** + * Creates Event with given task description. + * + * @param input Task description. + * @return Event with given task description. + * @throws SerenityException If task description is missing. + */ + private static Task createEvent(String input) throws SerenityException { + String[] components = input.split(" "); + if (components.length == 1) { + throw new SerenityException("Error: The description of an event cannot be empty."); + } + String taskDescription = input.split(" ", 2)[1]; + if (!taskDescription.contains("/from") || !taskDescription.contains("/to")) { + throw new SerenityException("Error: Missing event info."); + } + String[] parts = taskDescription.split("/from"); + if (parts.length == 1) { + throw new SerenityException("Error: Missing event info."); + } + String[] timings = parts[1].split("/to"); + if (timings.length == 1 || parts[0].isBlank() || timings[0].isBlank() + || timings[1].isBlank()) { + throw new SerenityException("Error: Missing event info."); + } + return new Event(parts[0].strip(), timings[0].strip(), timings[1].strip()); + } + + /** + * Creates Deadline with given task description. + * + * @param input Task description. + * @return Deadline with given task description. + * @throws SerenityException If task description is missing. + */ + private static Task createDeadline(String input) throws SerenityException { + String[] components = input.split(" "); + if (components.length == 1) { + throw new SerenityException("Error: The description of a deadline cannot be empty."); + } + String taskDescription = input.split(" ", 2)[1]; + if (!taskDescription.contains("/by")) { + throw new SerenityException("Error: Missing deadline info."); + } + String[] parts = taskDescription.split("/by"); + if (parts[0].isBlank() || parts[1].isBlank()) { + throw new SerenityException("Error: Missing deadline info."); + } + return new Deadline(parts[0].strip(), parts[1].strip()); + } + + /** + * Creates Todo with given task description. + * + * @param input Task description. + * @return Deadline with given task description. + * @throws SerenityException If task description is missing. + */ + private static Todo createTodo(String input) throws SerenityException { + String[] components = input.split(" "); + if (components.length == 1) { + throw new SerenityException("Error: The description of a todo cannot be empty."); + } + String taskDescription = input.split(" ", 2)[1]; + return new Todo(taskDescription.strip()); + } + + /** + * Adds task to task list. + * + * @param t Task to be added. + * @return Message to be displayed when task is added. + */ + public String addTask(Task t) { + tasks.add(t); + assert tasks.contains(t) : "Task failed to be added to task list"; + + String numOfTasks = tasks.size() == 1 ? "task" : "tasks"; + return ("Got it. I've added this task:\n" + t + + "\nNow you have " + tasks.size() + " " + numOfTasks + " in the list."); + + } + + /** + * Deletes task from task list. + * + * @param input Command containing index of task to delete. + * @return Message to be displayed when task is deleted. + * @throws SerenityException If task index is not specified. + */ + public String deleteTask(String input) throws SerenityException { + + String[] parts = input.split(" "); + if (parts.length == 1) { + throw new SerenityException("Error: Missing index."); + } + + int index = Integer.parseInt(input.substring(7)) - 1; + if (index < 0 || index > tasks.size() - 1) { + throw new SerenityException("Error: Index is out of bounds."); + } + Task t = tasks.get(index); + tasks.remove(index); + + String numOfTasks = tasks.size() == 1 ? "task" : "tasks"; + return ("Noted. I've removed this task:\n" + t + + "\nNow you have " + tasks.size() + " " + numOfTasks + " in the list."); + } + + /** + * Changes status of task. + * + * @param input Command to containing index of task to change status. + * @return Message to be displayed to user when task status is changed. + * @throws SerenityException If task index is not specified. + */ + public String changeStatus(String input) throws SerenityException { + + String[] parts = input.split(" "); + if (parts.length == 1) { + throw new SerenityException("Error: Missing index."); + } + + String message; + + if (input.startsWith("mark")) { + int index = Integer.parseInt(input.substring(5)) - 1; + if (index < 0 || index > tasks.size() - 1) { + throw new SerenityException("Error: Index is out of bounds."); + } + Task t = tasks.get(index); + t.markAsDone(); + message = "Nice! I've marked this task as done:\n" + t; + } else if (input.startsWith("unmark")) { + int index = Integer.parseInt(input.substring(7)) - 1; + if (index < 0 || index > tasks.size() - 1) { + throw new SerenityException("Error: Index is out of bounds."); + } + Task t = tasks.get(index); + t.markAsNotDone(); + message = "OK, I've marked this task as not done yet:\n" + t; + } else { + throw new SerenityException("Error: Type of task is not specified."); + } + return message; + } + + /** + * Returns String representation of TaskList of matching tasks. + * Matching tasks are identified by the given keyword. + * + * @param input keyword to search tasks for + * @return String representation of TaskList of matching tasks + * @throws SerenityException If keyword is missing. + */ + public String findTask(String input) throws SerenityException { + + String[] parts = input.split(" "); + if (parts.length == 1) { + throw new SerenityException("Error: Missing keyword."); + } + + String keyWord = parts[1].strip(); + ArrayList foundTasks = new ArrayList<>(); + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i).toString().contains(keyWord)) { + foundTasks.add(tasks.get(i)); + } + } + + String message = "Here are the matching tasks in your list:"; + for (int j = 0; j < foundTasks.size(); j++) { + int index = j + 1; + message += "\n" + index + ". " + foundTasks.get(j); + } + return message; + } + + /** + * Updates task at specified index. + * + * @param input User's input. + * @return Message to be displayed to user when task is updated. + * @throws SerenityException If update info or task index is missing. + */ + public String updateTask(String input) throws SerenityException { + String[] parts = input.split("/update"); + if (parts.length == 1) { + throw new SerenityException("Error: Missing info to update task with."); + } + String[] command = parts[0].split(" "); + if (command.length == 1) { + throw new SerenityException("Error: Missing index."); + } + int index = Integer.parseInt(command[1]) - 1; + if (index < 0 || index > tasks.size() - 1) { + throw new SerenityException("Error: Index is out of bounds."); + } + String taskDescription = parts[1].strip(); + Task newTask = TaskList.createTask(taskDescription); + Task t = tasks.set(index, newTask); + + return "Task has been updated! Updated task: \n " + newTask; + } + + /** + * Returns string representation of TaskList. + * + * @return String representation. + */ + @Override + public String toString() { + String message = "Here are the tasks in your list:"; + for (int i = 0; i < tasks.size(); i++) { + int index = i + 1; + message += "\n" + index + ". " + tasks.get(i); + } + return message; + } + + /** + * Returns string representation of TaskList to save as data. + * + * @return String representation to save as data. + */ + public String formatData() { + String data = ""; + for (int i = 0; i < tasks.size(); i++) { + data += tasks.get(i).formatData() + "\n"; + } + return data; + } +} diff --git a/src/main/java/serenity/task/Todo.java b/src/main/java/serenity/task/Todo.java new file mode 100644 index 0000000000..0176ff0b12 --- /dev/null +++ b/src/main/java/serenity/task/Todo.java @@ -0,0 +1,36 @@ +package serenity.task; + +/** + * Represents a type of Task with only a basic description. + */ +public class Todo extends Task { + + /** + * Constructs a Todo. + * + * @param description Description of Task. + */ + public Todo(String description) { + super(description); + } + + /** + * Returns string representation of Todo. + * + * @return String representation. + */ + @Override + public String toString() { + return "[T]" + super.toString(); + } + + /** + * Returns string representation of Todo to save as data. + * + * @return String representation to save as data. + */ + @Override + public String formatData() { + return "T | " + super.formatData(); + } +} diff --git a/src/main/resources/images/Serenity.png b/src/main/resources/images/Serenity.png new file mode 100644 index 0000000000..85df6f1004 Binary files /dev/null and b/src/main/resources/images/Serenity.png differ diff --git a/src/main/resources/images/User.png b/src/main/resources/images/User.png new file mode 100644 index 0000000000..198b730e8d Binary files /dev/null and b/src/main/resources/images/User.png differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..fe672c90ea --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..ae1140e358 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,45 @@ + + + + + + + + + + + +