diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..a23efb5028 --- /dev/null +++ b/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'org.openjfx.javafxplugin' version '0.0.10' + 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 = "duke.gui.Launcher" +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +checkstyle { + toolVersion = '8.29' +} + +run{ + standardInput = System.in + enableAssertions = true +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..b1e659a3f8 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..dcaa1af3c3 --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 8077118ebe..a5436d7d07 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,28 +2,225 @@ ## Features -### Feature-ABC +### Add Tasks +There are a few types of tasks that can be added: +1. A basic ```Todo``` task containing a description +2. A ```Deadline``` task that need to be done before a specific date/time +3. An ```Event``` task that occurs at a specific date/time/location +4. A ```DoAfter``` task that needs to be done after a specific time/task -Description of the feature. +
+Note: Automatic Date & Time Formatting -### Feature-XYZ +Dates in the form of YYYY/MM/DD, or with delimiters - | / . will be converted to MMM d yyyy format. Time in HHMM format will similarly be converted to HH:MM format.
-Description of the feature. + +### List tasks +Tasks that have been added so far can be listed. + +### Delete task +Task at a specified index can be deleted. + +### Search for tasks +Matching tasks containing a keyword can be found and listed. + +### Marking tasks as (in)complete + +Tasks at a specified index can be marked as complete or incomplete. ## Usage -### `Keyword` - Describe action +### `todo` - Add a Todo task + +Adds a ```Todo``` task containing a description. + +Example of usage: + +`todo collect primogems` + +Expected outcome: + +Task of the type ```Todo``` containing the description 'read a book' will be added. + +``` +Got it! I have noted down the following task in your list. +[T][ ] collect primogems +``` + +
+ +### `deadline` - Add a Deadline task + +Adds a ```Deadline``` task containing a description and a datetime string. + +Example of usage: + +`deadline wish on Zhongli's banner by 2022/02/18 2359 GMT +8` + +Expected outcome: + +Task of the type ```Deadline``` containing the description 'wish on Zhongli's banner' and formatted datetime string 'Feb 18 2022 23:59 GMT +8' will be added. + +``` +Got it! I have noted down the following task in your list. +Remember the deadline! +[D][ ] wish on Zhongli's banner (by: Feb 18 2022 23:59 GMT +8) +``` + +
+ +### `event` - Add an event task -Describe the action and its outcome. +Adds an ```Event``` task containing a description and a datetime string. Example of usage: -`keyword (optional arguments)` +`event Windtrace festival at 2022/02/08 online` + +Expected outcome: + +Task of the type ```Event``` containing the description 'Windtrace festival' and formatted datetime string 'Feb 8 2022 online' will be added. + +``` +Got it! I have noted down the following task in your list. +Do be there on time! +[E][ ] Windtrace festival (at: Feb 8 2022 online) +``` + +
+ +### `doafter` - Add a DoAfter task + +Adds a ```DoAfter``` task containing a description and a datetime string. + +Example of usage: + +`doafter collect 900 primogems after reaching AR 15` + +Expected outcome: + +Task of the type ```DoAfter``` containing the description 'collect 900 primogems' and formatted datetime string 'reaching AR 15' will be added. + +``` +Got it! I have noted down the following task in your list. +Don't forget it! +[A][ ] collect 900 primogems (at: reaching AR 15) +``` + +
+ +### `list` - List all tasks + +Lists all tasks thus far. + +Example of usage: + +`list` + +Expected outcome: +A printed list of all saved tasks and the total number of tasks. + +``` +Hmm... Paimon keeps a clear record in her diary. + 1. [T][ ] collect primogems + 2. [D][ ] wish on Zhongli's banner (by: Feb 18 2022 23:59 GMT +8) + 3. [E][ ] Windtrace festival (at: Feb 8 2022 online) + 4. [A][ ] collect 900 primogems (at: reaching AR 15) +You have 4 tasks on your list. +``` + +
+ +### `delete` - Delete a task + +Deletes a task at a specified index. + +Example of usage: + +```delete 2``` + +Expected outcome: + +Task at the specified index will be removed. The resulting list will be printed. + +``` +Noted, the task has been scrubbed off the list! + 1. [T][ ] collect primogems + 2. [E][ ] Windtrace festival (at: Feb 8 2022 online) + 3. [A][ ] collect 900 primogems (at: reaching AR 15) +You have 3 tasks on your list. +``` + +
+ +### `do` - Do a task + +Marks a task at a specified index as complete. + +Example of usage: + +```do 1``` + +Expected outcome: + +Task at the specified index will be marked complete. + +``` +Task successfully updated. +``` +A subsequent ```list``` command will result in +``` +Noted, the task has been scrubbed off the list! + 1. [T][✓] collect primogems + 2. [E][ ] Windtrace festival (at: Feb 8 2022 online) + 3. [A][ ] collect 900 primogems (at: reaching AR 15) +You have 3 tasks on your list. +``` + +
+ +### `undo` - Undo a task + +Marks a task at a specified index as incomplete. + +Example of usage: + +```undo 1``` + +Expected outcome: + +Task at the specified index will be marked complete. + +``` +Task successfully updated. +``` +A subsequent ```list``` command will result in +``` +Noted, the task has been scrubbed off the list! + 1. [T][ ] collect primogems + 2. [E][ ] Windtrace festival (at: Feb 8 2022 online) + 3. [A][ ] collect 900 primogems (at: reaching AR 15) +You have 3 tasks on your list. +``` + +
+ + +### `find` - Search for keyword + +Finds and lists tasks containing a keyword. The keyword is case-sensitive. + +Example of usage: + +```find collect``` Expected outcome: -Description of the outcome. +The list of tasks containing the keyword 'collect', and the number of such tasks. ``` -expected output +Here are the matching tasks in your list: + 1. [T][ ] collect primogems + 2. [A][ ] collect 900 primogems (at: reaching AR 15) +You have 2 tasks on your list. ``` diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..10d92a87e9 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..b71055e9b8 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,5 @@ +markdown: kramdown +highlighter: rouge +kramdown: + input: GFM + hard_wrap: false 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..b9e2753c97 --- /dev/null +++ b/src/main/java/duke/Duke.java @@ -0,0 +1,253 @@ +package duke; + +import duke.task.Deadline; +import duke.task.DoAfter; +import duke.task.Event; +import duke.task.TaskList; +import duke.task.Todo; + +/** + * The Duke program implements a simple task bot with CRUD functionality. + * The program can add three different types of tasks (todo, deadline, + * event), mark tasks as done, and delete tasks. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class Duke { + private Storage storage; + private TaskList tasks; + + /** + * Constructor for Duke specifying UI and Storage. + * + * It loads Tasks from a file into tasks. + * If there is an error with loading Tasks from the specified file, it + * initializes tasks to bean empty TaskList. + */ + public Duke(String filePath) { + try { + storage = new Storage(filePath); + tasks = new TaskList(storage.load()); + } catch (Exception e) { + System.out.println(Ui.showLoadingError()); + tasks = new TaskList(); + } + } + + /** + * Marks Task as complete. + * + * @param input index of task + * @return Duke's response and expression type. + * @throws Exception if an exception occurs in the saving of + * data to the file at filePath or NumberFormatException + */ + public Response doHandler(String input) throws Exception { + + int i = Integer.parseInt(input.replaceAll("[^0-9]", + "")) - 1; + assert i <= tasks.size() && i >= 0 : "index of item should be within scope of tasklist"; + tasks.get(i).markComplete(); + assert tasks.get(i).getStatusIcon().equals("\u2713") : "task should be marked done"; + + storage.save(tasks); + return new Response(Ui.showDoMessage(), Expression.HAPPY); + } + + /** + * Marks Task as incomplete. + * + * @param input index of task + * @return Duke's response and expression type. + * @throws Exception if an exception occurs in the saving of + * data to the file at filePath or NumberFormatException + */ + public Response undoHandler(String input) throws Exception { + int i = Integer.parseInt(input.replaceAll("[^0-9]", + "")) - 1; + assert i <= tasks.size() && i >= 0 : "index of item should be within scope of tasklist"; + tasks.get(i).markIncomplete(); + assert tasks.get(i).getStatusIcon().equals(" ") : "task should be marked incomplete"; + + storage.save(tasks); + return new Response(Ui.showUndoMessage(), Expression.DISAPPOINTED); + } + + /** + * Deletes Task at a specified index from tasks. + * + * @param input index of task + * @return Response of list of tasks, deletion message, and expression type. + * @throws Exception if an exception occurs in the saving of + * data to the file at filePath or NumberFormatException + */ + public Response deleteHandler(String input) throws Exception { + int i = Integer.parseInt(input.replaceAll("[^0-9]", + "")) - 1; + assert i <= tasks.size() && i >= 0 : "index of item should be within scope of tasklist"; + tasks.remove(i); + + storage.save(tasks); + return new Response(Ui.showDeleteMessage(tasks), Expression.THUMBSUP); + } + + /** + * Returns a string of the list of Tasks. + * + * @return Response of formatted string of list of tasks and + * expression type. + */ + public Response listHandler() { + return new Response(Ui.showListMessage(tasks), Expression.DEFAULT); + } + + /** + * Adds a Todo to tasks and returns string about the addition. + * + * @param input description of Todo + * @return Duke's response and expression type. + * @throws Exception if an exception occurs in the saving of + * data to the file at filePath + */ + public Response todoHandler(String input) throws Exception { + Todo t = new Todo(input); + tasks.add(t); + + storage.save(tasks); + return new Response(Ui.showTodoMessage() + t, Expression.THUMBSUP); + } + + + /** + * Finds tasks with matching keywords in tasklist and returns them. + * + * @param input keyword to search for + * @return Response of matching tasks and expression type. + */ + public Response findHandler(String input) { + return new Response(Ui.showFindMessage(tasks.find(input)), + Expression.DEFAULT); + } + + /** + * Adds a Deadline task to tasks and returns string about the addition. + * + * @param input string containing description and dateTime of task + * @return Duke's response and expression type. + * @throws Exception if an exception occurs in the saving of + * data to the file at filePath + */ + public Response deadlineHandler(String input) throws Exception { + String datetime = input.replaceAll(".* by ", ""); + input = input.replaceAll(" by .*", ""); + + Deadline d = new Deadline(input, datetime); + tasks.add(d); + + storage.save(tasks); + return new Response(Ui.showDeadlineMessage() + d, Expression.THUMBSUP); + } + + /** + * Adds an Event task to tasks and returns string about the addition. + * + * @param input string containing description and dateTime of task + * @return Duke's response and expression type. + * @throws Exception if an exception occurs in the saving of + * data to the file at filePath + */ + public Response eventHandler(String input) throws Exception { + String time = input.replaceAll(".* at ", ""); + input = input.replaceAll(" at .*", ""); + Event e = new Event(input, time); + tasks.add(e); + storage.save(tasks); + return new Response(Ui.showEventMessage() + e, Expression.THUMBSUP); + } + + /** + * Adds a doAfter task to tasks and returns string about the addition. + * + * @param input string containing description and dateTime of task + * @return Duke's response and expression type. + * @throws Exception if an exception occurs in the saving of + * data to the file at filePath + */ + public Response doAfterHandler(String input) throws Exception { + String time = input.replaceAll(".* after ", ""); + input = input.replaceAll(" after .*", ""); + DoAfter e = new DoAfter(input, time); + tasks.add(e); + storage.save(tasks); + return new Response(Ui.showDoAfterMessage() + e, Expression.THUMBSUP); + } + + /** + * Returns 'EXIT' string to signal program termination. + * + * @return Duke's response and expression type. + */ + public Response byeHandler() { + Response r = new Response(Ui.showByeMessage(), Expression.DEFAULT); + r.setExit(); + return r; + } + + /** + * Returns response to indecipherable input. + * + * @param input string to respond to + * @return generic response indicative of incorrect input and + * expression type. + */ + public Response defaultHandler(String input) { + String response = input.equals("") + ? Ui.showEmptyMessage() + : Ui.showDefaultMessage(); + return new Response(response, Expression.DEFAULT); + } + + + /** + * Determines Duke's responses to user input. + * + * @param input user input + * @return Duke's response and expression type. + */ + public Response getResponse(String input) { + try { + String command = Parser.parse(input, tasks); + input = Parser.handleInput(input); + + switch (command) { + case "list": + return listHandler(); + case "do": + return doHandler(input); + case "undo": + return undoHandler(input); + case "delete": + return deleteHandler(input); + case "todo": + return todoHandler(input); + case "find": + return findHandler(input); + case "deadline": + return deadlineHandler(input); + case "event": + return eventHandler(input); + case "doafter": + return doAfterHandler(input); + case "bye": + return byeHandler(); + default: + return defaultHandler(input); + } + } catch (NumberFormatException e) { + return new Response(Ui.showNumberFormatMessage(), Expression.DISAPPOINTED); + } catch (Exception e) { + return new Response(Ui.showError(e.getMessage()), Expression.DISAPPOINTED); + } + } +} diff --git a/src/main/java/duke/Expression.java b/src/main/java/duke/Expression.java new file mode 100644 index 0000000000..1d4e93d2c5 --- /dev/null +++ b/src/main/java/duke/Expression.java @@ -0,0 +1,8 @@ +package duke; + +public enum Expression { + DEFAULT, + DISAPPOINTED, + HAPPY, + THUMBSUP +} diff --git a/src/main/java/duke/Main.java b/src/main/java/duke/Main.java new file mode 100644 index 0000000000..ab5a129dd0 --- /dev/null +++ b/src/main/java/duke/Main.java @@ -0,0 +1,42 @@ +package duke; + +import java.io.IOException; + +import duke.gui.MainWindow; +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 static final String FILEPATH = "data/tasks.txt"; + private final Duke duke = new Duke(FILEPATH); + + @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.getIcons().add(new Image(this.getClass().getResourceAsStream("/images/Icon.png"))); + stage.setTitle("The Traveller's Handbook"); + stage.setResizable(false); + scene.getStylesheets().add("https://fonts.googleapis.com/css2?family=Open+Sans"); + scene.getStylesheets().add(getClass().getResource("/style.css").toExternalForm()); + + fxmlLoader.getController().setDuke(duke); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/duke/Parser.java b/src/main/java/duke/Parser.java new file mode 100644 index 0000000000..5f92fdf178 --- /dev/null +++ b/src/main/java/duke/Parser.java @@ -0,0 +1,130 @@ +package duke; + +import duke.task.TaskList; + +/** + * Represents a class that validates and interprets user input with a + * pre-generated list of allowable input formats. + * Note: Current allowable formats are "list", "bye", "do X", "undo X", + * "delete X", "todo S", "deadline S by T", "event S at T", "find W", + * where X is an integer, S is a string descriptor of the task, W is a + * singular keyword and T is a string descriptor of the date(s) and/or + * time(s) associated with the task. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class Parser { + + /** + * Extracts the non-command contents of a given input string. + * Strips the string of leading whitespaces and removes the first word. + * + * @param input input string + * @return input consisting of the non-command contents of the input string + */ + public static String handleInput(String input) { + input = input.trim(); + if (input.contains(" ")) { + input = input.substring(input.indexOf(" ")); + } + return input; + } + + /** + * Validates input format and extracts command. + * Note: Current allowable formats are "list", "bye", "do X", "undo X", + * "delete X", "todo S", "find W", "deadline S by T", "event S at T", + * where X is an integer, S is a string descriptor of the task, W is a + * singular keyword and T is a string descriptor of the date(s) and/or + * time(s) associated with the task. + * + * @param input input string to be parsed + * @return command from input string if input is of a valid format, + * "" otherwise + * @throws Exception if input string is not of an expected format + */ + public static String parse(String input, TaskList tasklist) throws Exception { + input = input.trim(); + String command = input.replaceAll(" .*", ""); + + input = input.trim(); + if (input.equals("bye") || input.equals("list")) { + return command; + } + + // Handle do, undo, delete + String firstWord = input.replaceAll(" .*", ""); + input = input.substring(firstWord.length()).trim(); + + switch (firstWord) { + case "do": + // Fallthrough + case "undo": + // Fallthrough + case "delete": + int index = Integer.parseInt(input); + input = input.replaceAll(".* ", ""); + if (input.matches("[0-9]+") && index <= tasklist.size() + && index >= 1) { + assert index <= tasklist.size() : "index not within range"; + return command; + } + + throw new Exception("You need to specify the task you want to " + + firstWord + " by its index :c"); + case "todo": + if (input.equals("")) { + throw new Exception("Oops, you need to mention what the " + + "task is :c"); + } + return command; + case "deadline": + if (!input.contains(" by ")) { + throw new Exception("Oops, you need to format deadline tasks " + + "as \"deadline X by Y\" :c"); + } + + String lastWord = input.substring(input.lastIndexOf(" ") + 1); + if (lastWord.equals("by")) { + return ""; + } + return command; + case "event": + if (!input.contains(" at ")) { + throw new Exception("Oops, you need to format event tasks " + + "as \"event X at Y\" :c"); + } + + String finalWord = input.substring(input.lastIndexOf(" ") + 1); + if (finalWord.equals("at")) { + return ""; + } + return command; + case "doafter": + if (!input.contains(" after ")) { + throw new Exception("Oops, you need to format doafter tasks " + + "as \"doafter X after Y\" :c"); + } + + String endWord = input.substring(input.lastIndexOf(" ") + 1); + if (endWord.equals("after")) { + return ""; + } + return command; + case "find": + if (input.equals("")) { + throw new Exception("Oops, you need to mention what " + + "the keyword is :c"); + } + if (input.contains(" ")) { + throw new Exception("Oops, you can only search for " + + "one keyword at a time :c"); + } + + return command; + default: + return ""; + } + } +} diff --git a/src/main/java/duke/Response.java b/src/main/java/duke/Response.java new file mode 100644 index 0000000000..0385cbbbe8 --- /dev/null +++ b/src/main/java/duke/Response.java @@ -0,0 +1,40 @@ +package duke; + +/** + * Represents a Response class that contains Duke's response to user input. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class Response { + private Expression expression; + private String message; + private Boolean isExit = false; + + /** + * Constructor for Response. + * + * @param message Duke's text response + * @param expression Duke's visual expression + */ + public Response(String message, Expression expression) { + this.expression = expression; + this.message = message; + } + + public Expression getExpression() { + return expression; + } + + public String getMessage() { + return message; + } + + public void setExit() { + isExit = true; + } + + public boolean isExit() { + return isExit; + } +} diff --git a/src/main/java/duke/Storage.java b/src/main/java/duke/Storage.java new file mode 100644 index 0000000000..9d424c62cb --- /dev/null +++ b/src/main/java/duke/Storage.java @@ -0,0 +1,194 @@ +package duke; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Scanner; + +import duke.task.Deadline; +import duke.task.DoAfter; +import duke.task.Event; +import duke.task.Task; +import duke.task.TaskList; +import duke.task.Todo; + +/** + * Represents a storage space for tasks on hard-drive. + * It handles the loading of tasks from a file and saving + * of tasks to the same file. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class Storage { + private String filePath; + private ArrayList tasks; + + /** + * Constructor for Storage. + * Initializes the Storage object with a given file path. + * It parses input from the specified file and saves the list of Tasks from + * the specified file, if any. If file does not exist, it creates the file. + * Note: any missing parent directories in the specified file path are + * created prior to file creation. + * + * @param filePath path of the specified file from the current directory + * @throws IOException if an exception occurs in the creation/access of + * the specified file + * @throws RuntimeException if file at specified path contains data in a + * non-standard format + */ + Storage(String filePath) throws IOException { + tasks = new ArrayList<>(); + this.filePath = filePath; + File data = new File(filePath); + //make preceding directories, if any are not found + ignoreResult(data.getParentFile().mkdirs()); + + // If file does not exist, create new file and return + if (data.createNewFile()) { + assert data.exists() : "file should be created if it does not exist"; + return; + } + assert data.exists() : "file should exist"; + + Scanner fileReader = new Scanner(data); + while (fileReader.hasNextLine()) { + String line = fileReader.nextLine(); + String[] tmp = line.split("\\|"); + boolean isDone = tmp[1].trim().equals("D"); + + switch (tmp[0].trim()) { + case "T": + assert tmp.length > 2 : "tmp[2] should exist"; + todoHandler(tmp, isDone); + break; + case "D": + assert tmp.length > 3 : "tmp[2], tmp[3] should exist"; + deadlineHandler(tmp, isDone); + break; + case "E": + assert tmp.length > 3 : "tmp[2], tmp[3] should exist"; + eventHandler(tmp, isDone); + break; + case "A": + assert tmp.length > 3 : "tmp[2], tmp[3] should exist"; + doAfterHandler(tmp, isDone); + break; + default: + throw new RuntimeException("Corrupted data in data file at " + + filePath); + } + } + fileReader.close(); + } + + /** + * Suppresses any unused warnings from a given boolean result. + * + * @param result variable to suppress warnings from + */ + //@@author Eric Lange-reused + //Reused from https://stackoverflow.com/questions/27904329/warning-file-mkdir-is-ignored#answer-54341862 + // with minor modifications + @SuppressWarnings("unused") + private static void ignoreResult(boolean result) { + + } + //@@author + + /** + * Adds Todo to tasks. + * + * @param arr array of data [Type, isDone, description] + * @param isDone bool indicating if Todo is marked complete + */ + public void todoHandler(String[] arr, Boolean isDone) { + Todo t = new Todo(arr[2].trim()); + if (isDone) { + t.markComplete(); + assert t.getStatusIcon().equals("\u2713") : "task should be complete"; + } + tasks.add(t); + } + + /** + * Adds Deadline to tasks. + * + * @param arr array of data [Type, isDone, description, dateTime] + * @param isDone bool indicating if Deadline is marked complete + */ + public void deadlineHandler(String[] arr, Boolean isDone) { + Deadline d = new Deadline(arr[2].trim(), arr[3].trim()); + if (isDone) { + d.markComplete(); + assert d.getStatusIcon().equals("\u2713") : "task should be complete"; + } + tasks.add(d); + } + + /** + * Adds Event to tasks. + * + * @param arr array of data [Type, isDone, description, dateTime] + * @param isDone bool indicating if Deadline is marked complete + */ + public void eventHandler(String[] arr, Boolean isDone) { + Event e = new Event(arr[2].trim(), arr[3].trim()); + if (isDone) { + e.markComplete(); + assert e.getStatusIcon().equals("\u2713") : "task should be complete"; + } + tasks.add(e); + } + + /** + * Adds DoAfter to tasks. + * + * @param arr array of data [Type, isDone, description, dateTime] + * @param isDone bool indicating if Deadline is marked complete + */ + public void doAfterHandler(String[] arr, Boolean isDone) { + DoAfter d = new DoAfter(arr[2].trim(), arr[3].trim()); + if (isDone) { + d.markComplete(); + assert d.getStatusIcon().equals("\u2713") : "task should be complete"; + } + tasks.add(d); + } + + /** + * Retrieves and returns Tasks saved from the storage file. + * + * @return ArrayList of saved Tasks + */ + public ArrayList load() { + return tasks; + } + + /** + * Updates the list of Tasks saved on hard-drive at filePath. + * This overwrites the existing file at filePath. + * Note: File creation is to be handled via load() prior this method as + * this assumes that filePath is valid and that the file exists at filePath. + * + * @param tasks TaskList of Tasks to be saved on hard-drive + * @throws IOException if an exception occurs in the saving of + * data to the file at filePath + */ + public void save(TaskList tasks) throws IOException { + File data = new File(filePath); + assert data.exists() : "file was not created at start of program"; + FileWriter f; + + f = new FileWriter(data, false); + boolean isFirst = true; + for (int i = 0; i < tasks.size(); i++) { + String s = isFirst ? "" : "\n"; + f.write(s + tasks.get(i).writeToFile()); + isFirst = false; + } + f.close(); + } +} diff --git a/src/main/java/duke/Ui.java b/src/main/java/duke/Ui.java new file mode 100644 index 0000000000..301aa3a2df --- /dev/null +++ b/src/main/java/duke/Ui.java @@ -0,0 +1,196 @@ +package duke; + +import duke.task.TaskList; + +/** + * Represents a UI class that deals with user input and application output. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class Ui { + + /** + * Returns a welcome message. + * + * @return welcome message. + */ + public static String showWelcome() { + return "Hello, traveller! I'm Paimon.\n" + + "How can I help you today?"; + } + + /** + * Returns a generic error message about failed data loading from a file. + * + * @return error message about data loading errors. + */ + public static String showLoadingError() { + return "An error occurred with processing the data file"; + } + + /** + * Returns a specific error message about some exception in the program. + * + * @param s string describing error + * @return string describing error + */ + public static String showError(String s) { + return s; + } + + /** + * Returns an error handling message for empty strings. + */ + public static String showEmptyMessage() { + return "Paimon cannot read minds!"; + } + + /** + * Returns string of list of tasks and number of tasks. + * + * @param tasks current TaskList + * @return formatted string of tasks + */ + public static String showListMessage(TaskList tasks) { + StringBuilder str = new StringBuilder(); + str.append("Hmm... Paimon keeps a clear record in her diary.\n") + .append(tasks) + .append("You now have ") + .append(tasks.size()) + .append(" task") + .append(tasks.size() == 1 ? "" : "s") + .append(" on your list."); + + return str.toString(); + } + + /** + * Returns message for task being marked incomplete. + * + * @return string stating that task is updated. + */ + public static String showUndoMessage() { + return "...ah. The task has been updated accordingly."; + } + + /** + * Returns message for task being completed. + * + * @return string stating that task is completed. + */ + public static String showDoMessage() { + return "Great job! The task has been updated accordingly."; + } + + /** + * Returns task deletion message and resulting list of tasks. + * + * @param tasks TaskList of tasks + * @return string of list of tasks and deletion message. + */ + public static String showDeleteMessage(TaskList tasks) { + StringBuilder str = new StringBuilder(); + + str.append("Noted, the task has been scrubbed off the list!\n") + .append(tasks) + .append("You now have ") + .append(tasks.size()) + .append(" task") + .append(tasks.size() == 1 ? "" : "s") + .append(" on your list."); + + return str.toString(); + } + + /** + * Returns message for the addition of a Todo. + * + * @return string stating that a Todo task has been added. + */ + public static String showTodoMessage() { + return "Got it! I have noted down the following task " + + "in your list.\n"; + } + + /** + * Returns generic message for finding tasks. + * + * @return string stating that matching tasks are found. + */ + public static String showFindMessage(TaskList foundTasks) { + if (foundTasks.size() == 0) { + return "Paimon could not find any matching tasks."; + } + StringBuilder str = new StringBuilder(); + + str.append("Here are the matching tasks in your list:\n") + .append(foundTasks) + .append("There ") + .append(foundTasks.size() == 1 ? "is " : "are ") + .append(foundTasks.size()) + .append(" matching task") + .append(foundTasks.size() == 1 ? "" : "s") + .append(" on your list."); + + return str.toString(); + } + + /** + * Returns message for the addition of a Deadline task. + * + * @return string stating that a Deadline task has been added. + */ + public static String showDeadlineMessage() { + return "Got it! I have noted down the following task in" + + " your list. \nRemember the deadline!\n"; + } + + /** + * Returns message for the addition of an Event task. + * + * @return string stating that an Event task has been added. + */ + public static String showEventMessage() { + return "Got it! I have noted down the following task in" + + " your list.\nDo be there on time!\n"; + } + + /** + * Returns message for the addition of a DoAfter task. + * + * @return string stating that a DoAfter task has been added. + */ + public static String showDoAfterMessage() { + return "Got it! I have noted down the following task in" + + " your list.\nDon't forget it!\n"; + } + + /** + * Returns message for the end of the program. + * + * @return string to end program. + */ + public static String showByeMessage() { + return "Bye, hope to see you again soon!"; + } + + /** + * Returns generic message for indecipherable input. + * + * @return generic message about unclear input. + */ + public static String showDefaultMessage() { + return "That went over Paimon's head a little..."; + } + + /** + * Returns generic message for incorrect number input. + * + * @return warning message about number input. + */ + public static String showNumberFormatMessage() { + return "That went over Paimon's head a little...\n" + + "You need to type a number instead."; + } +} diff --git a/src/main/java/duke/gui/DialogBox.java b/src/main/java/duke/gui/DialogBox.java new file mode 100644 index 0000000000..14a0c782ef --- /dev/null +++ b/src/main/java/duke/gui/DialogBox.java @@ -0,0 +1,66 @@ +package duke.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.shape.Circle; + +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); + + double x = displayPicture.getX() + displayPicture.getFitWidth() / 2; + double y = displayPicture.getY() + displayPicture.getFitHeight() / 2; + double radius = Math.min(displayPicture.getFitWidth(), + displayPicture.getFitHeight()) / 2; + displayPicture.setClip(new Circle(x, y, radius)); + } + + /** + * 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; + } +} diff --git a/src/main/java/duke/gui/Launcher.java b/src/main/java/duke/gui/Launcher.java new file mode 100644 index 0000000000..57842f4834 --- /dev/null +++ b/src/main/java/duke/gui/Launcher.java @@ -0,0 +1,14 @@ +package duke.gui; + +import duke.Main; + +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/duke/gui/MainWindow.java b/src/main/java/duke/gui/MainWindow.java new file mode 100644 index 0000000000..98ffb673ee --- /dev/null +++ b/src/main/java/duke/gui/MainWindow.java @@ -0,0 +1,129 @@ +package duke.gui; + +import duke.Duke; +import duke.Expression; +import duke.Response; +import duke.Ui; + +import javafx.animation.PauseTransition; +import javafx.application.Platform; + +import javafx.fxml.FXML; + +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import javafx.util.Duration; + +/** + * Controller for MainWindow. Provides the layout for the other controls. + * Images used in this application are sourced from sticker packs from + * https://getstickerpack.com/stickers/genshin-impact-aether and + * https://getstickerpack.com/stickers/genshin-impact-paimon-1 by U-KEY. + * + */ +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/User.png")); + private Image dukeImage = new Image(this.getClass().getResourceAsStream("/images/DukeDefault.png")); + + /** + * Initializes the main window. + */ + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + showWelcomeMessage(); + } + + 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(); + Response r = duke.getResponse(input); + + String response = r.getMessage(); + Expression expression = r.getExpression(); + + dukeImage = getDukeImage(expression); + + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + + if (r.isExit()) { + PauseTransition end = new PauseTransition(Duration.seconds(1)); + end.setOnFinished(a -> endProgram()); + end.play(); + } + } + + /** + * Exit program. + */ + private void endProgram() { + Platform.exit(); + System.exit(0); + } + + /** + * Displays welcome message. + */ + private void showWelcomeMessage() { + String response = Ui.showWelcome(); + + dialogContainer.getChildren().addAll( + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + } + + /** + * Gets Duke's image depending on its expression. + * Idea is adapted from kev-intq(@github)'s GUI. + * + * @param expression Duke's expression + * @return image with expression. + */ + private Image getDukeImage(Expression expression) { + String filePath = "/images/"; + + switch(expression) { + case DISAPPOINTED: + filePath += "DukeDisappointed.png"; + break; + case HAPPY: + filePath += "DukeHappy.png"; + break; + case THUMBSUP: + filePath += "DukeThumbsUp.png"; + break; + default: + filePath += "DukeDefault.png"; + break; + } + + return new Image(this.getClass().getResourceAsStream(filePath)); + } +} diff --git a/src/main/java/duke/task/Deadline.java b/src/main/java/duke/task/Deadline.java new file mode 100644 index 0000000000..35903aae5d --- /dev/null +++ b/src/main/java/duke/task/Deadline.java @@ -0,0 +1,47 @@ +package duke.task; + +/** + * Represents a task that occurs at a particular date and/or time. + * Note: the allowable formats for date and time to be interpreted + * correctly by Deadline is YYYY/MM/DD (with ./| being valid separators) + * and HHMM. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class Deadline extends TaskWithDateTime { + + /** + * Constructor for Deadline specifying description, dateTime string. + * + * @param description description of Deadline + * @param by dateTime string associated with Deadline + */ + public Deadline(String description, String by) { + super(description, by); + } + + /** + * Default toString method that returns formatted Deadline. + * + * @return formatted string of the description, dateTime and completeness + * status of Deadline with a Deadline marker. + */ + @Override + public String toString() { + String s = "[D]" + super.toString(); + return s.replace(" (at: ", " (by: "); + } + + /** + * Parses contents of Deadline into a csv-like format. + * Delimiter is '|'. + * + * @return formatted string of Deadline, its dateTime, completion status + * and a Deadline marker. + */ + @Override + public String writeToFile() { + return "D | " + super.writeToFile(); + } +} diff --git a/src/main/java/duke/task/DoAfter.java b/src/main/java/duke/task/DoAfter.java new file mode 100644 index 0000000000..715c86eb94 --- /dev/null +++ b/src/main/java/duke/task/DoAfter.java @@ -0,0 +1,47 @@ +package duke.task; + +/** + * Represents a task that occurs at a particular date and/or time. + * Note: the allowable formats for date and time to be interpreted + * correctly by Deadline is YYYY/MM/DD (with ./| being valid separators) + * and HHMM. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class DoAfter extends TaskWithDateTime { + + /** + * Constructor for DoAfter specifying description, dateTime string. + * + * @param description description of Deadline + * @param after dateTime string associated with Deadline + */ + public DoAfter(String description, String after) { + super(description, after); + } + + /** + * Default toString method that returns formatted Deadline. + * + * @return formatted string of the description, dateTime and completeness + * status of Deadline with a Deadline marker. + */ + @Override + public String toString() { + String s = "[A]" + super.toString(); + return s.replace(" (at: ", " (after: "); + } + + /** + * Parses contents of Deadline into a csv-like format. + * Delimiter is '|'. + * + * @return formatted string of Deadline, its dateTime, completion status + * and a Deadline marker. + */ + @Override + public String writeToFile() { + return "A | " + super.writeToFile(); + } +} diff --git a/src/main/java/duke/task/Event.java b/src/main/java/duke/task/Event.java new file mode 100644 index 0000000000..fe3a69a187 --- /dev/null +++ b/src/main/java/duke/task/Event.java @@ -0,0 +1,45 @@ +package duke.task; + +/** + * Represents a task that occurs at a particular date and/or time. + * Note: the allowable formats for date and time to be interpreted correctly + * by Event is YYYY/MM/DD (with * ./| being valid separators) and HHMM. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class Event extends TaskWithDateTime { + + /** + * Constructor for Event specifying description, dateTime string. + * + * @param description description of Event + * @param at dateTime string associated with Event + */ + public Event(String description, String at) { + super(description, at); + } + + /** + * Default toString method that returns formatted Event. + * + * @return formatted string of the description, dateTime and completeness + * status of Event with an Event marker. + */ + @Override + public String toString() { + return "[E]" + super.toString(); + } + + /** + * Parses contents of Event into a csv-like format. + * Delimiter is '|'. + * + * @return formatted string of Event, its dateTime, completion status + * and an Event marker. + */ + @Override + public String writeToFile() { + return "E | " + super.writeToFile(); + } +} diff --git a/src/main/java/duke/task/Task.java b/src/main/java/duke/task/Task.java new file mode 100644 index 0000000000..5c82142abd --- /dev/null +++ b/src/main/java/duke/task/Task.java @@ -0,0 +1,67 @@ +package duke.task; + +/** + * Represents a task and consists of its description and completeness status. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class Task { + private String description; + private boolean isDone; + + /** + * Constructor for Task specifying description. + * + * @param description description of task + */ + public Task(String description) { + this.description = description; + this.isDone = false; + } + + /** + * Marks Task as complete. + */ + public void markComplete() { + this.isDone = true; + } + + /** + * Marks Task as incomplete. + * Note: this is true by default for all Task objects. + */ + public void markIncomplete() { + this.isDone = false; + } + + /** + * Returns the state of completeness of Task. + * + * @return "X" if Task is completed, " " otherwise. + */ + public String getStatusIcon() { + return (isDone ? "\u2713" : " "); + } + + /** + * Default toString method that returns the description of Task. + * Includes completion status. + * + * @return formatted string of the description and completeness status + * of Task. + */ + public String toString() { + return "[" + this.getStatusIcon() + "] " + description; + } + + /** + * Parses contents of Task into a csv-like format delimited by '|'. + * + * @return formatted string of Task and its completion status. + */ + public String writeToFile() { + String s = (isDone) ? "D" : "N"; + return s + " | " + description; + } +} diff --git a/src/main/java/duke/task/TaskList.java b/src/main/java/duke/task/TaskList.java new file mode 100644 index 0000000000..56183f8b0b --- /dev/null +++ b/src/main/java/duke/task/TaskList.java @@ -0,0 +1,112 @@ +package duke.task; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a collection of tasks. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class TaskList { + private List tasks; + + /** + * Empty constructor for TaskList. + */ + public TaskList() { + this.tasks = new ArrayList<>(); + } + + /** + * Constructor for TaskList specifying a list of Tasks. + * + * @param tasks list of Tasks to be initialized + */ + public TaskList(List tasks) { + this.tasks = new ArrayList<>(); + this.tasks.addAll(tasks); + } + + /** + * Returns the length of TaskList. + * + * @return number of elements in tasks. + */ + public int size() { + return tasks.size(); + } + + /** + * Retrieves and returns Task at a given index in TaskList. + * + * @param i index of Task to be retrieved in TaskList + * @return Task at a given index in tasks. + */ + public Task get(int i) { + if (i < size() && i >= 0) { + return tasks.get(i); + } + return null; + } + + /** + * Deletes Task at a given index in TaskList. + * + * @param i index of task to be removed + */ + public void remove(int i) { + if (i < size() && i >= 0) { + tasks.remove(i); + } + } + + /** + * Appends Task to end of TaskList. + * + * @param t Task to be added + */ + public void add(Task t) { + tasks.add(t); + } + + /** + * Returns matching occurrences of a given word in tasks. + * + * @param word keyword to be searched for + * @return TaskList of the Tasks containing the specified word + */ + public TaskList find(String word) { + ArrayList matchingTasks = new ArrayList<>(); + + for (Task task: tasks) { + if (task.toString().contains(word)) { + matchingTasks.add(task); + } + } + + return new TaskList(matchingTasks); + } + + /** + * Default toString method that returns list of all tasks. + * + * @return formatted string of all Tasks in TaskList delimited by newlines. + */ + public String toString() { + StringBuilder str = new StringBuilder(); + + for (int i = 0; i < tasks.size(); i++) { + int index = i + 1; + + str.append(" ") + .append(index) + .append(". ") + .append(tasks.get(i)) + .append("\n"); + } + + return str.toString(); + } +} diff --git a/src/main/java/duke/task/TaskWithDateTime.java b/src/main/java/duke/task/TaskWithDateTime.java new file mode 100644 index 0000000000..08e56e375f --- /dev/null +++ b/src/main/java/duke/task/TaskWithDateTime.java @@ -0,0 +1,120 @@ +package duke.task; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents a task with an associated date and/or time. + * + * Note: the allowable formats for date and time to be interpreted correctly + * by TaskWithDateTime is YYYY/MM/DD (with ./| being valid separators) and HHMM. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class TaskWithDateTime extends Task { + private String dateTime; + private String originalDateTime; + + /** + * Constructor for TaskWithDateTime. + * Specifies description and dateTime. + * + * @param description description of task + * @param dateTime datetime associated with task in string format + */ + public TaskWithDateTime(String description, String dateTime) { + super(description); + + dateTime = dateTime.trim(); + this.dateTime = dateTime; + this.originalDateTime = dateTime; + + extractTime(extractDate()); + } + + /** + * Formats date in dateTime correctly (if any). + * Dates detected are of the format YYYYMMDD delimited by -|/. + * and are changed to MMM d yyyy format. + * + * @return string with date removed. + */ + public String extractDate() { + String regexDate = "\\d{4}[-|/.]\\d{2}[-|/.]\\d{2}"; + Matcher m = Pattern.compile(regexDate).matcher(dateTime); + String strWithoutDate = dateTime; + + // If specified date format is found + if (m.find()) { + strWithoutDate = strWithoutDate.replace(m.group(0), ""); + + try { + LocalDate day = LocalDate.parse(m.group(0).replaceAll("[./|]", "-")); + dateTime = dateTime.replace(m.group(0), + day.format(DateTimeFormatter.ofPattern("MMM d yyyy"))); + } catch (DateTimeException e) { + // Set date to the next day if date is invalid + dateTime = dateTime.replace(m.group(0), + LocalDate.now().plus(1, ChronoUnit.DAYS).toString()); + } + } + + return strWithoutDate; + } + + /** + * Formats time in dateTime correctly (if any). + * Time detected is of the format HHMM, and is changed to HH:MM. + * Note: input string must not contain any dates or other four + * contiguous digits. + * + * @param str time string associated with task + */ + public void extractTime(String str) { + String regexTime = "\\d{4}"; + Matcher m = Pattern.compile(regexTime).matcher(str); + + // If specified time format is found + if (m.find()) { + try { + LocalTime timeOfDay = LocalTime.parse(m.group(0).substring(0, 2) + + ":" + m.group(0).substring(2)); + dateTime = dateTime.replace(m.group(0), timeOfDay.toString()); + } catch (DateTimeException e) { + // Set time to an hour from now if time is invalid + dateTime = dateTime.replace(m.group(0), + LocalTime.now().plus(1, ChronoUnit.HOURS).toString()); + } + } + } + + /** + * Default toString method that returns formatted TaskWithDateTime. + * + * @return formatted string of description, dateTime and completion + * status of the TaskWithDateTime object. + */ + @Override + public String toString() { + return super.toString() + " (at: " + dateTime.trim() + ")"; + } + + /** + * Parses contents of TaskWithDateTime into a csv-like format. + * Delimiter is '|'. + * + * @return formatted string of TaskWithDateTime, its completion + * status and associated dateTime. + */ + @Override + public String writeToFile() { + return super.writeToFile() + " | " + originalDateTime; + } +} diff --git a/src/main/java/duke/task/Todo.java b/src/main/java/duke/task/Todo.java new file mode 100644 index 0000000000..3dfcbb3130 --- /dev/null +++ b/src/main/java/duke/task/Todo.java @@ -0,0 +1,41 @@ +package duke.task; + +/** + * Represents a todo - a task with a description and no time limit. + * + * @author Elumalai Oviya Dharshini + * @version 1.1 + */ +public class Todo extends Task { + + /** + * Constructor for Todo specifying Task with description. + * + * @param description description of Todo + */ + public Todo(String description) { + super(description); + } + + /** + * Default toString method that returns the description of Todo. + * Includes completion status and Todo marker. + * + * @return formatted string of the description and completeness + * status of Todo with a Todo marker. + */ + @Override + public String toString() { + return "[T]" + super.toString(); + } + + /** + * Parses contents of Todo into a csv-like format delimited by '|'. + * + * @return formatted string of Todo, its completion status and Todo marker + */ + @Override + public String writeToFile() { + return "T | " + super.writeToFile(); + } +} diff --git a/src/main/resources/images/DukeDefault.png b/src/main/resources/images/DukeDefault.png new file mode 100644 index 0000000000..8796aaf73b Binary files /dev/null and b/src/main/resources/images/DukeDefault.png differ diff --git a/src/main/resources/images/DukeDisappointed.png b/src/main/resources/images/DukeDisappointed.png new file mode 100644 index 0000000000..20dd6b004f Binary files /dev/null and b/src/main/resources/images/DukeDisappointed.png differ diff --git a/src/main/resources/images/DukeThumbsUp.png b/src/main/resources/images/DukeThumbsUp.png new file mode 100644 index 0000000000..36caaf14c9 Binary files /dev/null and b/src/main/resources/images/DukeThumbsUp.png differ diff --git a/src/main/resources/images/Dukehappy.png b/src/main/resources/images/Dukehappy.png new file mode 100644 index 0000000000..5a7fb7d7ec Binary files /dev/null and b/src/main/resources/images/Dukehappy.png differ diff --git a/src/main/resources/images/Icon.png b/src/main/resources/images/Icon.png new file mode 100644 index 0000000000..eb8311a19e Binary files /dev/null and b/src/main/resources/images/Icon.png differ diff --git a/src/main/resources/images/User.png b/src/main/resources/images/User.png new file mode 100644 index 0000000000..b7ddb51276 Binary files /dev/null and b/src/main/resources/images/User.png differ diff --git a/src/main/resources/style.css b/src/main/resources/style.css new file mode 100644 index 0000000000..e7f14eaeae --- /dev/null +++ b/src/main/resources/style.css @@ -0,0 +1,11 @@ +.label { + -fx-font-family: 'Open Sans', sans-serif; + -fx-font-size: 12; + -fx-border-radius: 5 5 5 5; + -fx-background-radius: 5 5 5 5; + -fx-background-color: #e8e8e8; + + -fx-padding: 10px; + -fx-border-insets: 5px; + -fx-background-insets: 5px; +} \ No newline at end of file diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..e7fb9db365 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ 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..8c26537855 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + +