diff --git a/README.md b/README.md index 8715d4d915..48ba0c52c4 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,4 @@ Prerequisites: JDK 11, update Intellij to the most recent version. | | | | | | | |/ / _ \ | |_| | |_| | < __/ |____/ \__,_|_|\_\___| - ``` + ``` \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..46b9671e45 --- /dev/null +++ b/build.gradle @@ -0,0 +1,60 @@ +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'com.github.johnrengelman.shadow' version '5.1.0' +} + +repositories { + mavenCentral() +} + +dependencies { + String javaFxVersion = '11' + + 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' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.5.0' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClassName = "duke.Launcher" +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +checkstyle { + toolVersion = '8.29' +} + +run{ + standardInput = System.in +} diff --git a/docs/README.md b/docs/README.md index 8077118ebe..640fb2f19b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,126 @@ # User Guide +Duke is a **desktop app for managing tasks, optimized via a Command Line Interface** (CLI). + +A Graphical User Interface (GUI) has also been added which mimics a chatbot you can interact with. + +- [Quick Start](#quick-start) +- [Features](#features) + - [Adding a task](#adding-a-task) + - [Listing all tasks](#listing-all-tasks-list) + - [Sorting all tasks by time](#sorting-all-tasks-by-time-sort) + - [Finding tasks by keyword](#finding-tasks-by-keyword-find) + - [Marking a task as done](#marking-a-task-as-done-mark) + - [Unmarking a task](#unmarking-a-task-unmark) + - [Deleting a task](#deleting-a-task-delete) + - [Exiting the program](#exiting-the-program-bye) +- [Command Summary](#command-summary) +- [References](#references) + +## Quick Start +1. Ensure that you have Java `11` or above installed on your Computer. +2. Download the latest `duke.jar` from [here](https://github.com/Decaxical/ip/releases). +3. Copy the file to the folder you want to use as the *home folder* for Duke. +4. Double-click the file to start the app. +5. Type the command in the command box and press `Enter` key or click the `Send` button to execute it. +6. Refer to the [Features](#features) below for details of each command. ## Features +**Notes about the command format:** +- Parameters to be supplied by the user are in `UPPER_CASE`. + + e.g. `todo TASK_NAME`, `TASK_NAME` is a parameter to be supplied such as `todo homework`. + +--- +### Adding a task: +Duke supports 3 types of tasks: `todo`, `deadline` and `event` +### Adding a todo task: `todo` +Adds a task that has no date and time parameters. + +Format: `todo TASK_NAME` + +Example: `todo return book` + +### Adding a deadline task: `deadline` +Adds a task that needs to be done before a specific date/time. + +Format: `deadline TASK_NAME /by DD/MM/YYYY HH:MM` + +Example: `deadline submit project /by 22/02/2022 22:22` + +### Adding an event task: `event` +Adds a task that starts at a specific date/time. + +Format: `event TASK_NAME /at DD/MM/YYYY HH:MM` + +Example: `event volleyball training /at 24/02/2022 21:00` + +--- +### Listing all tasks: `list` + +Shows a numbered list of all tasks in the task list. + +Format: `list` + +--- +### Sorting all tasks by time: `sort` + +Separates all tasks by type before sorting them by time. Upcoming tasks are displayed first. + +Format: `sort` + +--- +### Finding tasks by keyword: `find` +Find tasks whose name contains the keyword supplied. + +Format: `find KEYWORD` + +Example: `find meeting` + +--- +### Marking a task as done: `mark` +Marks task at given index as done. + +Format: `mark INDEX` + +Example: `mark 4` -### Feature-ABC +--- +### Unmarking a task: `unmark` +Unmarks task at given index. -Description of the feature. +Format: `unmark INDEX` -### Feature-XYZ +Example: `unmark 4` -Description of the feature. +--- +### Deleting a task: `delete` +Deletes task at given index. -## Usage +Format: `delete INDEX` -### `Keyword` - Describe action +Example: `delete 3` -Describe the action and its outcome. +--- +### Exiting the program: `bye` -Example of usage: +Exits the program. -`keyword (optional arguments)` +Format: `bye` -Expected outcome: +## Command Summary -Description of the outcome. + Action | Format, Examples + ---| --- + Todo | `todo TASK_NAME`
e.g., `todo return book` + Deadline| `deadline TASK_NAME /by DD/MM/YYYY HH:MM`
e.g., `deadline submit project /by 22/02/2022 22:22` + Event | `event TASK_NAME /at DD/MM/YYYY HH:MM`
e.g., `event volleyball training /at 24/02/2022 21:00` + List | `list` + Sort | `sort` + Find | `find KEYWORD`
e.g., `find meeting` + Mark | `mark INDEX`
e.g., `mark 4` + Unmark | `unmark INDEX`
e.g., `unmark 4` + Delete | `delete INDEX`
e.g., `delete 3` + Exit | `bye` -``` -expected output -``` +## References +[AddressBook-Level3](https://github.com/j-lum/addressbook-level3) diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..e0338d4af7 Binary files /dev/null and b/docs/Ui.png differ diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000000..18854876c6 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-midnight \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..f3d88b1c2f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..b7c8c5dbf5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..2fe81a7d95 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..62bd9b9cce --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega 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/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..2c9a9745c5 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: duke.Duke + diff --git a/src/main/java/duke/Duke.java b/src/main/java/duke/Duke.java new file mode 100644 index 0000000000..21d0a36625 --- /dev/null +++ b/src/main/java/duke/Duke.java @@ -0,0 +1,89 @@ +package duke; + +import duke.commands.Command; +import duke.commands.ExitCommand; +import duke.exceptions.DukeException; +import duke.parser.Parser; +import duke.storage.Storage; +import duke.tasks.*; +import duke.ui.TextUI; +import duke.ui.gui.DialogBox; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + + + +/** + * Entry point of DUke. + * Initializes the application and initiates contact with user. + */ +public class Duke { + + private static TaskList taskList; + private static Storage storage; + private static TextUI textUI; + private static Parser parser; + + public Duke() { + initialize(); + } + + public static void main(String[] args) { + initialize(); + run(); + } + + private static void initialize() { + textUI = new TextUI(); + storage = new Storage(); + parser = new Parser(); + try { + taskList = Storage.loadTasklist(); + } catch (DukeException e) { + taskList = new TaskList(); + System.out.println(e.getMessage()); + } + Command.defineTaskList(taskList); + } + + private static void run() { + assert taskList != null : "TaskList failed to initialize"; + textUI.printWelcomeMessage(); + do { + String userInputCommand = textUI.getUserCommand(); + try { + Command currCommand = parser.parseCommands(userInputCommand); + textUI.printMessage(currCommand.execute()); + storage.saveTasklist(taskList); + } catch (DukeException e) { + textUI.printMessage(e.getMessage()); + } + } while (ExitCommand.isRunning()); + } + + /** + * You should have your own function to generate a response to user input. + * Replace this stub with your completed method. + */ + public String getResponse(String input) { + assert taskList != null : "TaskList failed to initialize"; + try { + Command currCommand = parser.parseCommands(input); + String outputToUser = currCommand.execute(); + storage.saveTasklist(taskList); + return outputToUser; + } catch (DukeException e) { + return e.getMessage(); + } + } +} diff --git a/src/main/java/duke/Launcher.java b/src/main/java/duke/Launcher.java new file mode 100644 index 0000000000..d4b928a05e --- /dev/null +++ b/src/main/java/duke/Launcher.java @@ -0,0 +1,12 @@ +package duke; + +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(duke.ui.gui.Main.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/duke/commands/AddCommand.java b/src/main/java/duke/commands/AddCommand.java new file mode 100644 index 0000000000..499937115c --- /dev/null +++ b/src/main/java/duke/commands/AddCommand.java @@ -0,0 +1,55 @@ +package duke.commands; + +import duke.tasks.Task; +import duke.tasks.Deadline; +import duke.tasks.Event; +import duke.tasks.Todo; +import java.time.LocalDateTime; + +/** + * Command that adds a Task to the TaskList. + */ + +public class AddCommand extends Command{ + + private String taskType; + private String content; + private LocalDateTime datetime; + + /** + * Constructor for AddCommand. + * + * @param taskType string representing subclass of Task to add. + * @param content description of Task to add. + * @param datetime date and time of task to add. + */ + public AddCommand(String taskType, String content, LocalDateTime datetime) { + this.taskType = taskType; + this.content = content; + this.datetime = datetime; + } + + /** + * Creates respective Task depending on taskType which is then added to the TaskList. + * + * @return Message for completing the command which is displayed to user. + */ + @Override + public String execute() { + Task newTask = null; + switch (taskType.toLowerCase()) { + case "todo": + newTask = new Todo(content); + break; + case "deadline": + newTask = new Deadline(content, datetime); + break; + case "event": + newTask = new Event(content, datetime); + break; + } + taskList.addTask(newTask); + return String.format("This task has been added as requested:\n" + + "%s\n" + "You now have %d item(s) in your list", newTask, taskList.size()); + } +} \ No newline at end of file diff --git a/src/main/java/duke/commands/Command.java b/src/main/java/duke/commands/Command.java new file mode 100644 index 0000000000..ca67429e36 --- /dev/null +++ b/src/main/java/duke/commands/Command.java @@ -0,0 +1,32 @@ +package duke.commands; + +import duke.exceptions.DukeException; +import duke.exceptions.DukeUnsupportedOperationException; +import duke.tasks.TaskList; + +/** + * Parent class of all duke commands. + */ +public class Command { + + static TaskList taskList; + + /** + * Defines the TaskList shared with all commands. + * + * @param taskList TaskList defined and used by all commands. + */ + public static void defineTaskList(TaskList taskList) { + Command.taskList = taskList; + } + + /** + * Carries out the command. + * + * @return Message for completing the command which is displayed to user. + * @throws DukeException If arguments passed to the commands are invalid. + */ + public String execute() throws DukeException{ + throw new DukeUnsupportedOperationException("This method is to be implemented by child classes"); + } +} diff --git a/src/main/java/duke/commands/DeleteCommand.java b/src/main/java/duke/commands/DeleteCommand.java new file mode 100644 index 0000000000..2774e7e92d --- /dev/null +++ b/src/main/java/duke/commands/DeleteCommand.java @@ -0,0 +1,38 @@ +package duke.commands; + +import duke.exceptions.DukeInvalidArgumentException; +import duke.tasks.Task; + +/** + * Command that deletes a Task from TaskList. + */ +public class DeleteCommand extends Command { + + private int index; + + /** + * Constructor for DeleteCommand. + * + * @param index index of Task to delete. + */ + public DeleteCommand(int index) { + this.index = index; + } + + /** + * Deletes Task from TaskList + * + * @return Message for completing the command which is displayed to user. + * @throws DukeInvalidArgumentException If index is not within TaskList. + */ + @Override + public String execute() throws DukeInvalidArgumentException { + if (index > taskList.size() - 1 || index < 0) { + throw new DukeInvalidArgumentException("I am afraid that's an invalid task!" + + "Please check your task number"); + } + Task deletedTask = taskList.deleteTask(index); + return String.format("As you wish. The following task has been removed:\n" + + "%s\n" + "You now have %d item(s) in your list", deletedTask, taskList.size()); + } +} diff --git a/src/main/java/duke/commands/ExitCommand.java b/src/main/java/duke/commands/ExitCommand.java new file mode 100644 index 0000000000..162d2fd68e --- /dev/null +++ b/src/main/java/duke/commands/ExitCommand.java @@ -0,0 +1,29 @@ +package duke.commands; + +/** + * Command that indicates exit of program. + */ +public class ExitCommand extends Command{ + + private static boolean isProgramRunning = true; + + /** + * returns true if the program should be running and false otherwise. + * + * @return whether program should be running. + */ + public static boolean isRunning() { + return isProgramRunning; + } + + /** + * Sets isProgramRunning to false. + * + * @return Message for completing the command which is displayed to user. + */ + @Override + public String execute() { + isProgramRunning = false; + return "Till we meet again"; + } +} diff --git a/src/main/java/duke/commands/FindCommand.java b/src/main/java/duke/commands/FindCommand.java new file mode 100644 index 0000000000..ec36de50c5 --- /dev/null +++ b/src/main/java/duke/commands/FindCommand.java @@ -0,0 +1,39 @@ +package duke.commands; + +import duke.tasks.TaskList; + +/** + * Command that finds a string in Tasks in TaskList. + */ +public class FindCommand extends Command { + + private String searchStr; + + /** + * Constructor for FindCommand. + * + * @param searchStr string to be searched in Task content. + */ + public FindCommand(String searchStr) { + this.searchStr = searchStr.toLowerCase(); + } + + /** + * Iterates through each Task in TaskList and searches for given string. + * + * @return List of Task found according to search. + */ + @Override + public String execute() { + TaskList foundTasks = new TaskList(); + for (int i = 0; i < taskList.size(); i++) { + if (taskList.getTaskContent(i).toLowerCase().contains(searchStr)) { + foundTasks.addTask(taskList.getTask(i)); + } + } + if (foundTasks.size() == 0) { + return "Apologies, no task matches what you are looking for"; + } + return String.format("These are the results of my search:\n%s", foundTasks.toString()); + } +} diff --git a/src/main/java/duke/commands/InvalidCommand.java b/src/main/java/duke/commands/InvalidCommand.java new file mode 100644 index 0000000000..3a5ec1a083 --- /dev/null +++ b/src/main/java/duke/commands/InvalidCommand.java @@ -0,0 +1,17 @@ +package duke.commands; + +/** + * Command for any unknown commands. + */ +public class InvalidCommand extends Command{ + + /** + * Returns String to tell user command is unknown. + * + * @return Message for completing the command which is displayed to user. + */ + @Override + public String execute(){ + return "I am unable to comprehend your request. Please try again"; + } +} diff --git a/src/main/java/duke/commands/ListCommand.java b/src/main/java/duke/commands/ListCommand.java new file mode 100644 index 0000000000..a783c0c5a3 --- /dev/null +++ b/src/main/java/duke/commands/ListCommand.java @@ -0,0 +1,17 @@ +package duke.commands; + +/** + * Command that displays all Task in TaskList. + */ +public class ListCommand extends Command { + + /** + * Checks if TaskList is empty, otherwise displays all Task. + * + * @return All Task in TaskList and message for completing the command which is displayed to user. + */ + @Override + public String execute() { + return taskList.toString(); + } +} diff --git a/src/main/java/duke/commands/MarkCommand.java b/src/main/java/duke/commands/MarkCommand.java new file mode 100644 index 0000000000..851f2e0262 --- /dev/null +++ b/src/main/java/duke/commands/MarkCommand.java @@ -0,0 +1,38 @@ +package duke.commands; + +import duke.exceptions.DukeInvalidArgumentException; +import duke.tasks.Task; + +/** + * Command that marks Task from TaskList as done. + */ +public class MarkCommand extends Command { + + private int index; + + /** + * Constructor for MarkCommand. + * + * @param index index of Task to mark. + */ + public MarkCommand(int index) { + this.index = index; + } + + /** + * Marks Task from TaskList as done. + * + * @return Message for completing the command which is displayed to user. + * @throws DukeInvalidArgumentException If index is not within TaskList. + */ + @Override + public String execute() throws DukeInvalidArgumentException { + if (index > taskList.size() - 1 || index < 0) { + throw new DukeInvalidArgumentException("I am afraid that's an invalid task!" + + " Please check your task number"); + } + Task markedTask = taskList.markTask(index); + return String.format("Duly noted. The following task has been marked as done:\n" + + "%s", markedTask); + } +} diff --git a/src/main/java/duke/commands/SortCommand.java b/src/main/java/duke/commands/SortCommand.java new file mode 100644 index 0000000000..fe3be2e84b --- /dev/null +++ b/src/main/java/duke/commands/SortCommand.java @@ -0,0 +1,50 @@ +package duke.commands; + +import duke.tasks.*; + +import java.util.PriorityQueue; + +/** + * Command that sorts and displays all Task in TaskList. + */ +public class SortCommand extends Command { + private PriorityQueue deadlineList = new PriorityQueue<>(new TaskDateComparator()); + private PriorityQueue eventList = new PriorityQueue<>(new TaskDateComparator()); + private PriorityQueue todoList = new PriorityQueue<>(new TaskDateComparator()); + + @Override + public String execute() { + initializeQueues(); + String sortedDeadlines = "These are your upcoming Deadlines: \n" + priorityQueueStringFormat(deadlineList); + String sortedEvents = "These are your upcoming Events: \n" + priorityQueueStringFormat(eventList); + String sortedTodos = "These are your upcoming Todos: \n" + priorityQueueStringFormat(todoList); + return sortedDeadlines + "\n" + sortedEvents + "\n" + sortedTodos; + } + + private static String priorityQueueStringFormat(PriorityQueue pq) { + if(pq.isEmpty()) { + return "You have no such tasks\n"; + } + + int index = 1; + StringBuilder numberedPriorityQueue = new StringBuilder(); + while (!pq.isEmpty()) { + numberedPriorityQueue.append(String.format("%d: %s\n", index, pq.poll().toString())); + index++; + } + return numberedPriorityQueue.toString(); + } + + private void initializeQueues() { + for (int i = 0; i < taskList.size(); i++) { + Task currTask = taskList.getTask(i); + if (currTask instanceof Deadline) { + deadlineList.add((Deadline) currTask); + } else if (currTask instanceof Event) { + eventList.add((Event) currTask); + } else { + todoList.add((Todo) currTask); + } + } + } +} diff --git a/src/main/java/duke/commands/UnmarkCommand.java b/src/main/java/duke/commands/UnmarkCommand.java new file mode 100644 index 0000000000..ad2cf99bd5 --- /dev/null +++ b/src/main/java/duke/commands/UnmarkCommand.java @@ -0,0 +1,38 @@ +package duke.commands; + +import duke.exceptions.DukeInvalidArgumentException; +import duke.tasks.Task; + +/** + * Command that unmarks a Task from TaskList + */ +public class UnmarkCommand extends Command { + + private int index; + + /** + * Constructor for UnmarkCommand. + * + * @param index index of Task to unmark. + */ + public UnmarkCommand(int index) { + this.index = index; + } + + /** + * Unmarks Task from TaskList. + * + * @return Message for completing the command which is displayed to user. + * @throws DukeInvalidArgumentException If index is not within TaskList. + */ + @Override + public String execute() throws DukeInvalidArgumentException { + if (index > taskList.size() - 1 || index < 0) { + throw new DukeInvalidArgumentException("I am afraid that's an invalid task!" + + " Please check your task number"); + } + Task unmarkedTask = taskList.unmarkTask(index); + return String.format("Very well. The following task has been marked as not done:" + + "%s", unmarkedTask); + } +} diff --git a/src/main/java/duke/exceptions/DukeException.java b/src/main/java/duke/exceptions/DukeException.java new file mode 100644 index 0000000000..e9ec37397e --- /dev/null +++ b/src/main/java/duke/exceptions/DukeException.java @@ -0,0 +1,16 @@ +package duke.exceptions; + +/** + * Parent class of all duke exceptions. + */ +public class DukeException extends Exception { + + /** + * Constructor for DukeException. + * + * @param message Contains information of the exception. + */ + public DukeException(String message) { + super(message); + } +} diff --git a/src/main/java/duke/exceptions/DukeInvalidArgumentException.java b/src/main/java/duke/exceptions/DukeInvalidArgumentException.java new file mode 100644 index 0000000000..7ca7ff703a --- /dev/null +++ b/src/main/java/duke/exceptions/DukeInvalidArgumentException.java @@ -0,0 +1,16 @@ +package duke.exceptions; + +/** + * DukeException for invalid arguments inputted. + */ +public class DukeInvalidArgumentException extends DukeException{ + + /** + * Constructor for DukeInvalidArgumentException. + * + * @param message Contains information of the exception. + */ + public DukeInvalidArgumentException(String message) { + super(message); + } +} diff --git a/src/main/java/duke/exceptions/DukeUnsupportedOperationException.java b/src/main/java/duke/exceptions/DukeUnsupportedOperationException.java new file mode 100644 index 0000000000..336b1b3c59 --- /dev/null +++ b/src/main/java/duke/exceptions/DukeUnsupportedOperationException.java @@ -0,0 +1,16 @@ +package duke.exceptions; + +/** + * DukeException for operations that cannot be carried out. + */ +public class DukeUnsupportedOperationException extends DukeException{ + + /** + * Constructor for DukeUnsupportedOperationException. + * + * @param message Contains information of the exception. + */ + public DukeUnsupportedOperationException(String message) { + super(message); + } +} diff --git a/src/main/java/duke/parser/Parser.java b/src/main/java/duke/parser/Parser.java new file mode 100644 index 0000000000..4ba5b74ef6 --- /dev/null +++ b/src/main/java/duke/parser/Parser.java @@ -0,0 +1,112 @@ +package duke.parser; + +import duke.commands.*; +import duke.exceptions.DukeInvalidArgumentException; +import duke.tasks.Deadline; +import duke.tasks.Event; +import duke.tasks.Task; +import duke.tasks.Todo; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Parses user inputs for commands. + */ +public class Parser { + + /** + * Parses user input for specified commands. + * + * @param userInput raw input from user. + * @return Command parsed from user input. + * @throws DukeInvalidArgumentException If user input invalid arguments. + */ + public Command parseCommands(String userInput) throws DukeInvalidArgumentException { + String[] parsedUserInput = userInput.split(" ", 2); + String commandType = parsedUserInput[0].toLowerCase(); + switch (commandType) { + case "bye": + return new ExitCommand(); + case "list": + return new ListCommand(); + case "sort": + return new SortCommand(); + case "mark": + return new MarkCommand(parseIndex(parsedUserInput[1])); + case "unmark": + return new UnmarkCommand(parseIndex(parsedUserInput[1])); + case "delete": + return new DeleteCommand(parseIndex(parsedUserInput[1])); + case "find": + return new FindCommand(parsedUserInput[1].trim()); + case "todo": + String[] todoParsedArguments = parseArguments(parsedUserInput); + String content = todoParsedArguments[0]; + return new AddCommand(commandType, content, null); + case "deadline": + case "event": + String[] parsedArguments = parseArguments(parsedUserInput); + content = parsedArguments[0]; + LocalDateTime datetime = parseDateTime(parsedArguments[1]); + return new AddCommand(commandType, content, datetime); + default: + return new InvalidCommand(); + } + } + + private int parseIndex(String strIndex) throws DukeInvalidArgumentException { + try { + return Integer.parseInt(strIndex.trim()) - 1; + } catch (NumberFormatException e) { + throw new DukeInvalidArgumentException("Please input an integer as index"); + } + } + + private String[] parseArguments(String[] arguments) throws DukeInvalidArgumentException { + if (arguments.length < 2) { + throw new DukeInvalidArgumentException("There appears to be insufficient arguments"); + } + return arguments[1].split(" /([Aa][Tt]|[Bb][Yy]) ", 2); + } + + private LocalDateTime parseDateTime(String datetime) throws DukeInvalidArgumentException { + try{ + DateTimeFormatter datetimePattern = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"); + return LocalDateTime.parse(datetime, datetimePattern); + } catch (DateTimeParseException e) { + throw new DukeInvalidArgumentException("Pardon me. Please input date in the format DD/MM/YYYY HH:MM"); + } + } + + /** + * Parses date and time from data in savefile. + * + * @param datetime date and time to be parsed. + * @return LocalDateTime object based on given date time. + */ + public LocalDateTime parseSaveDateTime(String datetime) { + DateTimeFormatter datetimePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + return LocalDateTime.parse(datetime, datetimePattern); + } + + public Task parseSavedTask(String currSaveLine) { + Task currentTask = null; + switch (currSaveLine.charAt(0)) { + case 'T' : { + currentTask = Todo.createFromData(currSaveLine); + break; + } + case 'E' : { + currentTask = Event.createFromData(currSaveLine); + break; + } + case 'D' : { + currentTask = Deadline.createFromData(currSaveLine); + break; + } + } + return currentTask; + } +} \ No newline at end of file diff --git a/src/main/java/duke/storage/Storage.java b/src/main/java/duke/storage/Storage.java new file mode 100644 index 0000000000..e4ca4da538 --- /dev/null +++ b/src/main/java/duke/storage/Storage.java @@ -0,0 +1,78 @@ +package duke.storage; + +import duke.exceptions.DukeException; +import duke.parser.Parser; +import duke.tasks.*; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.Path; + + +/** + * This class saves and loads data from save file. + */ +public class Storage { + + private static final Path DATA_PATH = Paths.get("data", "duke.txt"); + + /** + * Writes TaskList data to save file. + * + * @param taskList TaskList to write to save file. + * @throws DukeException If there is an error writing to save file. + */ + public void saveTasklist(TaskList taskList) throws DukeException { + assert taskList != null : "Null tasklist to save"; + initialiseSaveFile(); + String dataToWrite = taskList.toSaveData(); + try { + FileWriter saveFileWriter = new FileWriter(DATA_PATH.toString(), false); + saveFileWriter.write(dataToWrite); + saveFileWriter.close(); + } catch (IOException e) { + throw new DukeException("Unable to write into save file"); + } + } + + /** + * Loads TaskList data from save file. + * + * @return new TaskList recreated from data. + * @throws DukeException If data cannot be loaded from save file. + */ + public static TaskList loadTasklist() throws DukeException{ + initialiseSaveFile(); + String strCurrentLine; + Parser parser = new Parser(); + TaskList taskList = new TaskList(); + try { + BufferedReader saveFilereader = new BufferedReader(new FileReader(DATA_PATH.toString())); + while ((strCurrentLine = saveFilereader.readLine()) != null) { + Task currentTask = parser.parseSavedTask(strCurrentLine); + taskList.addTask(currentTask); + } + saveFilereader.close(); + } catch (IOException e) { + throw new DukeException("Unable to load save file"); + } + return taskList; + } + + private static void initialiseSaveFile() throws DukeException { + try { + if (Files.notExists(DATA_PATH)) { + if(Files.notExists(DATA_PATH.getParent())) { + Files.createDirectory(DATA_PATH.getParent()); + } + Files.createFile(DATA_PATH); + } + } catch (IOException e) { + throw new DukeException("Unable to create a save file"); + } + } +} + diff --git a/src/main/java/duke/tasks/Deadline.java b/src/main/java/duke/tasks/Deadline.java new file mode 100644 index 0000000000..7f746887b1 --- /dev/null +++ b/src/main/java/duke/tasks/Deadline.java @@ -0,0 +1,64 @@ +package duke.tasks; + +import duke.parser.Parser; + +import java.time.LocalDateTime; + +/** + * Deadlines are tasks due on a certain date. + */ +public class Deadline extends Task{ + + private LocalDateTime date; + + /** + * Constructor for Deadline. + * + * @param content Description of Deadline. + * @param date date and time Deadline is due. + */ + public Deadline(String content, LocalDateTime date) { + super(content); + this.date = date; + } + + @Override + public LocalDateTime getDate() { + return this.date; + } + + /** + * Adds Deadline indicator at the front and the date time to the back of Task string. + * + * @return Formatted string representation of Deadline. + */ + @Override + public String toString() { + return String.format("[D]%s (by: %s)", super.toString(), this.date.toString().replace("T", " ")); + } + + /** + * Adds Deadline indicator and date time to save data. + * + * @return Data to write into save file. + */ + public String toSaveData() { + return String.format("D|%s|%s\n", super.toSaveData(), this.date.toString().replace("T", " ")); + } + + /** + * Recreate Deadline from save data. + * + * @param savedData Deadline Data in save file. + * @return Deadline that was saved. + */ + public static Deadline createFromData(String savedData) { + Parser parser = new Parser(); + String[] parsedSavedData = savedData.split("\\|"); + Deadline newDeadline = new Deadline(parsedSavedData[2], parser.parseSaveDateTime(parsedSavedData[3])); + if (parsedSavedData[1].equals("1")) { + newDeadline.markAsDone(); + } + return newDeadline; + } +} diff --git a/src/main/java/duke/tasks/Event.java b/src/main/java/duke/tasks/Event.java new file mode 100644 index 0000000000..ff6afcf2c2 --- /dev/null +++ b/src/main/java/duke/tasks/Event.java @@ -0,0 +1,64 @@ +package duke.tasks; + +import duke.parser.Parser; + +import java.time.LocalDateTime; + +/** + * Events are tasks happening at a point in time. + */ +public class Event extends Task { + + private LocalDateTime date; + + /** + * Constructor for Event. + * + * @param content Description of Event. + * @param date date and time of Event. + */ + public Event(String content, LocalDateTime date) { + super(content); + this.date = date; + } + + @Override + public LocalDateTime getDate() { + return this.date; + } + + /** + * Adds Event indicator at the front and the date time to the back of Task string. + * + * @return Formatted string representation of Event. + */ + @Override + public String toString() { + return String.format("[E]%s (at: %s)", super.toString(), this.date.toString().replace("T", " ")); + } + + /** + * Adds Event indicator and date time to save data. + * + * @return Data to write into save file. + */ + public String toSaveData() { + return String.format("E|%s|%s\n", super.toSaveData(), this.date.toString().replace("T", " ")); + } + + /** + * Recreate Event from save data. + * + * @param savedData Event Data in save file. + * @return Event that was saved. + */ + public static Event createFromData(String savedData) { + Parser parser = new Parser(); + String[] parsedSavedData = savedData.split("\\|"); + Event newEvent = new Event(parsedSavedData[2], parser.parseSaveDateTime(parsedSavedData[3])); + if (parsedSavedData[1].equals("1")) { + newEvent.markAsDone(); + } + return newEvent; + } +} diff --git a/src/main/java/duke/tasks/Task.java b/src/main/java/duke/tasks/Task.java new file mode 100644 index 0000000000..665a5ae982 --- /dev/null +++ b/src/main/java/duke/tasks/Task.java @@ -0,0 +1,73 @@ +package duke.tasks; + +import duke.exceptions.DukeUnsupportedOperationException; + +import java.time.LocalDateTime; + +/** + * Parent Class of all duke tasks. + */ +public class Task { + + private String content; + private boolean isDone = false; + + /** + * Constructor for Task. + * + * @param content Description of Task. + */ + public Task(String content) { + this.content = content; + } + + /** + * Marks task as done. + */ + public void markAsDone() { + this.isDone = true; + } + + /** + * Unmarks task as not done. + */ + public void markAsNotDone() { + this.isDone = false; + } + + /** + * Returns content of the Task. + * + * @return content of the Task. + */ + public String getContent() { + return content; + } + + public LocalDateTime getDate() { + return LocalDateTime.MIN; + } + + /** + * Formats Task indicating done tasks with an X followed by content of task. + * + * @return Formatted string representation of task. + */ + @Override + public String toString() { + String markedDoneIndicator = this.isDone ? "X" : " "; + return String.format("[%s] %s", markedDoneIndicator, this.content); + } + + /** + * Formats Task to write into save file. + * Marked done tasks are denoted by 1 and 0 otherwise. + * + * @return Data to write into save file. + */ + public String toSaveData() { + String markedDoneIndicator = this.isDone ? "1" : "0"; + return String.format("%s|%s", markedDoneIndicator, this.content); + } + +} diff --git a/src/main/java/duke/tasks/TaskDateComparator.java b/src/main/java/duke/tasks/TaskDateComparator.java new file mode 100644 index 0000000000..e45a19ff49 --- /dev/null +++ b/src/main/java/duke/tasks/TaskDateComparator.java @@ -0,0 +1,22 @@ +package duke.tasks; + +import duke.exceptions.DukeUnsupportedOperationException; + +import java.time.LocalDateTime; +import java.util.Comparator; + +public class TaskDateComparator implements Comparator { + + public int compare(Task task1, Task task2) { + LocalDateTime task1Date = task1.getDate(); + LocalDateTime task2Date = task2.getDate(); + + if (task1Date.isEqual(task2Date)) { + return 0; + } else if (task1Date.isBefore(task2Date)) { + return -1; + } else { + return 1; + } + } +} diff --git a/src/main/java/duke/tasks/TaskList.java b/src/main/java/duke/tasks/TaskList.java new file mode 100644 index 0000000000..95cdf5f440 --- /dev/null +++ b/src/main/java/duke/tasks/TaskList.java @@ -0,0 +1,126 @@ +package duke.tasks; + +import java.util.ArrayList; + +/** + * ArrayList of Task objects. + */ +public class TaskList { + + private final ArrayList taskList; + + /** + * Constructor for TaskList. + */ + public TaskList() { + this.taskList = new ArrayList<>(); + } + + /** + * Get task at given index. + * + * @param index Index of task to get. + * @return Task at given index. + */ + public Task getTask(int index) { + assert index < taskList.size() && index >= 0 : "TaskList out of bounds"; + return taskList.get(index); + } + + /** + * Get content of task at given index. + * + * @param index Index of task to get content. + * @return Content of task at given index. + */ + public String getTaskContent(int index) { + assert index < taskList.size() && index >= 0 : "TaskList out of bounds"; + return taskList.get(index).getContent(); + } + + /** + * Adds Task to TaskList. + * + * @param taskToAdd Task to add to TaskList. + */ + public void addTask(Task taskToAdd) { + taskList.add(taskToAdd); + } + + /** + * Deletes Task from Tasklist specified by given index. + * + * @param indexTaskToDelete Index of Task to delete. + * @return Deleted task. + */ + public Task deleteTask(int indexTaskToDelete) { + assert indexTaskToDelete < taskList.size() && indexTaskToDelete >= 0 : "TaskList out of bounds"; + return taskList.remove(indexTaskToDelete); + } + + /** + * Marks Task from TaskList specified by given index. + * + * @param indexTaskToMark Index of Task to mark. + * @return Marked task + */ + public Task markTask(int indexTaskToMark) { + assert indexTaskToMark < taskList.size() && indexTaskToMark >= 0 : "TaskList out of bounds"; + Task taskToMark = taskList.get(indexTaskToMark); + taskToMark.markAsDone(); + return taskToMark; + } + + /** + * Unmarks Task from TaskList specified by given index. + * + * @param indexTaskToUnmark Index of Task to Unmark. + * @return Unmarked task + */ + public Task unmarkTask(int indexTaskToUnmark) { + assert indexTaskToUnmark < taskList.size() && indexTaskToUnmark >= 0 : "TaskList out of bounds"; + Task taskToUnmark = taskList.get(indexTaskToUnmark); + taskToUnmark.markAsNotDone(); + return taskToUnmark; + } + + /** + * Returns number of tasks in TaskList. + * + * @return size of TaskList. + */ + public int size() { + return taskList.size(); + } + + /** + * Formats each Task in TaskList to write into save file. + * + * @return Data to write into save file. + */ + public String toSaveData() { + StringBuilder dataToWrite = new StringBuilder(); + for (Task task : taskList) { + dataToWrite.append(task.toSaveData()); + } + return dataToWrite.toString(); + } + + /** + * Returns string indicating empty TaskList if empty. + * Else return numbered list of task. + * + * @return String representation of TaskList. + */ + @Override + public String toString() { + if (taskList.size() == 0) { + return "Your current task list is empty"; + } + StringBuilder numberedTaskList = new StringBuilder(); + for (int i = 0; i < taskList.size(); i++) { + numberedTaskList.append(String.format("%d. %s \n", i + 1, taskList.get(i))); + } + return numberedTaskList.toString().trim(); + } +} diff --git a/src/main/java/duke/tasks/Todo.java b/src/main/java/duke/tasks/Todo.java new file mode 100644 index 0000000000..ca8148d67a --- /dev/null +++ b/src/main/java/duke/tasks/Todo.java @@ -0,0 +1,57 @@ +package duke.tasks; + +import java.time.LocalDateTime; + +/** + * Todos are tasks that have no dates involved. + */ +public class Todo extends Task{ + + /** + * Constructor for Todo. + * + * @param content Description of Todo. + */ + public Todo(String content) { + super(content); + } + + @Override + public LocalDateTime getDate() { + return LocalDateTime.MIN; + } + + /** + * Adds Todo indicator to the front of Task string. + * + * @return Formatted string representation of Event. + */ + @Override + public String toString() { + return String.format("[T]%s", super.toString()); + } + + /** + * Adds Todo indicator and date time to save data. + * + * @return Data to write into save file. + */ + public String toSaveData() { + return String.format("T|%s\n",super.toSaveData()); + } + + /** + * Recreate Todo from save data. + * + * @param savedData Todo Data in save file. + * @return Todo that was saved. + */ + public static Todo createFromData(String savedData) { + String[] parsedSavedData = savedData.split("\\|"); + Todo newTodo = new Todo(parsedSavedData[2]); + if (parsedSavedData[1].equals("1")) { + newTodo.markAsDone(); + } + return newTodo; + } +} diff --git a/src/main/java/duke/ui/TextUI.java b/src/main/java/duke/ui/TextUI.java new file mode 100644 index 0000000000..8eea1aab43 --- /dev/null +++ b/src/main/java/duke/ui/TextUI.java @@ -0,0 +1,86 @@ +package duke.ui; + +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Scanner; + +/** + * Class that handles input and output with user. + */ +public class TextUI { + + private final Scanner in; + private final PrintStream out; + + /** + * Constructor for TextUI. + * + * @param in To receive user input by scanning. + * @param out output to print to. + */ + public TextUI(InputStream in, PrintStream out){ + this.in = new Scanner(in); + this.out = out; + } + + /** + * Constructor for TextUI with default System in and out. + */ + public TextUI() { + this(System.in, System.out); + } + + /** + * Gets next line entered by user. + * Ignores empty lines. + * + * @return raw user input + */ + public String getUserCommand() { + String currInput = in.nextLine().trim(); + while (shouldIgnore(currInput)) { + currInput = in.nextLine().trim(); + } + return currInput; + } + + private boolean shouldIgnore(String rawInputLine) { + return rawInputLine.trim().isEmpty(); + } + + /** + * Formats the message with borders and prints it. + * + * @param message Message to be printed. + */ + public void printMessage(String message) { + assert !message.isEmpty() : "No message to print"; + String textBorder = "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"; + String textToPrint = "\n" + textBorder + message + "\n" + textBorder + "\n"; + out.print(textToPrint); + } + + /** + * Generates and print the welcome message upon the start of the application. + */ + public void printWelcomeMessage() { + String logo = " ____ _ \n" + + "| _ \\ _ _| | _____ \n" + + "| | | | | | | |/ / _ \\\n" + + "| |_| | |_| | < __/\n" + + "|____/ \\__,_|_|\\_\\___|\n"; + out.print("\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + out.println("\nGreetings from\n" + logo); + out.println("How may I assist you?"); + out.print("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n\n"); + } + + /** + * Generates the welcome message upon the start of the application. + * + * @return String of welcome message. + */ + public String getWelcomeMessage() { + return "Greetings from Duke. \nHow may I assist you today?"; + } +} diff --git a/src/main/java/duke/ui/gui/DialogBox.java b/src/main/java/duke/ui/gui/DialogBox.java new file mode 100644 index 0000000000..47aebe3a88 --- /dev/null +++ b/src/main/java/duke/ui/gui/DialogBox.java @@ -0,0 +1,64 @@ +package duke.ui.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; +import javafx.scene.layout.Region; + +/** + * This control represents a dialog box consisting of an Image to represent the speaker's face and a String + * 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); + } + + public static DialogBox getUserDialog(String text, Image img) { + DialogBox newdb = new DialogBox(text, img); + newdb.setMinHeight(Region.USE_PREF_SIZE); + return newdb; + } + + public static DialogBox getDukeDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + db.setMinHeight(Region.USE_PREF_SIZE); + return db; + } + + /** + * Flips the dialog box such that the Image 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); + } +} \ No newline at end of file diff --git a/src/main/java/duke/ui/gui/Main.java b/src/main/java/duke/ui/gui/Main.java new file mode 100644 index 0000000000..6560506836 --- /dev/null +++ b/src/main/java/duke/ui/gui/Main.java @@ -0,0 +1,35 @@ +package duke.ui.gui; + +import java.io.IOException; + +import duke.Duke; +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +/** + * A GUI for Duke using FXML. + */ +public class Main extends Application { + + private Duke duke = new Duke(); + + @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("Duke"); + stage.getIcons().add(new Image("/images/DaDuke.png")); + fxmlLoader.getController().setDuke(duke); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/duke/ui/gui/MainWindow.java b/src/main/java/duke/ui/gui/MainWindow.java new file mode 100644 index 0000000000..5ffcfb6258 --- /dev/null +++ b/src/main/java/duke/ui/gui/MainWindow.java @@ -0,0 +1,68 @@ +package duke.ui.gui; + +import duke.Duke; +import duke.ui.TextUI; +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 java.util.concurrent.TimeUnit; + +/** + * 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 Duke duke; + + private Image userImage = new Image(this.getClass().getResourceAsStream("/images/DaUser.png")); + private Image dukeImage = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png")); + + @FXML + public void initialize() { + TextUI tempTextUI = new TextUI(); + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + dialogContainer.getChildren().addAll( + DialogBox.getDukeDialog(tempTextUI.getWelcomeMessage(), dukeImage) + ); + } + + public void setDuke(Duke d) { + duke = d; + } + + /** + * 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() { + assert duke != null : "duke is null"; + String input = userInput.getText(); + if(input.isBlank()) { + return; + } + String response = duke.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + if (input.equals("bye")) { + System.exit(0); + } + } +} + diff --git a/src/main/resources/images/DaDuke.png b/src/main/resources/images/DaDuke.png new file mode 100644 index 0000000000..e27af8f651 Binary files /dev/null and b/src/main/resources/images/DaDuke.png differ diff --git a/src/main/resources/images/DaUser.png b/src/main/resources/images/DaUser.png new file mode 100644 index 0000000000..b76dca863e Binary files /dev/null and b/src/main/resources/images/DaUser.png differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..a504886ee9 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..5538336da4 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + +