diff --git a/.gitignore b/.gitignore index f69985ef1f..ee65ab0d27 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.txt text-ui-test/EXPECTED-UNIX.TXT +/data/tasks.txt +/data/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..db380655f4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '5.1.0' + id 'checkstyle' +} + +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 = "drake.Drake" +} + +shadowJar { + archiveBaseName = "drake" + archiveClassifier = null +} + +run { + standardInput = System.in + enableAssertions = true +} + +checkstyle { + toolVersion = '10.2' +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..fb88cedfc2 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..135ea49ee0 --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/data/tasks.txt b/data/tasks.txt new file mode 100644 index 0000000000..76095bd517 --- /dev/null +++ b/data/tasks.txt @@ -0,0 +1,4 @@ +E;NUS Literary Society Book Exchange; ;2022-09-18 +W;Boyfriend's wedding; ;2022-12-14;2022-12-21 +T;Ask girlfriend's bestfriend out ; +D;Finish Week 6 iP; ;2022-09-16 diff --git a/docs/README.md b/docs/README.md index 8077118ebe..485030190b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,100 @@ # User Guide -## Features +## Features -### Feature-ABC +### Create tasks -Description of the feature. +In Drake, you can create 4 different types of tasks. -### Feature-XYZ +- ✨ _Todo_ ✨ tasks that do not have any associated dates +- ✨ _Deadline_ ✨ tasks that have an associated deadline +- ✨ _Event_ ✨ events that occur at a specific date +- ✨ _Within_ ✨ events that occur between two specific dates -Description of the feature. +### Mark a task as done + +Once you have completed a task, mark it as completed. 🤯 + +### Delete a task + +Delete a task that you no longer want to track from the list. ❌ + +### Show all tasks + +See all your tasks (and their associated task numbers) in a numbered list. 👀 + +### Search for a specific task by its name or date + +Search for tasks using their names or dates (if they have dates) as search filters. 🧐 + +### Tasks don't disappear + +Drake remembers your tasks! Tasks are automatically saved and retrieved when you open the app again. 🧠 ## Usage -### `Keyword` - Describe action +### `list` - Displays all tasks + +Displays all tasks you are currently tracking. + +Format: `list` + +### `find` - Displays tasks with matching names + +Displays tasks that match the search string. + +Format: `find ...` + +### `todo` - Creates a new Todo type task + +Creates a new Todo and adds it to the current list. + +Format: `todo ` + +### `deadline` - Creates a new Deadline type task + +Creates a new Deadline and adds it to the current list. The deadline must be in `YYYY-MM-dd` format. + +Format: `deadline /by ` + +Example: `deadline return book /by 2021-09-31` + +### `event` - Creates a new Event type task + +Creates a new Event and adds it to the current list. The deadline must be in `YYYY-MM-dd` format. + +Format: `event /at ` + +Example: `event NUS Literary Society Book Club and Exchange /at 2022-09-18` + +### `within` - Creates a new Within type task + +Creates a new Within task and adds it to the current list. The date range must be in `YYYY-MM-dd` format. + +Format: `within /range ` + +Example: `within Recess Week /range 2022-09-18 2022-09-25` + +### `mark` - Marks a task as completed + +Marks a task as completed. + +Format: `mark ` + +### `unmark` - Marks a task as not completed + +Marks a task as not completed. + +Format: `unmark ` -Describe the action and its outcome. +### `delete` - Deletes a task -Example of usage: +Removes a task from the list. -`keyword (optional arguments)` +Format: `delete ` -Expected outcome: +### `bye` - Closes the application -Description of the outcome. +Exits the app. -``` -expected output -``` +Format: `bye` \ No newline at end of file diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..7e6fcb19f4 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/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/drake/Drake.java b/src/main/java/drake/Drake.java new file mode 100644 index 0000000000..ae3ef76b8c --- /dev/null +++ b/src/main/java/drake/Drake.java @@ -0,0 +1,27 @@ +package drake; + +import java.io.IOException; + +import drake.gui.Main; +import javafx.application.Application; + +/** + * Entrypoint for the Drake to-do list chatbot. + */ +public class Drake { + + private Drake() throws IOException, DrakeException { + Storage storage = new Storage(); + new TaskList(storage.fileToList()); + } + + /** + * Entrypoint for app. + * + * @param args Command-line arguments. + */ + public static void main(String[] args) { + Application.launch(Main.class, args); + } + +} diff --git a/src/main/java/drake/DrakeException.java b/src/main/java/drake/DrakeException.java new file mode 100644 index 0000000000..1995ed9c80 --- /dev/null +++ b/src/main/java/drake/DrakeException.java @@ -0,0 +1,16 @@ +package drake; + +/** + * An exception with user-facing messages that sound like Drake. + */ +public class DrakeException extends Exception { + + /** + * Constructor + * + * @param errorMessage User-facing error message that sound like Drake. + */ + public DrakeException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/drake/EmptyDescriptionException.java b/src/main/java/drake/EmptyDescriptionException.java new file mode 100644 index 0000000000..5922c8b174 --- /dev/null +++ b/src/main/java/drake/EmptyDescriptionException.java @@ -0,0 +1,14 @@ +package drake; + +/** + * An exception for tasks without a description with a user-facing message that sound like Drake. + */ +public class EmptyDescriptionException extends DrakeException { + + /** + * Constructor + */ + public EmptyDescriptionException() { + super("Well well well! A task without a description is like a picture of Singapore without MBS"); + } +} diff --git a/src/main/java/drake/IncompatibleCommandException.java b/src/main/java/drake/IncompatibleCommandException.java new file mode 100644 index 0000000000..8da6dbdbee --- /dev/null +++ b/src/main/java/drake/IncompatibleCommandException.java @@ -0,0 +1,16 @@ +package drake; + +/** + * An exception for incompatible commands with user-facing messages that sound like Drake. + */ +public class IncompatibleCommandException extends DrakeException { + + /** + * Constructor. + * + * @param error Customised error message for the incompatible commands. + */ + public IncompatibleCommandException(String error) { + super("These words you just typed go together like oil mixes with water. " + error); + } +} diff --git a/src/main/java/drake/InvalidTaskNumberException.java b/src/main/java/drake/InvalidTaskNumberException.java new file mode 100644 index 0000000000..252cad49ca --- /dev/null +++ b/src/main/java/drake/InvalidTaskNumberException.java @@ -0,0 +1,14 @@ +package drake; + +/** + * An exception for an invalid task number with user-facing messages that sound like Drake. + */ +public class InvalidTaskNumberException extends IncompatibleCommandException { + + /** + * Constructor + */ + public InvalidTaskNumberException() { + super("That task number doesn't exist!"); + } +} diff --git a/src/main/java/drake/Parser.java b/src/main/java/drake/Parser.java new file mode 100644 index 0000000000..cdf9a016e7 --- /dev/null +++ b/src/main/java/drake/Parser.java @@ -0,0 +1,63 @@ +package drake; + +import drake.commands.ByeCommand; +import drake.commands.Command; +import drake.commands.DeadlineCommand; +import drake.commands.DeleteCommand; +import drake.commands.EventCommand; +import drake.commands.FindCommand; +import drake.commands.ListCommand; +import drake.commands.MarkCommand; +import drake.commands.TodoCommand; +import drake.commands.UnmarkCommand; +import drake.commands.WithinCommand; + +/** + * Command parser. + */ +public class Parser { + + /** + * Parses and executes the given input using the given module instances. + * @param fullInput The input given to the bot. + * @return The Command parsed from the user input. + */ + public static Command parse(String fullInput) throws UnknownCommandException, + IncompatibleCommandException, EmptyDescriptionException { + int firstSpace = fullInput.indexOf(' '); + String commandText; + if (fullInput.matches("list|bye")) { + commandText = fullInput; + } else if (firstSpace == -1 && fullInput.matches("deadline|event|todo")) { + throw new EmptyDescriptionException(); + } else if (firstSpace == -1) { + throw new UnknownCommandException(); + } else { + commandText = fullInput.substring(0, firstSpace); + } + switch (commandText) { + case "list": + return new ListCommand(); + case "mark": + return new MarkCommand(fullInput); + case "unmark": + return new UnmarkCommand(fullInput); + case "todo": + return new TodoCommand(fullInput); + case "deadline": + return new DeadlineCommand(fullInput); + case "event": + return new EventCommand(fullInput); + case "delete": + return new DeleteCommand(fullInput); + case "bye": + return new ByeCommand(); + case "find": + return new FindCommand(fullInput); + case "within": + return new WithinCommand(fullInput); + default: + throw new UnknownCommandException(); + } + } +} diff --git a/src/main/java/drake/Storage.java b/src/main/java/drake/Storage.java new file mode 100644 index 0000000000..549ba54677 --- /dev/null +++ b/src/main/java/drake/Storage.java @@ -0,0 +1,168 @@ +package drake; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +import drake.commands.CommandType; +import drake.tasks.Deadline; +import drake.tasks.DoWithinPeriod; +import drake.tasks.Event; +import drake.tasks.Task; +import drake.tasks.Todo; + +/** + * Task list saving and loading functionalities. + */ +public class Storage { + + private static final String TASK_FILE_PATH = "data/tasks.txt"; + private static final String TASK_FILE_DIR = "data"; + private final List> tasks; + private final File taskFile; + + /** + * Constructor using the default savefile location. + * + */ + public Storage() throws IOException, DrakeException { + taskFile = new File(TASK_FILE_PATH); + tasks = new ArrayList<>(); + addTasks(fileToList()); + } + + /** + * Reads tasks from the task file into a list of Tasks. + * + * @return A list of Tasks present in the task file. + */ + public List fileToList() throws UnknownCommandException { + ArrayList list = new ArrayList<>(); + Scanner fileReader; + try { + fileReader = new Scanner(taskFile); + } catch (FileNotFoundException e) { + return list; + } + while (fileReader.hasNext()) { + String[] taskParts = fileReader.nextLine().split(";"); + switch (taskParts[0]) { + case "D": + Deadline deadline = new Deadline(taskParts[1], taskParts[3]); + if (taskParts[2].equals("X")) { + deadline.markAsDone(); + } + list.add(deadline); + break; + case "T": + Task task = new Todo(taskParts[1]); + if (taskParts[2].equals("X")) { + task.markAsDone(); + } + list.add(task); + break; + case "E": + Event event = new Event(taskParts[1], taskParts[3]); + if (taskParts[2].equals("X")) { + event.markAsDone(); + } + list.add(event); + break; + case "W": + DoWithinPeriod doWithinPeriod = new DoWithinPeriod(taskParts[1], taskParts[3], taskParts[4]); + if (taskParts[2].equals("X")) { + doWithinPeriod.markAsDone(); + } + list.add(doWithinPeriod); + break; + default: + throw new UnknownCommandException(); + } + } + return list; + } + + /** + * Updates the task in the task list. + * + * @param taskNumber The task number of the task to update. + * @param command The update demanded by the user. + * @throws DrakeException when saving fails. + */ + public void updateTask(int taskNumber, CommandType command) throws DrakeException { + switch (command) { + case MARK: + tasks.get(taskNumber - 1).set(2, "X"); + break; + + case UNMARK: + tasks.get(taskNumber - 1).set(2, " "); + break; + + case DELETE: + tasks.remove(taskNumber - 1); + break; + default: + throw new UnknownCommandException(); + } + + updateFile(); + } + + /** + * Updates the task file to the current state of the task list. + * + * @throws DrakeException when saving fails + */ + private void updateFile() throws DrakeException { + //Inspired by parnikkapore's PR + try { + File fileDir = new File(TASK_FILE_DIR); + if (!fileDir.isDirectory() && !fileDir.mkdirs()) { + throw new DrakeException("Higher powers taking a hold on me... I cannot save the task list."); + } + + FileWriter fileWriter = new FileWriter(TASK_FILE_PATH); + for (List task : tasks) { + fileWriter.write(listToCsv(task)); + } + fileWriter.close(); + } catch (IOException e) { + throw new DrakeException( + "Higher powers taking a hold on me... I cannot save the task list. This might help: " + e); + } + } + + /** + * Adds a task to the task file. + * + * @param addedTask The task to be added to the file. + * @throws DrakeException when saving fails. + */ + public void addTask(Task addedTask) throws DrakeException { + tasks.add(addedTask.toList()); + updateFile(); + } + + private void addTasks(List addedTasks) throws DrakeException { + for (Task addedTask : addedTasks) { + addTask(addedTask); + } + updateFile(); + } + + private String listToCsv(List list) { + StringBuilder csv = new StringBuilder(list.get(0)).append(";"); + for (int i = 1; i < list.size(); i++) { + csv.append(list.get(i)); + if (i != list.size() - 1) { + csv.append(";"); + } + } + return csv.append("\n").toString(); + } +} diff --git a/src/main/java/drake/TaskList.java b/src/main/java/drake/TaskList.java new file mode 100644 index 0000000000..2532489ce1 --- /dev/null +++ b/src/main/java/drake/TaskList.java @@ -0,0 +1,114 @@ +package drake; + +import java.util.ArrayList; +import java.util.List; + +import drake.tasks.Task; + +/** + * Represents a list of tasks. + */ +public class TaskList { + + private final List list; + + /** + * Constructor. Loads the task list from a list of Tasks. + * + * @param list The initial task list. + */ + public TaskList(List list) { + this.list = list; + } + + /** + * Constructor. Initialises the task list with an empty list. + */ + public TaskList() { + list = new ArrayList<>(); + } + + /** + * Checks if the given task number is valid. + * + * @param taskNumber The given task number. + * @return Whether the task number is valid. + */ + public boolean isValidTaskNumber(int taskNumber) { + return taskNumber >= 1 && taskNumber <= list.size(); + } + + /** + * Marks the task with the given task number as done. + * + * @param taskNumber The given task number. + */ + public void markAsDone(int taskNumber) { + list.get(taskNumber - 1).markAsDone(); + } + + /** + * Gets the String representation of the task with the given task number. + * + * @param taskNumber The given task number. + * @return The String representation of the requested task. + */ + public String getTaskToString(int taskNumber) { + return list.get(taskNumber - 1).toString(); + } + + /** + * Marks the task with the given task number as not done. + * + * @param taskNumber The given task number. + */ + public void markAsNotDone(int taskNumber) { + list.get(taskNumber - 1).markAsNotDone(); + } + + /** + * Removes the task with the given task number from the task list. + * + * @param taskNumber The given task number. + */ + public void removeTask(int taskNumber) { + list.remove(taskNumber - 1); + } + + /** + * Gets the String representation of the size of the task list in a sentence. + * + * @return A sentence with the size of the task list. + */ + public String getSizeToString() { + return "You now have " + list.size() + " tasks in the list"; + } + + /** + * Adds the given task to the task list. + * + * @param task The given task. + * @return The task added to the task list. + */ + public Task addTask(Task task) { + list.add(task); + return task; + } + + /** + * Filters the task list and returns the tasks that match the given search keywords. + * @param searchKeywords The given search keywords. + * @return The tasks that match the given search keywords. + */ + public TaskList filter(List searchKeywords) { + TaskList result = new TaskList(new ArrayList<>()); + + for (Task task : list) { + if (task.isMatch(searchKeywords)) { + result.addTask(task); + } + } + + return result; + } +} diff --git a/src/main/java/drake/Ui.java b/src/main/java/drake/Ui.java new file mode 100644 index 0000000000..8490921d2e --- /dev/null +++ b/src/main/java/drake/Ui.java @@ -0,0 +1,98 @@ +package drake; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * Interact with the user. + */ +public class Ui { + private static final String DASH = "------------------------------------------------------"; + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_RED = "\u001B[31m"; + private final Scanner sc; + + /** + * Constructor. + */ + public Ui() { + this.sc = new Scanner(System.in); + } + + /** + * Format a line of text according to the format for the chat bubble on the GUI. + * Do not use \n for multiline text - use the list version. + * + * @param text The text to output. + */ + public List chatBubbleText(String text) { + return chatBubbleText(List.of(text)); + } + + /** + * Format some multiline text according to the format for the chat bubble on the GUI. + * + * @param lines A list of lines of text to output. + */ + public List chatBubbleText(List lines) { + ArrayList result = new ArrayList<>(); + for (String line : lines) { + result.add(" " + line); + } + return result; + } + + /** + * Greets the user by printing the welcome message. + */ + public List replyWelcome() { + ArrayList reply = new ArrayList<>(); + reply.add("You used to call me on my cellphone \uD83D\uDE14"); + reply.add("Drake's the kind of guy to help you out uwu"); + reply.add("Go ahead, make that hotline bling ☎"); + return reply; + } + + /** + * Reads input from the console into a String. + * + * @return The trimmed input line as a String. + */ + public String readInput() { + return sc.nextLine().trim(); + } + + /** + * Prints the given line into the console. + * + * @param line The line to print. + */ + public void printLine(Object line) { + System.out.println(line); + } + + /** + * Prints the exit message. + * + */ + public String replyBye() { + return "I'm down for you always. See you ❤"; + } + + /** + * Prints a dash. + */ + public void printDash() { + System.out.println(DASH); + } + + /** + * Prints the given error message with special formatting. + * + * @param errorMessage The given error message. + */ + public void printError(String errorMessage) { + System.out.println(ANSI_RED + errorMessage + ANSI_RESET); + } +} diff --git a/src/main/java/drake/UnknownCommandException.java b/src/main/java/drake/UnknownCommandException.java new file mode 100644 index 0000000000..212c08372d --- /dev/null +++ b/src/main/java/drake/UnknownCommandException.java @@ -0,0 +1,14 @@ +package drake; + +/** + * An exception for when an unknown command is entered with user-facing messages that sound like Drake. + */ +public class UnknownCommandException extends DrakeException { + + /** + * Constructor with the Drake-sounding exception for an unknown command. + */ + public UnknownCommandException() { + super("Uh oh spaghettios I don't know what that means!"); + } +} diff --git a/src/main/java/drake/commands/ByeCommand.java b/src/main/java/drake/commands/ByeCommand.java new file mode 100644 index 0000000000..b76d6601b1 --- /dev/null +++ b/src/main/java/drake/commands/ByeCommand.java @@ -0,0 +1,24 @@ +package drake.commands; + +import java.io.IOException; +import java.util.List; + +import drake.DrakeException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; + +/** + * The bye command. + */ +public class ByeCommand extends Command { + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws IOException, DrakeException { + return List.of(ui.replyBye()); + } + + @Override + public boolean isExit() { + return true; + } +} diff --git a/src/main/java/drake/commands/Command.java b/src/main/java/drake/commands/Command.java new file mode 100644 index 0000000000..c5691123b5 --- /dev/null +++ b/src/main/java/drake/commands/Command.java @@ -0,0 +1,36 @@ +package drake.commands; + +import java.io.IOException; +import java.util.List; + +import drake.DrakeException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; + +/** + * Represents a command given by the user. + */ +public abstract class Command { + + /** + * Executes the command. + * + * @param tasks The task list before the command is executed. + * @param ui Gives access to the UI of the program. + * @param storage Gives access to local storage. + * @return The list of replies + * @throws IOException when there is an issue with the IO. + * @throws DrakeException when there is inappropriate input or save file issues. + */ + public abstract List execute(TaskList tasks, Ui ui, Storage storage) throws IOException, DrakeException; + + /** + * Checks whether the user has given the bye command. + * + * @return True if the user has given the bye command, false otherwise. + */ + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/drake/commands/CommandType.java b/src/main/java/drake/commands/CommandType.java new file mode 100644 index 0000000000..73a58b540d --- /dev/null +++ b/src/main/java/drake/commands/CommandType.java @@ -0,0 +1,10 @@ +package drake.commands; + +/** + * Types of commands to update tasks. + */ +public enum CommandType { + MARK, + UNMARK, + DELETE, +} diff --git a/src/main/java/drake/commands/CreateTaskCommand.java b/src/main/java/drake/commands/CreateTaskCommand.java new file mode 100644 index 0000000000..fec2c8839c --- /dev/null +++ b/src/main/java/drake/commands/CreateTaskCommand.java @@ -0,0 +1,41 @@ +package drake.commands; + +import java.io.IOException; +import java.util.List; + +import drake.DrakeException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; + +/** + * Represents a command given by the user to create a new task. + */ +public abstract class CreateTaskCommand extends Command { + + protected String description; + + /** + * Constructor. + * + * @param fullInput The input given by the user. + */ + public CreateTaskCommand(String fullInput) { + description = fullInput.substring(fullInput.indexOf(' ') + 1); + } + + /** + * Executes the command to create a new task, printing the size of the task list after execution. + * + * @param tasks The task list before the command is executed. + * @param ui Gives access to the UI of the program. + * @param storage Gives access to local storage. + * @return The list of replies + * @throws IOException when there is an issue with the IO. + * @throws DrakeException when there is inappropriate input or save file issues. + */ + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws IOException, DrakeException { + return List.of(tasks.getSizeToString()); + } +} diff --git a/src/main/java/drake/commands/DeadlineCommand.java b/src/main/java/drake/commands/DeadlineCommand.java new file mode 100644 index 0000000000..b66e2a0a06 --- /dev/null +++ b/src/main/java/drake/commands/DeadlineCommand.java @@ -0,0 +1,59 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import drake.DrakeException; +import drake.IncompatibleCommandException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; +import drake.tasks.Deadline; +import drake.tasks.Task; + +/** + * Represents a command given by the user to create a new task with a deadline. + */ +public class DeadlineCommand extends CreateTaskCommand { + + private static final Pattern descriptionPattern = Pattern.compile("(?.*) /by (?.*)"); + + /** + * Constructor. + * + * @param fullInput The user input. + */ + public DeadlineCommand(String fullInput) { + super(fullInput); + assert fullInput.startsWith("deadline"); + } + + /** + * Executes the command to create a new deadline task, saving the new task and + * printing the size of the task list after execution. + * + * @param tasks The task list before the command is executed. + * @param ui Gives access to the UI of the program. + * @param storage Gives access to local storage. + * @return The list of replies + * @throws IOException when there is an issue with the IO. + * @throws DrakeException when there is inappropriate input or save file issues. + */ + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws DrakeException, IOException { + ArrayList reply = new ArrayList<>(); + Matcher match = descriptionPattern.matcher(description); + if (!match.matches()) { + throw new IncompatibleCommandException("A deadline task without a deadline?"); + } + reply.add("I've added this task:"); + Task addedTask = tasks.addTask(new Deadline(match.group("taskName"), match.group("by"))); + reply.add(addedTask.toString()); + storage.addTask(addedTask); + reply.addAll(super.execute(tasks, ui, storage)); + return reply; + } +} diff --git a/src/main/java/drake/commands/DeleteCommand.java b/src/main/java/drake/commands/DeleteCommand.java new file mode 100644 index 0000000000..91c66da205 --- /dev/null +++ b/src/main/java/drake/commands/DeleteCommand.java @@ -0,0 +1,36 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import drake.DrakeException; +import drake.IncompatibleCommandException; +import drake.InvalidTaskNumberException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; + +/** + * Represents a delete command. + */ +public class DeleteCommand extends TaskOperationCommand { + + public DeleteCommand(String fullInput) throws IncompatibleCommandException { + super(fullInput); + } + + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws DrakeException, IOException { + if (!tasks.isValidTaskNumber(taskNumber)) { + throw new InvalidTaskNumberException(); + } + + ArrayList reply = new ArrayList<>(); + reply.add("I've removed this task: "); + reply.add(tasks.getTaskToString(taskNumber)); + tasks.removeTask(taskNumber); + storage.updateTask(taskNumber, CommandType.DELETE); + return reply; + } +} diff --git a/src/main/java/drake/commands/EventCommand.java b/src/main/java/drake/commands/EventCommand.java new file mode 100644 index 0000000000..1baec48053 --- /dev/null +++ b/src/main/java/drake/commands/EventCommand.java @@ -0,0 +1,58 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import drake.DrakeException; +import drake.IncompatibleCommandException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; +import drake.tasks.Event; +import drake.tasks.Task; +/** + * Represents a command given by the user to create a new event task. + */ +public class EventCommand extends CreateTaskCommand { + + private static final Pattern descriptionPattern = Pattern.compile("(?.*) /at (?.*)"); + + /** + * Constructor. + * + * @param fullInput The user input. + */ + public EventCommand(String fullInput) { + super(fullInput); + assert fullInput.startsWith("event"); + } + + /** + * Executes the command to create a new event task, saving the new task and + * printing the size of the task list after execution. + * + * @param tasks The task list before the command is executed. + * @param ui Gives access to the UI of the program. + * @param storage Gives access to local storage. + * @return The list of replies + * @throws IOException when there is an issue with the IO. + * @throws DrakeException when there is inappropriate input or save file issues. + */ + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws DrakeException, IOException { + ArrayList reply = new ArrayList<>(); + Matcher match = descriptionPattern.matcher(description); + if (!match.matches()) { + throw new IncompatibleCommandException("An event task without an event time?"); + } + reply.add("I've added this task:"); + Task addedTask = tasks.addTask(new Event(match.group("taskName"), match.group("at"))); + reply.add(addedTask.toString()); + storage.addTask(addedTask); + reply.addAll(super.execute(tasks, ui, storage)); + return reply; + } +} diff --git a/src/main/java/drake/commands/FindCommand.java b/src/main/java/drake/commands/FindCommand.java new file mode 100644 index 0000000000..d824a6f413 --- /dev/null +++ b/src/main/java/drake/commands/FindCommand.java @@ -0,0 +1,49 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import drake.DrakeException; +import drake.IncompatibleCommandException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; + +/** + * Represents a find command. + */ +public class FindCommand extends Command { + + private final ArrayList searchKeywords; + + /** + * Constructor. + * @param fullInput The user's input + * @throws IncompatibleCommandException When the user does not enter the search keywords. + */ + public FindCommand(String fullInput) throws IncompatibleCommandException { + int firstSpace = fullInput.indexOf(" "); + if (firstSpace == -1) { + throw new IncompatibleCommandException("Where are the wordssss :weary_face:"); + } + String afterFirstSpace = fullInput.substring(firstSpace + 1); + searchKeywords = new ArrayList<>(); + searchKeywords.addAll(Arrays.asList(afterFirstSpace.split(" "))); + } + + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws IOException, DrakeException { + //Inspired by parnikkapore's PR + TaskList matches = tasks.filter(searchKeywords); + ArrayList reply = new ArrayList<>(); + reply.add(String.format("Here are the tasks that match \"%s\":", + String.join("\", \"", searchKeywords))); + + for (int i = 1; matches.isValidTaskNumber(i); i++) { + reply.add(String.format("%d. %s", i + 1, matches.getTaskToString(i))); + } + return reply; + } +} diff --git a/src/main/java/drake/commands/ListCommand.java b/src/main/java/drake/commands/ListCommand.java new file mode 100644 index 0000000000..a739d010d4 --- /dev/null +++ b/src/main/java/drake/commands/ListCommand.java @@ -0,0 +1,25 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import drake.DrakeException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; + +/** + * Represent a list command. + */ +public class ListCommand extends Command { + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws IOException, DrakeException { + ArrayList reply = new ArrayList<>(); + reply.add("Here are the tasks in your list:"); + for (int i = 1; tasks.isValidTaskNumber(i); i++) { + reply.add(i + ". " + tasks.getTaskToString(i)); + } + return reply; + } +} diff --git a/src/main/java/drake/commands/MarkCommand.java b/src/main/java/drake/commands/MarkCommand.java new file mode 100644 index 0000000000..159d1ff048 --- /dev/null +++ b/src/main/java/drake/commands/MarkCommand.java @@ -0,0 +1,36 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import drake.DrakeException; +import drake.IncompatibleCommandException; +import drake.InvalidTaskNumberException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; + +/** + * Represents the mark command. + */ +public class MarkCommand extends TaskOperationCommand { + + public MarkCommand(String fullInput) throws IncompatibleCommandException { + super(fullInput); + } + + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws DrakeException, IOException { + if (tasks.isValidTaskNumber(taskNumber)) { + ArrayList reply = new ArrayList<>(); + reply.add("I've marked this task as done!"); + tasks.markAsDone(taskNumber); + storage.updateTask(taskNumber, CommandType.MARK); + reply.add(tasks.getTaskToString(taskNumber)); + return reply; + } else { + throw new InvalidTaskNumberException(); + } + } +} diff --git a/src/main/java/drake/commands/TaskOperationCommand.java b/src/main/java/drake/commands/TaskOperationCommand.java new file mode 100644 index 0000000000..7fd5d6925a --- /dev/null +++ b/src/main/java/drake/commands/TaskOperationCommand.java @@ -0,0 +1,56 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import drake.DrakeException; +import drake.IncompatibleCommandException; +import drake.InvalidTaskNumberException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; + +/** + * Represents a command given by the user to perform an operation on a task currently in the list. + */ +public abstract class TaskOperationCommand extends Command { + + protected final int taskNumber; + + /** + * Constructor. + * + * @param fullInput The user input. + * @throws IncompatibleCommandException when the user input does not contain a task number. + */ + public TaskOperationCommand(String fullInput) throws IncompatibleCommandException { + String[] commands = fullInput.split(" "); + try { + taskNumber = Integer.parseInt(commands[1]); + } catch (NumberFormatException e) { + throw new IncompatibleCommandException("Where's the number?"); + } + } + + /** + * Executes the command to perform an operation on a task currently in the list. + * + * @param tasks The task list before the command is executed. + * @param ui Gives access to the UI of the program. + * @param storage Gives access to local storage. + * @return The list of replies. + * @throws IOException when there is an issue with the IO. + * @throws DrakeException when there is inappropriate input or save file issues. + */ + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws DrakeException, IOException { + if (tasks.isValidTaskNumber(taskNumber)) { + ArrayList reply = new ArrayList<>(); + reply.add(tasks.getTaskToString(taskNumber)); + return reply; + } else { + throw new InvalidTaskNumberException(); + } + } +} diff --git a/src/main/java/drake/commands/TodoCommand.java b/src/main/java/drake/commands/TodoCommand.java new file mode 100644 index 0000000000..9a7c11fe76 --- /dev/null +++ b/src/main/java/drake/commands/TodoCommand.java @@ -0,0 +1,49 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import drake.DrakeException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; +import drake.tasks.Task; +import drake.tasks.Todo; + +/** + * Represents a command given by the user to create a new to-do task. + */ +public class TodoCommand extends CreateTaskCommand { + + /** + * Constructor. + * + * @param fullInput The user input. + */ + public TodoCommand(String fullInput) { + super(fullInput); + } + + /** + * Executes the command to create a new to-do task, saving the new task and + * printing the size of the task list after execution. + * + * @param tasks The task list before the command is executed. + * @param ui Gives access to the UI of the program. + * @param storage Gives access to local storage. + * @return The list of replies. + * @throws IOException when there is an issue with the IO. + * @throws DrakeException when there is inappropriate input or save file issues. + */ + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws DrakeException, IOException { + ArrayList reply = new ArrayList<>(); + reply.add("I've added this task:"); + Task addedTask = tasks.addTask(new Todo(description)); + reply.add(addedTask.toString()); + storage.addTask(addedTask); + reply.addAll(super.execute(tasks, ui, storage)); + return reply; + } +} diff --git a/src/main/java/drake/commands/UnmarkCommand.java b/src/main/java/drake/commands/UnmarkCommand.java new file mode 100644 index 0000000000..aa6440d166 --- /dev/null +++ b/src/main/java/drake/commands/UnmarkCommand.java @@ -0,0 +1,36 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import drake.DrakeException; +import drake.IncompatibleCommandException; +import drake.InvalidTaskNumberException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; + +/** + * Represents the unmark command. + */ +public class UnmarkCommand extends TaskOperationCommand { + + public UnmarkCommand(String fullInput) throws IncompatibleCommandException { + super(fullInput); + } + + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws DrakeException, IOException { + if (tasks.isValidTaskNumber(taskNumber)) { + ArrayList reply = new ArrayList<>(); + reply.add("I've marked this task as not done (yet ;))"); + tasks.markAsNotDone(taskNumber); + storage.updateTask(taskNumber, CommandType.UNMARK); + reply.add(tasks.getTaskToString(taskNumber)); + return reply; + } else { + throw new InvalidTaskNumberException(); + } + } +} diff --git a/src/main/java/drake/commands/WithinCommand.java b/src/main/java/drake/commands/WithinCommand.java new file mode 100644 index 0000000000..5877cee14e --- /dev/null +++ b/src/main/java/drake/commands/WithinCommand.java @@ -0,0 +1,60 @@ +package drake.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import drake.DrakeException; +import drake.IncompatibleCommandException; +import drake.Storage; +import drake.TaskList; +import drake.Ui; +import drake.tasks.DoWithinPeriod; +import drake.tasks.Task; + +/** + * Represents the within command to create a new DoWithinPeriod task. + */ +public class WithinCommand extends CreateTaskCommand { + + private static final Pattern descriptionPattern = + Pattern.compile("(?.*) /range (?.*) (?.*)"); + /** + * Constructor. + * + * @param fullInput The input given by the user. + */ + public WithinCommand(String fullInput) { + super(fullInput); + assert fullInput.startsWith("within"); + } + + /** + * Executes the command to create a new deadline task, saving the new task and + * printing the size of the task list after execution. + * + * @param tasks The task list before the command is executed. + * @param ui Gives access to the UI of the program. + * @param storage Gives access to local storage. + * @return The list of replies + * @throws IOException when there is an issue with the IO. + * @throws DrakeException when there is inappropriate input or save file issues. + */ + @Override + public List execute(TaskList tasks, Ui ui, Storage storage) throws DrakeException, IOException { + ArrayList reply = new ArrayList<>(); + Matcher match = descriptionPattern.matcher(description); + if (!match.matches()) { + throw new IncompatibleCommandException("I need two dates!"); + } + reply.add("I've added this task:"); + Task addedTask = tasks.addTask(new DoWithinPeriod(match.group("taskName"), + match.group("from"), match.group("to"))); + reply.add(addedTask.toString()); + storage.addTask(addedTask); + reply.addAll(super.execute(tasks, ui, storage)); + return reply; + } +} diff --git a/src/main/java/drake/gui/ChatMessage.java b/src/main/java/drake/gui/ChatMessage.java new file mode 100644 index 0000000000..0e2bd1f09a --- /dev/null +++ b/src/main/java/drake/gui/ChatMessage.java @@ -0,0 +1,73 @@ +package drake.gui; + +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +/** + * A JavaFX component displaying a chat message. + */ +public class ChatMessage extends VBox { + @FXML + private Label name; + + @FXML + private Label message; + + @FXML + private ImageView profilePicture; + + @FXML + private HBox header; + + private ChatMessage(String name, String text, Image img, boolean isWhiteBackground) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Window.class.getResource("/view/ChatMessage.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + this.name.setText(name); + message.setText(text); + profilePicture.setImage(img); + if (isWhiteBackground) { + message.setStyle("-fx-text-fill : #000000; -fx-background-color: white;"); + } + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(header.getChildren()); + Collections.reverse(tmp); + header.getChildren().setAll(tmp); + header.setAlignment(Pos.TOP_LEFT); + this.setAlignment(Pos.TOP_LEFT); + } + + public static ChatMessage getUserDialog(String text, Image img) { + return new ChatMessage("Lost Soul", text, img, true); + } + + public static ChatMessage getDrakeDialog(String text, Image img) { + var db = new ChatMessage("Drake", text, img, false); + db.flip(); + return db; + } + +} diff --git a/src/main/java/drake/gui/Main.java b/src/main/java/drake/gui/Main.java new file mode 100644 index 0000000000..908403ddee --- /dev/null +++ b/src/main/java/drake/gui/Main.java @@ -0,0 +1,34 @@ +package drake.gui; + +import java.io.IOException; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + + +/** + * A GUI for Drake. + */ +public class Main extends Application { + + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/Window.fxml")); + AnchorPane anchorPane = fxmlLoader.load(); + Scene scene = new Scene(anchorPane); + scene.getStylesheets().add("/view/styles.css"); + stage.setScene(scene); + stage.show(); + stage.setTitle("Drake"); + stage.setMinHeight(650.0); + stage.setMaxWidth(400.0); + stage.setMinWidth(400.0); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/src/main/java/drake/gui/Window.java b/src/main/java/drake/gui/Window.java new file mode 100644 index 0000000000..017e0c9218 --- /dev/null +++ b/src/main/java/drake/gui/Window.java @@ -0,0 +1,83 @@ +package drake.gui; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import drake.DrakeException; +import drake.Parser; +import drake.Storage; +import drake.TaskList; +import drake.Ui; +import drake.commands.Command; +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; + +/** + * Represents the chat window. + * Inspired by parnikkapore's PR. + */ +public class Window extends AnchorPane { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private final Image userImage = new Image(this.getClass().getResourceAsStream("/media/lostsoul.png")); + private final Image drakeImage = new Image(this.getClass().getResourceAsStream("/media/drake.png")); + + // initialize plugins + + private TaskList taskList; + private Storage storage; + private final Ui ui = new Ui(); + + public Window() throws DrakeException, IOException { + } + + /** + * Initializes the GUI Window. + * @throws DrakeException When the user enters incorrect input. + * @throws IOException When IO encounters an issue. + */ + @FXML + public void initialize() throws DrakeException, IOException { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + + // intro string + List messages = new ArrayList<>(ui.chatBubbleText(ui.replyWelcome())); + + // initialize plugins + storage = new Storage(); + taskList = new TaskList(storage.fileToList()); + + for (String message : messages) { + dialogContainer.getChildren().add(ChatMessage.getDrakeDialog(message, drakeImage)); + } + } + + /** + * Handles an input from the user. + */ + @FXML + private void handleUserInput() throws DrakeException, IOException { + String input = userInput.getText(); + Command command = Parser.parse(input); + List reply = command.execute(taskList, ui, storage); + dialogContainer.getChildren().add(ChatMessage.getUserDialog(input, userImage)); + + dialogContainer.getChildren().add( + ChatMessage.getDrakeDialog(String.join("\n", reply), drakeImage)); + + userInput.clear(); + } +} diff --git a/src/main/java/drake/tasks/Deadline.java b/src/main/java/drake/tasks/Deadline.java new file mode 100644 index 0000000000..dbaae0f139 --- /dev/null +++ b/src/main/java/drake/tasks/Deadline.java @@ -0,0 +1,38 @@ +package drake.tasks; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a deadline task. + */ +public class Deadline extends Task { + + protected final LocalDate by; + + /** + * Constructor. + * @param description The task description. + * @param by The deadline of the task. + */ + public Deadline(String description, String by) { + super(description); + this.by = LocalDate.parse(by); + } + + @Override + public List toList() { + List result = new ArrayList<>(); + result.add("D"); + result.addAll(super.toList()); + result.add(by.toString()); + return result; + } + + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + by.format(DateTimeFormatter.ofPattern("dd MMM yyyy")) + ")"; + } +} diff --git a/src/main/java/drake/tasks/DoWithinPeriod.java b/src/main/java/drake/tasks/DoWithinPeriod.java new file mode 100644 index 0000000000..bec4aa7923 --- /dev/null +++ b/src/main/java/drake/tasks/DoWithinPeriod.java @@ -0,0 +1,43 @@ +package drake.tasks; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents tasks that need to be done with a certain period. + */ +public class DoWithinPeriod extends Task { + + private final LocalDate from; + private final LocalDate to; + + /** + * Constructor. + * @param description The task description. + * @param from The starting date of the period. + * @param to The ending date of the period. + */ + public DoWithinPeriod(String description, String from, String to) { + super(description); + this.from = LocalDate.parse(from); + this.to = LocalDate.parse(to); + } + + @Override + public List toList() { + List result = new ArrayList<>(); + result.add("W"); + result.addAll(super.toList()); + result.add(from.toString()); + result.add(to.toString()); + return result; + } + + @Override + public String toString() { + return "[W]" + super.toString() + " (from: " + from.format(DateTimeFormatter.ofPattern("dd MMM yyyy")) + ", " + + "to: " + to.format(DateTimeFormatter.ofPattern("dd MMM yyyy")) + ")"; + } +} diff --git a/src/main/java/drake/tasks/Event.java b/src/main/java/drake/tasks/Event.java new file mode 100644 index 0000000000..60fd5ddb2e --- /dev/null +++ b/src/main/java/drake/tasks/Event.java @@ -0,0 +1,29 @@ +package drake.tasks; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +public class Event extends Task { + protected final LocalDate at; + + public Event(String description, String at) { + super(description); + this.at = LocalDate.parse(at); + } + + @Override + public List toList() { + List result = new ArrayList<>(); + result.add("E"); + result.addAll(super.toList()); + result.add(at.toString()); + return result; + } + + @Override + public String toString() { + return "[E]" + super.toString() + " (at: " + at.format(DateTimeFormatter.ofPattern("dd MMM yyyy")) + ")"; + } +} diff --git a/src/main/java/drake/tasks/Task.java b/src/main/java/drake/tasks/Task.java new file mode 100644 index 0000000000..a1dc11a475 --- /dev/null +++ b/src/main/java/drake/tasks/Task.java @@ -0,0 +1,38 @@ +package drake.tasks; + +import java.util.Arrays; +import java.util.List; + +public abstract class Task { + protected String description; + protected boolean isDone; + + public Task(String description) { + this.description = description; + this.isDone = false; + } + + public String getStatusIcon() { + return (isDone ? "X" : " "); // mark done task with X + } + + public void markAsDone() { + isDone = true; + } + + public void markAsNotDone() { + isDone = false; + } + + public List toList() { + return Arrays.asList(description, getStatusIcon()); + } + + public boolean isMatch(List searchKeywords) { + return searchKeywords.stream().allMatch(description::contains); + } + @Override + public String toString() { + return "[" + getStatusIcon() + "] " + description; + } +} diff --git a/src/main/java/drake/tasks/Todo.java b/src/main/java/drake/tasks/Todo.java new file mode 100644 index 0000000000..17ae507bb4 --- /dev/null +++ b/src/main/java/drake/tasks/Todo.java @@ -0,0 +1,24 @@ +package drake.tasks; + +import java.util.ArrayList; +import java.util.List; + +public class Todo extends Task { + + public Todo(String description) { + super(description); + } + + @Override + public List toList() { + List result = new ArrayList<>(); + result.add("T"); + result.addAll(super.toList()); + return result; + } + + @Override + public String toString() { + return "[T]" + super.toString(); + } +} diff --git a/src/main/resources/fonts/YuseiMagic-Regular.ttf b/src/main/resources/fonts/YuseiMagic-Regular.ttf new file mode 100644 index 0000000000..b3480eb236 Binary files /dev/null and b/src/main/resources/fonts/YuseiMagic-Regular.ttf differ diff --git a/src/main/resources/media/background.png b/src/main/resources/media/background.png new file mode 100644 index 0000000000..52f7937329 Binary files /dev/null and b/src/main/resources/media/background.png differ diff --git a/src/main/resources/media/drake.png b/src/main/resources/media/drake.png new file mode 100644 index 0000000000..1b7f49e483 Binary files /dev/null and b/src/main/resources/media/drake.png differ diff --git a/src/main/resources/media/lostsoul.png b/src/main/resources/media/lostsoul.png new file mode 100644 index 0000000000..2f6760ca8d Binary files /dev/null and b/src/main/resources/media/lostsoul.png differ diff --git a/src/main/resources/view/ChatMessage.fxml b/src/main/resources/view/ChatMessage.fxml new file mode 100644 index 0000000000..2356a52687 --- /dev/null +++ b/src/main/resources/view/ChatMessage.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/view/Window.fxml b/src/main/resources/view/Window.fxml new file mode 100644 index 0000000000..4febe02231 --- /dev/null +++ b/src/main/resources/view/Window.fxml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + +