diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..4fe1c7f03c --- /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 { + 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' + 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' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClassName = "Launcher" +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +checkstyle { + toolVersion = '8.29' +} + +run{ + standardInput = System.in +} diff --git a/data/duke.txt b/data/duke.txt new file mode 100644 index 0000000000..f0f01f0aa2 --- /dev/null +++ b/data/duke.txt @@ -0,0 +1,4 @@ +[T][ ] read book +[E][X] project meeting at: 2021-10-15 19:00 +[E][X] make pasta at: 2021-08-16 13:00 +[D][ ] submit ip by: 2021-08-16 23:59 diff --git a/docs/README.md b/docs/README.md index 8077118ebe..1efbec5b65 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,238 @@ # User Guide +Duke is a **desktop app** for _managing tasks_ and having the benefits of a Graphical User Interface (GUI). +You can add 3 types of tasks (Todo, Deadline, Event) and use a whole range of features to manage those tasks. + +-------------------------------------------------------------------------------------------------------------------- + +## Quick start + +1. Ensure you have Java `11` or above installed in your Computer. + +1. Download the latest `duke.jar` from [here](https://github.com/loose-bus-change/ip/releases). + +1. Copy the file to the folder you want to use as the _home folder_ for your Duke. + +1. Double-click the file to start the app. + +1. Type the command in the popup and press **Send** to execute it. + +1. Refer to the [Features](#features) below for details of each command. + +-------------------------------------------------------------------------------------------------------------------- + ## Features -### Feature-ABC +### Add Todo Task + +Have a task that does not have a deadline ? Use this feature to add a **Todo** task. + +#### `todo ` - add a todo task + +Example of usage: + +`todo read book` + +Here is the output you should see from Duke: + +``` +Got it! I've added this task: +[T][ ] read book +Now you have 1 task(s) in the list. +``` + +### Add Deadline Task + +Have a task that is due by a particular date and time ? Use this feature to add a **Deadline** task. + +#### `deadline /by ` - add a deadline task + +Example of usage: + +`deadline return book /by 2/12/2021 1800` + +Here is the output you should see from Duke: + +``` +Got it! I've added this task: +[D][ ] return book by: 2021-12-02 18:00 +Now you have 2 task(s) in the list. +``` + +### Add Event Task + +Have an event that happens at a particular time ? Use this feature to add an **Event** task. + +#### `event /at ` - add an event task + +Example of usage: + +`event project meeting /at 15/10/2021 1900` + +Here is the output you should see from Duke: + +``` +Got it! I've added this task: +[E][ ] project meeting at: 2021-10-15 +Now you have 3 task(s) in the list. +``` + +### Mark as done + +Finished with a task ? Yay! You can use this feature to indicate that you have finished the task. + +#### `done ` - to indicate you are done with a task + +Example of usage: + +`done 2` + +Here is the output you should see from Duke: + +``` +Nice! I've marked this task as done: +[X] return book +``` + +### View Task List + +Use this feature to view the current list of tasks that you have. -Description of the feature. +#### `list` - to view list of tasks -### Feature-XYZ +Example of usage: -Description of the feature. +`list` -## Usage +Here is the output you should see from Duke: -### `Keyword` - Describe action +``` +Here are the tasks in your list: +1. [T][ ] read book +2. [D][X] return book by: 2021-12-02 18:00 +3. [E][ ] project meeting at: 2021-10-15 +``` + +### Search by keyword + +Struggling to find a particular task ? Use this feature to search for a task by using a keyword + +#### `find ` - search for a task using keyword + +Example of usage: + +`find meeting` + +Here is the output you should see from Duke: + +``` +[E][ ] project meeting at: 2021-10-15 +``` + +### Delete a task -Describe the action and its outcome. +Use this feature to remove a task from the list. + +#### `delete ` - delete a particular task + +Example of usage: + +`delete 2` + +Here is the output you should see from Duke: + +``` +Noted. I've removed this task: +[D][X] return book by: 2021-12-02 18:00 +Now you have 2 task(s) in your list +``` + +### Get schedule + +Want to get a schedule of your tasks. Use this feature to get tasks in three different ways. + +Here is a sample task list to demonstrate this feature: +``` +[T][ ] read book +[E][ ] project meeting at: 2021-10-15 19:00 +[E][ ] make pasta at: 2021-08-16 13:00 +[D][ ] submit ip by: 2021-08-16 23:59 +[E][ ] attend party at: 2022-03-07 15:00 +``` -Example of usage: +#### `schedule on ` - get the list of tasks _on_ a particular date -`keyword (optional arguments)` +Example of usage: -Expected outcome: +`schedule on 16/08/2021` -Description of the outcome. +Here is the output you should see from Duke: ``` -expected output +Here are the list of tasks scheduled on 16/08/2021: +1. [E][ ] make pasta at: 2021-08-16 13:00 +2. [D][ ] submit ip by: 2021-08-16 23:59 ``` +#### `schedule until ` - get the list of tasks _until_ a particular date + +Example of usage: + +`schedule until 16/12/2021` + +Here is the output you should see from Duke: + +``` +Here are the list of tasks scheduled until 16/12/2021: +1. [E][ ] project meeting at: 2021-10-15 19:00 +2. [E][ ] make pasta at: 2021-08-16 13:00 +3. [D][ ] submit ip by: 2021-08-16 23:59 +``` + +#### `schedule after ` - get the list of tasks _after_ a particular date + +Example of usage: + +`schedule after 16/08/2021` + +Here is the output you should see from Duke: + +``` +Here are the list of tasks scheduled after 16/08/2021: +1. [E][ ] project meeting at: 2021-10-15 19:00 +2. [E][ ] attend party at: 2022-03-07 15:00 +``` + +**Important Note:** you can also enter your dates in the following formats: +* d/m/yyyy +* dd/m/yyyy +* d/mm/yyyy + +### Help + +If you ever forget the format for the commands or want to refresh your memory on what commands are available, use this feature to have Duke remind you! + +#### `help` - get a list of Duke command formats + +### Exit the application + +If you are done, use this to exit the application! + +#### `bye` - to exit the application. + +-------------------------------------------------------------------------------------------------------------------- + +## Command summary + +Action | Format, Examples +--------|------------------ +**Add Todo Task** | `todo `
e.g., `todo read book` +**Add Deadline Task** | `deadline /by `
e.g., `deadline return book /by 2/12/2021 1800` +**Add Event Task** | `event /at `
e.g., `event project meeting /at 16/08/2021 1900` +**Find Task** | `find `
e.g., `find book` +**Delete Task** | `delete `
e.g., `delete 2` +**Schedule On** | `schedule on `
e.g., `schedule on 16/08/2021` +**Schedule Until** | `schedule until `
e.g., `schedule until 16/08/2021` +**Schedule After** | `schedule after `
e.g., `schedule after 16/08/2021` +**Show Task List** | `list` +**Help** | `help` +**Bye** | `bye` diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..bd009f31a0 Binary files /dev/null and b/docs/Ui.png differ 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/DialogBox.java b/src/main/java/DialogBox.java new file mode 100644 index 0000000000..60636ac988 --- /dev/null +++ b/src/main/java/DialogBox.java @@ -0,0 +1,59 @@ +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +/** + * An example of a custom control using FXML. + * This control represents a dialog box consisting of an ImageView to represent the speaker's face and a label + * containing text from the speaker. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + private DialogBox(String text, Image img) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(img); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + } + + public static DialogBox getUserDialog(String text, Image img) { + return new DialogBox(text, img); + } + + public static DialogBox getDukeDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + return db; + } +} \ No newline at end of file diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java index 5d313334cc..91031f3a1c 100644 --- a/src/main/java/Duke.java +++ b/src/main/java/Duke.java @@ -1,10 +1,88 @@ +import duke.Storage; +import duke.TaskList; +import duke.Ui; +import duke.Parser; +import duke.DukeException; + +import java.io.File; + +/** The main class of the application */ public class Duke { + private Storage storage; + private TaskList tasks; + private File file; + private Parser parser; + private Ui ui; + + public Duke() { + + } + + /** + * Constructor the Duke class + * + * @param filePath The file path of the file that holds the contents + */ + public Duke(String filePath) { + this.storage = new Storage(filePath); + try { + this.tasks = new TaskList(filePath); + } catch (DukeException e) { + ui.showLoadingError(); + this.tasks = new TaskList(filePath); + } + ui = new Ui(this.tasks, this.storage); + parser = new Parser(); + this.file = new File(filePath); + this.tasks.readFromFile(); + } + + /** + * Method that runs the Duke by asking for user input + * + * @return void + */ + public void run() { + System.out.println("Hello! This is Duke, your very own chat bot."); + System.out.println("What can I help you with ?"); + while (true) { + String userCommand = ui.input(); + if (!userCommand.equals("bye")) { + try { + parser.parse(userCommand, this.tasks, this.storage); + } catch (DukeException e) { + System.out.println(e.getErrorMessage()); + } + } else { + System.out.println("It's sad to see you go :("); + System.out.println("Goodbye, hope to you another day!"); + break; + } + } + } + + /** main method */ public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); + new Duke("./data/duke.txt").run(); + } + + /** + * Returns the appropriate response according to user input + * + * @param input The input entered by the user + * @return The output matching the input request of the user + */ + public String getResponse(String input) { + while (!input.equals("bye")) { + try { + this.tasks = new TaskList("./data/duke.txt"); + } catch (DukeException e) { + ui.showLoadingError(); + this.tasks = new TaskList("./data/duke.txt"); + } + this.tasks.readFromFile(); + return parser.parse(input, this.tasks, this.storage); + } + return "It's sad to see you go :("; } } diff --git a/src/main/java/Launcher.java b/src/main/java/Launcher.java new file mode 100644 index 0000000000..11dbf00c62 --- /dev/null +++ b/src/main/java/Launcher.java @@ -0,0 +1,10 @@ +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); + } +} \ No newline at end of file diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..9f37e4e0aa --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: Duke + diff --git a/src/main/java/Main.java b/src/main/java/Main.java new file mode 100644 index 0000000000..5bbcf5d55c --- /dev/null +++ b/src/main/java/Main.java @@ -0,0 +1,29 @@ +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; + +/** + * 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); + fxmlLoader.getController().setDuke(duke); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/MainWindow.java b/src/main/java/MainWindow.java new file mode 100644 index 0000000000..e170e1f45a --- /dev/null +++ b/src/main/java/MainWindow.java @@ -0,0 +1,68 @@ +//package duke; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.application.Platform; + +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +import java.util.Timer; +import java.util.TimerTask; + +/** + * 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() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + String welcomeMessage = "Hello! This is Duke, your very own chat bot. Type 'help' to see the list of commands available! Or if you're a pro, get started by adding some tasks!"; + dialogContainer.getChildren().addAll(DialogBox.getDukeDialog(welcomeMessage, 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() { + String input = userInput.getText(); + String response = duke.getResponse(input); + //below if-condition adapted from forum thread #172 + if (input.equals("bye")) { + new Timer().schedule(new TimerTask() { + public void run () { Platform.exit();; } + }, 1000); + new Timer().schedule(new TimerTask() { + public void run () { System.exit(0);; } + }, 2000); + } + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/duke/Deadline.java b/src/main/java/duke/Deadline.java new file mode 100644 index 0000000000..151016149a --- /dev/null +++ b/src/main/java/duke/Deadline.java @@ -0,0 +1,45 @@ +package duke; + +import java.time.LocalDateTime; + +/** Child class that extends parent 'Task' class and handles + * operations for the Deadline task. + */ +public class Deadline extends Task { + private LocalDateTime by; + + /** + * Constructor for the Deadline class. + * + * @param description The description of the task + * @param by The date which is the deadline of the task + */ + public Deadline(String description, LocalDateTime by) { + super(description); + this.by = by; + } + + /** + * Converts the date of the object from type LocalDateTime + * to String for easy manipulation + * + * @param dateTime The time of the task stored in list as LocalDateTime + * @return The manipulated string of the object's date and time + */ + public String getDateTimeString(LocalDateTime dateTime) { + String dateTimeString = dateTime.toString(); + String[] splitDateAndTime = dateTimeString.split("T"); + String DateTime = splitDateAndTime[0] + " " + splitDateAndTime[1]; + return DateTime; + } + + /** + * toString() method for deadline object + * + * @return The task printed in the correct format + */ + @Override + public String toString() { + return "[D]" + super.toString() + "by: " + getDateTimeString(this.by); + } +} \ No newline at end of file diff --git a/src/main/java/duke/DukeException.java b/src/main/java/duke/DukeException.java new file mode 100644 index 0000000000..9568f9583b --- /dev/null +++ b/src/main/java/duke/DukeException.java @@ -0,0 +1,55 @@ +package duke; + +/**Class that handles the exceptions*/ +public abstract class DukeException extends RuntimeException { + + /** Constructor for DukeException class */ + public DukeException(String message) { + super(message); + } + + public String getErrorMessage() { + return ""; + } +} + +/** Class that handles the null task input exceptions */ +class NullTaskError extends DukeException { + + /** Constructor for NullTaskError class */ + public NullTaskError() { + super("OOPS!!! The description of a todo cannot be empty."); + } + + /** + * Returns the error message when the user does not enter + * the description of a particular task + * + * @param type The type of the task - to-do or deadline or event + * @return The corresponding output error message + */ + public String getErrorMessage(String type) { + return "OOPS!!! The description of a " + type + " cannot be empty."; + } +} + +/**Class that handles the non exist keyword exceptions*/ +class NonExistentKeyword extends DukeException { + + /** Constructor for NonExistentKeyword class */ + public NonExistentKeyword() { + super("OOPS!!! I'm sorry, but I don't know what that means :-("); + } + + /** + * Returns an error message when the user types in a command + * that is not supported + * + * @return The error message as well as a list of approved commands + * that they can enter + */ + public String getErrorMessage() { + return "OOPS!!! I'm sorry, but I don't know what that means :-( " + "\n" + + "Type 'help' to see the list of commands available!"; + } +} diff --git a/src/main/java/duke/Event.java b/src/main/java/duke/Event.java new file mode 100644 index 0000000000..3b924e8ffe --- /dev/null +++ b/src/main/java/duke/Event.java @@ -0,0 +1,45 @@ +package duke; + +import java.time.LocalDateTime; + +/**Child class that extends parent 'Task' class and handles + * operations for the Event task. + * */ +public class Event extends Task{ + private LocalDateTime at; + + /** + * Constructor for the Event class. + * + * @param description The description of the task + * @param at The date on which the task must be executed + */ + public Event(String description, LocalDateTime at) { + super(description); + this.at = at; + } + + /** + * Converts the date of the object from type LocalDateTime + * to String for easy manipulation + * + * @param dateTime The time of the task stored in list as LocalDateTime + * @return The manipulated string of the object's date and time + */ + public String getEventDateTimeString(LocalDateTime dateTime) { + String dateTimeString = dateTime.toString(); + String[] splitDateAndTime = dateTimeString.split("T"); + String DateTime = splitDateAndTime[0] + " " + splitDateAndTime[1]; + return DateTime; + } + + /** + * toString() method for event object + * + * @return The task printed in the correct format + */ + @Override + public String toString() { + return "[E]" + super.toString() + "at: " + getEventDateTimeString(this.at); + } +} diff --git a/src/main/java/duke/Parser.java b/src/main/java/duke/Parser.java new file mode 100644 index 0000000000..9cac44270b --- /dev/null +++ b/src/main/java/duke/Parser.java @@ -0,0 +1,414 @@ +package duke; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** Class that handles the input provided by user */ +public class Parser { + + /** + * Parses the input entered by the user and takes the appropriate + * action to deal with the input + * + * @param input The string that contains the user input + * @param t The TaskList that contains the tasks to be added + * @param s The Storage that handles the reading and writing to a text file + */ + public static String parse(String input, TaskList t, Storage s) { + if (input.equals("list")) { + return returnListContents(t); + } else if (input.startsWith("done") && checkDoneStatementConditions(input)) { + return parseDoneCommand(input, t, s); + } else if (input.startsWith("todo")) { + return parseTodoCommand(input, t, s); + } else if (input.startsWith("deadline")) { + return parseDeadlineCommand(input, t, s); + } else if (input.startsWith("event")) { + return parseEventCommand(input, t, s); + } else if (input.startsWith("delete") && input.length() < 11) { + return parseDeleteCommand(input, t, s); + } else if (input.startsWith("find")) { + return parseFindCommand(input, t); + } else if (input.startsWith("schedule on")) { + return parseScheduleOnCommand(input, t); + } else if (input.startsWith("schedule until")) { + return parseScheduleUntilCommand(input, t); + } else if (input.startsWith("schedule after")) { + return parseScheduleAfterCommand(input, t); + } else if (input.startsWith("help")) { + return parseHelpCommand(); + } else { + DukeException e = new NonExistentKeyword(); + return e.getErrorMessage(); + } + } + + /** + * Returns the intended output for the 'list' input entered + * + * @param t The task list that gets read + * @return String The output string that contains all the elements + * in the task list + */ + public static String returnListContents(TaskList t) { + String returnString = " Here are the tasks in your list: " + "\n"; + if (t.size() == 0) { + return "There are no existing tasks! Type 'help' to see list of commands available to add tasks"; + } else { + for (int i = 0; i < t.size(); i++) { + returnString = returnString + (i + 1) + ". " + t.get(i).toString() + "\n"; + } + return returnString; + } + } + + /** + * Returns the intended output for the 'done' input entered + * + * @param t The task list that gets read + * @return String The output message that contains the intended output for + * the 'done' command + */ + public static String parseDoneCommand(String input, TaskList t, Storage s) { + if (t.size() == 0) { + return "There are no existing tasks! Type 'help' to see list of commands available to add tasks"; + } + String returnString = ""; + int index = Integer.parseInt(input.replaceAll("[^0-9]", "")); + if (index > t.size()) { + return "You have entered an invalid index. You only have " + Integer.valueOf(t.size()) + " task(s) in your list"; + } + assert index > 0: "index of task to be marked done must be more than 0"; + t.get(index - 1).setDone(); + s.appendListToFile(t); + returnString = returnString + "Nice! I've marked this task as done: " + "\n"; + returnString = returnString + "[X] " + t.get(index - 1).description + "\n"; + return returnString; + } + + /** + * Returns the intended output for the 'to-do' input entered + * + * @param t The task list that gets read + * @return String The output message that contains the intended output for the 'to-do' command + */ + public static String parseTodoCommand(String input, TaskList t, Storage s) { + String returnString = ""; + if (input.length() < 6) { + return new NullTaskError().getErrorMessage("todo"); + } else { + String firstTodo = input.substring(5); + t.addTodo(firstTodo); + s.appendListToFile(t); + returnString = returnString + "Got it! I've added this task: " + "\n"; + returnString = returnString + new Todo(firstTodo).toString() + "\n"; + returnString = returnString + "Now you have " + t.size() + " task(s) in the list" + "\n"; + return returnString; + } + } + + /** + * Returns the intended output for the 'deadline' input entered + * + * @param t The task list that gets read + * @return String The output message that contains the intended output for the 'deadline' command + */ + public static String parseDeadlineCommand(String input, TaskList t, Storage s) { + String returnString = ""; + if (input.length() < 10) { + return new NullTaskError().getErrorMessage("deadline"); + } else { + String[] wordsInTaskDescription = input.split("/by"); + String timeString = input.substring(input.length()-4); + String deadlineDescription = wordsInTaskDescription[0].substring(9); + LocalDateTime deadlineDate = getDate(wordsInTaskDescription, timeString); + t.addDeadline(deadlineDescription, deadlineDate); + s.appendListToFile(t); + returnString = returnString + "Got it! I've added this task: " + "\n"; + returnString = returnString + new Deadline(deadlineDescription, deadlineDate).toString() + "\n"; + returnString = returnString + "Now you have " + t.size() + " task(s) in the list" + "\n"; + return returnString; + } + } + + /** + * Returns the intended output for the 'event' input entered + * + * @param t The task list that gets read + * @return String The output message that contains the intended output for the 'event' command + */ + public static String parseEventCommand(String input, TaskList t, Storage s) { + String returnString = ""; + if (input.length() < 7) { + return new NullTaskError().getErrorMessage("event"); + } else { + String[] wordsInEventDescription = input.split("/at"); + String timeString = input.substring(input.length()-4); + String eventDescription = wordsInEventDescription[0].substring(6); + LocalDateTime eventDate = getDate(wordsInEventDescription, timeString); + t.addEvent(eventDescription, eventDate); + returnString = returnString + "Got it! I've added this task: " + "\n"; + returnString = returnString + new Event(eventDescription, eventDate).toString() + "\n"; + returnString = returnString + "Now you have " + t.size() + " task(s) in the list" + "\n"; + s.appendListToFile(t); + return returnString; + } + } + + /** + * Returns the intended output for the 'delete' input entered + * + * @param t The task list that gets read + * @return String The output message that contains the intended output for the 'delete' command + */ + public static String parseDeleteCommand(String input, TaskList t, Storage s) { + if (t.size() == 0) { + return "There are no existing tasks! To add tasks, use the 'todo', 'event' or 'deadline' keywords"; + } + String returnString = ""; + int taskIndex = Integer.parseInt(input.replaceAll("[^0-9]", "")); + if (taskIndex > t.size()) { + return "You have entered an invalid index. You only have " + Integer.valueOf(t.size()) + " task(s) in your list"; + } + Task removedTask = t.get(taskIndex - 1); + t.delete(taskIndex - 1); + returnString = returnString + "Noted. I've removed this task: " + "\n"; + returnString = returnString + removedTask.toString() + "\n"; + returnString = returnString + "Now you have " + t.size() + " task(s) in the list" + "\n"; + s.appendListToFile(t); + return returnString; + } + + /** + * Returns the intended output for the 'find' input entered + * + * @param t The task list that gets read + * @return String The output message that contains the intended output for the 'find' command + */ + public static String parseFindCommand(String input, TaskList t) { + String keyword = input.substring(5); + String returnString = ""; + int count = 0; + for (Task task : t.getTaskList()) { + String taskDescription = task.description.substring(0, task.description.length()); + String[] wordsInTaskDescription = taskDescription.split(" "); + for (String str : wordsInTaskDescription) { + if (str.equals(keyword)) { + returnString = returnString + task.toString() + "\n"; + count = count + 1; + } + } + } + if (count == 0) { + return "OOPS! The task does not exist"; + } else { + return returnString; + } + } + + /** + * Returns whether the done statement fits the correct format + * + * @param input The input of the user + * @return Boolean Whether the input matches the required format + */ + public static Boolean checkDoneStatementConditions(String input) { + return input.length() <= 8 && Character.isDigit(input.charAt(5)); + } + + /** + * Returns the date of the task (event/deadline) + * + * @param tempEvent An array of string that contains the userInput + * split into two parts + * @return LocalDate The date of the task + */ + public static LocalDateTime getDate(String[] tempEvent, String time) { + String[] splitDateAndDescription = tempEvent[1].split(" "); + String date = splitDateAndDescription[1]; + String[] splitDateComponents = date.split("/"); + String year = splitDateComponents[2]; + String month = splitDateComponents[1]; + String currentDate = splitDateComponents[0]; + if (currentDate.length() == 1) { + currentDate = "0" + currentDate; + } + if (month.length() == 1) { + month = "0" + month; + } + String finalDateFormat = year + "-" + month + "-" + currentDate; + String finalDateTimeFormat = finalDateFormat + " " + time; + DateTimeFormatter datePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm"); + LocalDateTime taskDateTime = LocalDateTime.parse(finalDateTimeFormat, datePattern); + return taskDateTime; + } + + /** + * Returns a string that should be output when the user types the + * 'schedule on' command + * + * @param input The input entered by the user + * @param t The task list that stores the current tasks + * @return the string that should be output to the user + */ + public static String parseScheduleOnCommand(String input, TaskList t) { + if (t.size() == 0) { + return "There are no existing tasks!"; + } else { + String stringOfDate = input.substring(12); + LocalDate scheduleOnDate = getScheduleDate(stringOfDate); + String returnString = "Here are the list of tasks scheduled on " + stringOfDate + ":" + "\n"; + int counter = 0; + for (Task task : t.getTaskList()) { + if (task.getType().equals("E")) { + String[] splitDescriptionAndDate = task.toString().split("at:"); + String evenDateAndTime = splitDescriptionAndDate[1].substring(1); + String eventDateWithoutTime = evenDateAndTime.substring(0, evenDateAndTime.length() - 6); + LocalDate eventDate = LocalDate.parse(eventDateWithoutTime); + if (eventDate.compareTo(scheduleOnDate) == 0) { + returnString = returnString + Integer.valueOf(counter + 1) + ". " + task.toString() + "\n"; + counter += 1; + } + } else if (task.getType().equals("D")) { + String[] splitEventDescriptionAndDate = task.toString().split("by:"); + String deadlineDateAndTime = splitEventDescriptionAndDate[1].substring(1); + String deadlineDateWithoutTime = deadlineDateAndTime.substring(0, deadlineDateAndTime.length() - 6); + LocalDate deadlineDate = LocalDate.parse(deadlineDateWithoutTime); + if (deadlineDate.compareTo(scheduleOnDate) == 0) { + returnString = returnString + Integer.valueOf(counter + 1) + ". " + task.toString() + "\n"; + counter += 1; + } + } else { + continue; + } + } + return returnString; + } + } + + /** + * Returns a string that should be output when the user types the + * 'schedule until' command + * + * @param input The input entered by the user + * @param t The task list that stores the current tasks + * @return the string that should be output to the user + */ + public static String parseScheduleUntilCommand(String input, TaskList t) { + if (t.size() == 0) { + return "There are no existing tasks!"; + } else { + String stringOfDate = input.substring(15); + LocalDate scheduleOnDate = getScheduleDate(stringOfDate); + String returnString = "Here are the list of tasks scheduled until " + stringOfDate + ":" + "\n"; + int counter = 0; + for (Task task : t.getTaskList()) { + if (task.getType().equals("E")) { + String[] splitDescriptionAndDate = task.toString().split("at:"); + String dateAndTime = splitDescriptionAndDate[1].substring(1); + String eventDateString = dateAndTime.substring(0, dateAndTime.length() - 6); + LocalDate eventOnDate = LocalDate.parse(eventDateString); + if (eventOnDate.compareTo(scheduleOnDate) < 0) { + returnString = returnString + Integer.valueOf(counter + 1) + ". " + task.toString() + "\n"; + counter += 1; + } + } else if (task.getType().equals("D")) { + String[] split = task.toString().split("by:"); + String dateAndTime = split[1].substring(1); + String deadlineDateString = dateAndTime.substring(0, dateAndTime.length() - 6); + LocalDate deadlineByDate = LocalDate.parse(deadlineDateString); + if (deadlineByDate.compareTo(scheduleOnDate) < 0) { + returnString = returnString + Integer.valueOf(counter + 1) + ". " + task.toString() + "\n"; + counter += 1; + } + } else { + continue; + } + } + return returnString; + } + } + + /** + * Returns a string that should be output when the user types the + * 'schedule after' command + * + * @param input The input entered by the user + * @param t The task list that stores the current tasks + * @return the string that should be output to the user + */ + public static String parseScheduleAfterCommand(String input, TaskList t) { + if (t.size() == 0) { + return "There are no existing tasks!"; + } else { + String stringOfDate = input.substring(15); + LocalDate scheduleAfterDate = getScheduleDate(stringOfDate); + String returnString = "Here are the list of tasks scheduled after " + stringOfDate + ":" + "\n"; + int counter = 0; + for (Task task : t.getTaskList()) { + if (task.getType().equals("E")) { + String[] splitDescriptionAndDate = task.toString().split("at:"); + String eventDateAndTime = splitDescriptionAndDate[1].substring(1); + String eventDateWithoutTime = eventDateAndTime.substring(0, eventDateAndTime.length() - 6); + LocalDate eventDate = LocalDate.parse(eventDateWithoutTime); + if (eventDate.compareTo(scheduleAfterDate) > 0) { + returnString = returnString + Integer.valueOf(counter + 1) + ". " + task.toString() + "\n"; + counter += 1; + } + } else if (task.getType().equals("D")) { + String[] splitDeadlineDescriptionAndDate = task.toString().split("by:"); + String deadlineDateAndTime = splitDeadlineDescriptionAndDate[1].substring(1); + String deadlineDateWithoutTime = deadlineDateAndTime.substring(0, deadlineDateAndTime.length() - 6); + LocalDate deadlineByDate = LocalDate.parse(deadlineDateWithoutTime); + if (deadlineByDate.compareTo(scheduleAfterDate) > 0) { + returnString = returnString + Integer.valueOf(counter + 1) + ". " + task.toString() + "\n"; + counter += 1; + } + } else { + continue; + } + } + return returnString; + } + } + + /** + * Returns the date entered by the user as a LocalDate + * + * @param stringOfDate The date that has been entered by the user + * @return The date entered by the user after being converted to a + * LocalDate object + */ + public static LocalDate getScheduleDate(String stringOfDate) { + String[] dateComponents = stringOfDate.split("/"); + String year = dateComponents[2]; + String month = dateComponents[1]; + String currentDate = dateComponents[0]; + if (currentDate.length() == 1) { + currentDate = "0" + currentDate; + } + if (month.length() == 1) { + month = "0" + month; + } + String finalDateFormat = year + "-" + month + "-" + currentDate; + LocalDate scheduleOnDate = LocalDate.parse(finalDateFormat); + return scheduleOnDate; + } + + public static String parseHelpCommand() { + return "Here are the list of commands that you can type: " + "\n" + + "'list' - to view current tasks" + "\n" + + "'done ' - to mark a task as done" + "\n" + + "'delete ' - to delete a task from list" + "\n" + + "'todo ' - add a todo task" + "\n" + + "deadline /by