diff --git a/.gitignore b/.gitignore index f69985ef1f..5c3663723f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ /out/ /*.iml +# .class files +/.class/ + # Gradle build files /.gradle/ /build/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..04d76f4a14 --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '5.1.0' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +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 { + // Change this to your main class. + mainClassName = "duke.Launcher" +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +run { + enableAssertions = true + standardInput = System.in +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 8077118ebe..823aae41ef 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,310 @@ -# User Guide +# User Guide to use Duke + +## What is Duke? + +Duke is a chat bot designed to help users manage their tasks. It is a command-line +software that processes user input and displays immediate responses. Furthermore, +tasks added by users to Duke will be stored and can be retrieved each +time Duke is opened. + +
## Features -### Feature-ABC +### Add Tasks + +Tasks can be added to the list of tasks via one of the following categories: +1. To-do task +2. Deadline task +3. Event task + +### View Tasks +Tasks can be viewed as a list. + +### Mark Tasks +Tasks can be marked as completed. + +### Un-mark Tasks +Tasks can be un-marked to show they are still incomplete. -Description of the feature. +### Delete tasks +Tasks can be deleted from the list of tasks. -### Feature-XYZ +### Find tasks +Tasks can be found from the list of tasks. -Description of the feature. +
-## Usage +## Features Details -### `Keyword` - Describe action +### `todo` - Adds a to-do task -Describe the action and its outcome. +**Adds a to-do task to the list of tasks.** -Example of usage: +**Format:** `todo TASK_DESCRIPTION` -`keyword (optional arguments)` +- The task description **must not** be empty. -Expected outcome: +**Example of usage:** +`todo Physics homework` -Description of the outcome. +**Expected outcome:** +A to-do task will be successfully added to the list of tasks. ``` -expected output +______________________________________________________ +Got it. I've added this task: +[T][] Physics homework +Now you have 1 tasks in the list. +______________________________________________________ ``` +
+ +### `event` - Adds an event task + +**Adds an event task to the list of tasks.** + +**Format**: `event TASK_DESCRIPTION /at TIME_OR_PLACE` + +- the task description **must not** be empty. +- the time or place **must not** be empty. + +**Example of usage:** +`event Sara's birthday /at office` + + +**Expected outcome:** +An event task will be successfully added to the list of tasks. + +``` +______________________________________________________ +Got it. I've added this task: +[E][] Sara's birthday (at: restaurant) +Now you have 1 tasks in the list. +______________________________________________________ +``` +
+ +### `deadline` - Adds a deadline task + +**Adds a deadline task to the list of tasks.** + +**Format:** `deadline TASK_DESCRIPTION /by DATE` + +- The task description **must not** be empty. +- The date **must adhere** to the YYYY-MM-DD format. + +**Example of usage:** +`deadline Physics assignment /by 2022-10-11` + +**Expected outcome:** +A deadline task will be successfully added to the list of tasks. + +``` +______________________________________________________ +Got it. I've added this task: +[D][] Physics assignment (by: Oct 11 2022) +Now you have 1 tasks in the list. +______________________________________________________ +``` +
+ + +### `list` - displays all tasks as a list + +**Allows user to view tasks as a list.** + +**Example of usage:** +`list` + +**Expected outcome:** +A list of all the user's tasks will be shown. + +``` +______________________________________________________ +Here are the tasks in your list: +1. [T][] Physics homework +2. [E][] Sara's birthday (at: restaurant) +3. [D][] Physics assignment (by: Oct 11 2022) +______________________________________________________ +``` +
+ + +### `mark` - marks a task as complete + +**Marks a task in the list of tasks according to the specified index to suggest +the task is completed.** + +**Format:** `mark TASK_INDEX` + +- Task index **must not** exceed the number of total tasks and must be more than 0. + +**Example of usage:** +`mark 1` + +**Expected outcome:** +The first task in the list of tasks will be marked as completed. + +``` +______________________________________________________ +Nice! I've marked this task as done: +[T][X] Physics homework +______________________________________________________ +``` +
+ + +### `unmark` - un-marks a task to show it is still incomplete + +**Un-marks a task in the list of tasks according to the specified index to suggest +that the task is still incomplete.** + +**Format:** `unmark TASK_INDEX` + +- Task index **must not** exceed the number of total tasks and must be more than 0. + +**Example of usage:** +`unmark 1` + +**Expected outcome:** +The first task in the list of tasks will be unmarked. + +``` +______________________________________________________ +OK. I've marked this task as not done yet: +[T][] Physics homework +______________________________________________________ +``` +
+ + +### `delete` - deletes a task from the list of tasks + +**Deletes a task in the list of tasks according to the specified index.** + +**Format:** `delete TASK_INDEX` + +- Task index **must not** exceed the number of total tasks and must be more than 0. + +**Example of usage:** +`delete 1` + +**Expected outcome:** +The first task in the list of tasks will be deleted. + +``` +______________________________________________________ +Noted. I've removed this task: +[T][] Physics homework +Now you have 0 tasks in the list. +______________________________________________________ +``` +
+ + +## `find` - finds a task from the list of tasks + +**Finds a task in the list of tasks with a keyword corresponding to the +description of the task.** + +**Format:** `find KEYWORD` + +- If no task descriptions matching the keyword are found, the list of tasks returned +will be empty. + +**Example of usage:** +`find homework` + +**Expected outcome:** +A list of tasks with descriptions matching the keyword `homework`. + +``` +______________________________________________________ +Here are the matching tasks in your list: +[T][] Physics homework +______________________________________________________ +``` +
+ + +## `help` - displays commands to use Duke + +**Displays basic commands to help the user use basic features of Duke.** + +**Example of usage:** +`help` + +**Expected outcome:** +A list of basic commands to utilise Duke. + +``` +______________________________________________________ +Hello! Here is some commands to help you use this app better! + +1. bye: +Ends the session and app will close + +2. todo {task description}: +Adds a to-do task to your list of tasks + +3. event {task description} /at {time or place}: +Adds a event task to your list of tasks + +4. deadline {task description} /by {date time in YYYY-MM-DD}: +Adds a deadline task to your list of tasks + +5. list: +Returns all tasks in the task list + +6. help!: +More advanced Duke features! +______________________________________________________ +``` +
+ + +## `help!` - displaying advanced commands to use Duke + +**Displays advanced commands to help the user use advanced features of Duke.** + +**Example of usage**: +`help!` + +**Expected outcome**: +A list of advanced commands to utilise Duke. + +``` +______________________________________________________ +More advanced commands here! + +1. mark {task number}: +marks the task with index corresponding to the task number as done + +2. unmark {task number}: +un-marks the task with index corresponding to the task number as done + +3. delete {task number}: +deletes the task with index corresponding to the task number + +4. find {keyword}: +find the task with description corresponding to the keyword input +______________________________________________________ +``` +
+ +## `bye` - exits Duke + +**Exits the Duke application.** + +**Example of usage:** +`bye` + +**Expected outcome:** +A bye message is displayed and the application closes. +``` +______________________________________________________ +Bye! I'll be closing soon, till we meet again! +______________________________________________________ +``` + diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..f9c7316269 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/duke/Duke.java b/src/main/java/duke/Duke.java new file mode 100644 index 0000000000..1a7ba0dc8c --- /dev/null +++ b/src/main/java/duke/Duke.java @@ -0,0 +1,69 @@ +package duke; + +import duke.command.Command; +import duke.parser.Parser; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; + +public class Duke { + + private final String SAVED_PATH = "data/duke.txt"; + private Storage storage; + private TaskList tasks; + private Ui ui; + private boolean isExit; + + //@@author Sampy147-reused + //Reused from https://github.com/nus-cs2103-AY1920S1/duke/pull/266/files + //with minor modifications + public Duke() { + this.isExit = false; + this.storage = new Storage(SAVED_PATH); + try { + this.tasks = this.storage.load(); + } catch (DukeException e) { + this.ui = new Ui(new TaskList()); + ui.showError(e.getMessage()); + exit(); + } + this.ui = new Ui(this.tasks); + } + + public boolean exitNow() { + return this.isExit; + } + + //@@author Sampy147-reused + //Reused from https://stackoverflow.com/questions/15747277/how-to-make-java-program-exit-after-a-couple-of-seconds + // with minor modifications + public void exit() { + Timer timer = new Timer(); + TimerTask exitApplication = new TimerTask() { + public void run() { + System.exit(0); + } + }; + timer.schedule(exitApplication, new Date(System.currentTimeMillis() + 3 * 1000)); + } + //@@author + + public String getResponse(String input) { + try { + String fullCommand = input; + Command c = Parser.parse(input); + String output = c.execute(tasks, ui, storage); + if (c.isExit()) { + this.isExit = true; + } + return output; + } catch (DukeException e) { + return ui.showError(e.getMessage()); + } + } + +} diff --git a/src/main/java/duke/DukeException.java b/src/main/java/duke/DukeException.java new file mode 100644 index 0000000000..73c1a8d300 --- /dev/null +++ b/src/main/java/duke/DukeException.java @@ -0,0 +1,9 @@ +package duke; + +public class DukeException extends Exception { + + public DukeException(String message) { + super(message); + } + +} diff --git a/src/main/java/duke/Launcher.java b/src/main/java/duke/Launcher.java new file mode 100644 index 0000000000..69a134b698 --- /dev/null +++ b/src/main/java/duke/Launcher.java @@ -0,0 +1,18 @@ +package duke; + +import javafx.application.Application; + +//@@author Sampy147-reused +//Reused from https://se-education.org/guides/tutorials/javaFxPart4.html +//with minor modifications +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + + public static void main(String[] args) { + Application.launch(Main.class, args); + } + +} +//@@author \ No newline at end of file diff --git a/src/main/java/duke/Main.java b/src/main/java/duke/Main.java new file mode 100644 index 0000000000..ff0d4ad804 --- /dev/null +++ b/src/main/java/duke/Main.java @@ -0,0 +1,36 @@ +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.layout.AnchorPane; +import javafx.stage.Stage; + + +/** + * A GUI for Duke using FXML. + */ +public class Main extends Application { + + private Duke duke = new Duke(); + + //@@author Sampy147-reused + //Reused from https://se-education.org/guides/tutorials/javaFxPart4.html + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + Scene scene = new Scene(ap); + stage.setScene(scene); + fxmlLoader.getController().setDuke(duke); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } + //@@author +} \ No newline at end of file diff --git a/src/main/java/duke/Message.java b/src/main/java/duke/Message.java new file mode 100644 index 0000000000..20be49b9ef --- /dev/null +++ b/src/main/java/duke/Message.java @@ -0,0 +1,63 @@ +package duke; + +import duke.task.TaskList; + +//@@author Sampy147-reused +//Reused from https://github.com/nus-cs2103-AY1920S1/duke/pull/266/files +//with minor modifications, appropriated the idea of a Message class +public class Message { + + public static final String HORIZONTAL_BORDER = "______________________________________________________"; + public static final String WELCOME_MESSAGE = "Hello! I'm Duke\nWhat can I do for you?"; + public static final String BYE_MESSAGE = "Bye! I'll be closing soon, till we meet again!"; + public static final String INVALID_TODO_INPUT = "The description of a todo cannot be empty."; + public static final String INVALID_DEADLINE_INPUT = + "Please use proper deadline formatting: deadline {task} /by {time}"; + public static final String INVALID_DATE_FORMAT = + "Please indicate your date after {/bye} as YYYY-MM-DD (e.g 2019-12-09)"; + public static final String INVALID_EVENT_INPUT = "Please use proper event formatting: event {task} /at {time}"; + public static final String INVALID_ACCESS_EMPTY_TASKLIST = + "Task does not exist. Initialise a task first, then try again"; + public static final String INVALID_MARK_TASK_FORMAT = + "To mark a task, please input this format: mark {task number}"; + public static final String INVALID_UNMARK_TASK_FORMAT = + "To unmark a task, please input this format: unmark {task number}"; + public static final String INVALID_DELETE_TASK_FORMAT = + "To delete a task, please input this format: delete {task number}"; + public static final String INVALID_USER_INPUT = + "Unfortunately, I am unable to recognise your command :( please input \"help\" for for more details"; + public static final String INVALID_DATE_INPUT = "The date given should not be before today's date"; + public static final String FILE_NOT_FOUND = "The memory file cannot be found."; + public static final String FILE_READ_ERROR = "There is an error when reading the memory file."; + public static final String FILE_CREATE_ERROR = "There is an error when creating the memory file"; + public static final String INVALID_FIND_TASK_FORMAT = "To find a task, please input this format: find {Keyword}"; + public static final String HELP_MESSAGE = + "Hello! Here is some commands to help you use this app better!\n\n" + + "1. bye:\nEnds the session and app will close\n\n" + + "2. todo {task description}:\nAdds a to-do task to your list of tasks\n\n" + + "3. event {task description} /at {time or place}:\nAdds a event task to your list of tasks\n\n" + + "4. deadline {task description} /by {date time in YYYY-MM-DD}:\n" + + "Adds a deadline task to your list of tasks\n\n" + + "5. list:\nReturns all tasks in the task list\n\n" + + "6. help!:\nMore advanced Duke features!"; + public static final String ADVANCED_HELP_MESSAGE = + "More advanced commands here!\n\n" + + "1. mark {task number}:\nmarks the task with index corresponding to the task number as done\n\n" + + "2. unmark {task number}:\nun-marks the task with index corresponding to the task number as done\n\n" + + "3. delete {task number}:\ndeletes the task with index corresponding to the task number\n\n" + + "4. find {keyword}:\nfind the task with description corresponding to the keyword input"; + + /** + * Returns a String that describes that the task does not exist within the specified tasklist + * and suggests potential numbers for the user to try to locate the task. This method is called + * when the task is not found in the tasklist specified + * + * @param tasks the tasklist of type TaskList + * @return a String message describing the task is not found + */ + public static String returnTaskNotFound(TaskList tasks) { + return "Task does not exist. Try another number between 1 and " + tasks.getCount(); + } + +} +//@@author \ No newline at end of file diff --git a/src/main/java/duke/command/AddCommand.java b/src/main/java/duke/command/AddCommand.java new file mode 100644 index 0000000000..d9c0ce6d83 --- /dev/null +++ b/src/main/java/duke/command/AddCommand.java @@ -0,0 +1,50 @@ +package duke.command; + +import duke.DukeException; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; +import duke.ui.Ui; + +/** + * Adds a task to the existing storage + */ +public class AddCommand extends Command { + + private final Task task; + + /** + * Creates a command to add a task to the existing storage + * + * @param task task to be added + */ + public AddCommand(Task task) { + assert task != null : "task cannot be null"; + this.task = task; + } + + /** + * Determines if the command should end the program for the user + * + * @return false by default + */ + @Override + public boolean isExit() { + return false; + } + + /** + * Executes the addition of a task from the storage + * + * @param tasks the list of tasks to be modified in execution + * @param ui the ui used to display messages to the user upon successful addition + * @param storage the storage to be modified in execution + * @throws DukeException if the change cannot be saved in storage successfully + */ + @Override + public String execute(TaskList tasks, Ui ui, Storage storage) throws DukeException { + tasks.add(this.task); + storage.save(this.task.toSimpleString()); + return ui.showAddition(this.task, tasks.getCount()); + } +} diff --git a/src/main/java/duke/command/AdvancedHelpCommand.java b/src/main/java/duke/command/AdvancedHelpCommand.java new file mode 100644 index 0000000000..727ff00518 --- /dev/null +++ b/src/main/java/duke/command/AdvancedHelpCommand.java @@ -0,0 +1,18 @@ +package duke.command; + +import duke.DukeException; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class AdvancedHelpCommand extends Command { + @Override + public boolean isExit() { + return false; + } + + @Override + public String execute(TaskList tasks, Ui ui, Storage storage) throws DukeException { + return ui.showAdvancedHelp(); + } +} diff --git a/src/main/java/duke/command/ByeCommand.java b/src/main/java/duke/command/ByeCommand.java new file mode 100644 index 0000000000..484acae733 --- /dev/null +++ b/src/main/java/duke/command/ByeCommand.java @@ -0,0 +1,35 @@ +package duke.command; + +import duke.DukeException; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +/** + * Exits the program + */ +public class ByeCommand extends Command { + + /** + * Determines if the command should end the program for the user + * + * @return true by default + */ + @Override + public boolean isExit() { + return true; + } + + /** + * Exits the program + * + * @param tasks the list of tasks + * @param ui the ui used to display the bye message + * @param storage the local storage + */ + @Override + public String execute(TaskList tasks, Ui ui, Storage storage) throws DukeException { + return ui.showBye(); + } + +} diff --git a/src/main/java/duke/command/Command.java b/src/main/java/duke/command/Command.java new file mode 100644 index 0000000000..2f2cec677b --- /dev/null +++ b/src/main/java/duke/command/Command.java @@ -0,0 +1,30 @@ +package duke.command; + +import duke.DukeException; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +/** + * Represents a command that can be executed + */ +public abstract class Command { + + /** + * Determines if the command should end the program for the user + * + * @return boolean that determines whether program should end + */ + public abstract boolean isExit(); + + /** + * Executes the command + * + * @param tasks the list of tasks + * @param ui the ui used to display messages after executing the command + * @param storage the local storage + * @throws DukeException if an exception is thrown when the command is executed + */ + public abstract String execute(TaskList tasks, Ui ui, Storage storage) throws DukeException; + +} diff --git a/src/main/java/duke/command/DeleteCommand.java b/src/main/java/duke/command/DeleteCommand.java new file mode 100644 index 0000000000..cd2bf5f9e7 --- /dev/null +++ b/src/main/java/duke/command/DeleteCommand.java @@ -0,0 +1,62 @@ +package duke.command; + +import duke.Message; +import duke.DukeException; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; +import duke.ui.Ui; + +/** + * Deletes a task from the existing storage + */ +public class DeleteCommand extends Command { + + private final int index; + + /** + * Creates a command to delete a task from the existing storage + * + * @param index the index of the task to be deleted + */ + public DeleteCommand(int index) { + this.index = index; + } + + /** + * Determines if the command should end the program for the user + * + * @return false by default + */ + @Override + public boolean isExit() { + return false; + } + + /** + * Executes the deletion of a task from the storage + * + * @param tasks the list of tasks to be modified in execution + * @param ui the ui used to display messages to the user upon successful deletion + * @param storage the storage to be modified in execution + * @throws DukeException if the change cannot be saved in storage successfully or task cannot be deleted + */ + @Override + public String execute(TaskList tasks, Ui ui, Storage storage) throws DukeException { + try { + Task deletedTask = tasks.deleteTaskAtPos(this.index); + assert this.index <= tasks.getCount() + 1 && this.index > 0 : + "Position argument should be more than 0 and less than or equal to the task list size"; + storage.save(tasks); + return ui.showDeleted(deletedTask); + } catch (IndexOutOfBoundsException e) { + if (tasks.getCount() == 0){ + throw new DukeException(Message.INVALID_ACCESS_EMPTY_TASKLIST); + } else { + assert tasks.getCount() > 0 : "task list should have 1 or more tasks"; + throw new DukeException(Message.returnTaskNotFound(tasks)); + } + } + } + +} diff --git a/src/main/java/duke/command/FindCommand.java b/src/main/java/duke/command/FindCommand.java new file mode 100644 index 0000000000..e28f3533a2 --- /dev/null +++ b/src/main/java/duke/command/FindCommand.java @@ -0,0 +1,28 @@ +package duke.command; + +import duke.DukeException; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class FindCommand extends Command { + + private String keyword; + + public FindCommand(String keyword) { + assert keyword != null : "keyword cannot be null"; + this.keyword = keyword; + } + + @Override + public boolean isExit() { + return false; + } + + @Override + public String execute(TaskList tasks, Ui ui, Storage storage) throws DukeException { + String foundTasks = tasks.findTasks(this.keyword); + return ui.showFound(foundTasks); + } + +} diff --git a/src/main/java/duke/command/HelpCommand.java b/src/main/java/duke/command/HelpCommand.java new file mode 100644 index 0000000000..ccab2db9a8 --- /dev/null +++ b/src/main/java/duke/command/HelpCommand.java @@ -0,0 +1,18 @@ +package duke.command; + +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class HelpCommand extends Command { + + @Override + public boolean isExit() { + return false; + } + + @Override + public String execute(TaskList tasks, Ui ui, Storage storage) { + return ui.showHelp(); + } +} diff --git a/src/main/java/duke/command/ListCommand.java b/src/main/java/duke/command/ListCommand.java new file mode 100644 index 0000000000..ba0defd132 --- /dev/null +++ b/src/main/java/duke/command/ListCommand.java @@ -0,0 +1,35 @@ +package duke.command; + +import duke.DukeException; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +/** + * Displays the list of tasks in the local storage + */ +public class ListCommand extends Command { + + /** + * Determines if the command should end the program for the user + * + * @return false by default + */ + @Override + public boolean isExit() { + return false; + } + + /** + * Displays the list of tasks in the local storage using the ui + * + * @param tasks the list of tasks + * @param ui the ui used to display messages to the user + * @param storage the local storage + */ + @Override + public String execute(TaskList tasks, Ui ui, Storage storage) throws DukeException { + return ui.showList(); + } + +} diff --git a/src/main/java/duke/command/MarkCommand.java b/src/main/java/duke/command/MarkCommand.java new file mode 100644 index 0000000000..fa5efd1ef3 --- /dev/null +++ b/src/main/java/duke/command/MarkCommand.java @@ -0,0 +1,63 @@ +package duke.command; + +import duke.Message; +import duke.DukeException; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; +import duke.ui.Ui; + +/** + * Marks a task in the existing storage + */ +public class MarkCommand extends Command { + + private final int index; + + /** + * Creates a command to mark a task in the existing storage + * + * @param index the index of the task to be marked + */ + public MarkCommand(int index) { + this.index = index; + } + + /** + * Determines if the command should end the program for the user + * + * @return false by default + */ + @Override + public boolean isExit() { + return false; + } + + /** + * Executes the marking of a task in the local storage + * + * @param tasks the list of tasks to be modified in execution + * @param ui the ui used to display messages to the user once the task is successfully marked + * @param storage the storage to be modified in execution + * @throws DukeException if the change cannot be saved in storage successfully or task cannot be marked + */ + @Override + public String execute(TaskList tasks, Ui ui, Storage storage) throws DukeException { + try { + tasks.markTaskAtPos(this.index); + Task currentTask = tasks.getTask(this.index); + assert this.index <= tasks.getCount() && this.index > 0 : + "Index should be more than 0 and less than or equal to the task list size"; + storage.save(tasks); + return ui.showMarked(currentTask); + } catch (IndexOutOfBoundsException e) { + if (tasks.getCount() == 0) { + throw new DukeException(Message.INVALID_ACCESS_EMPTY_TASKLIST); + } else { + assert tasks.getCount() > 0 : "task list should have 1 or more tasks"; + throw new DukeException(Message.returnTaskNotFound(tasks)); + } + } + } + +} diff --git a/src/main/java/duke/command/UnmarkCommand.java b/src/main/java/duke/command/UnmarkCommand.java new file mode 100644 index 0000000000..2d60146705 --- /dev/null +++ b/src/main/java/duke/command/UnmarkCommand.java @@ -0,0 +1,66 @@ +package duke.command; + +import duke.DukeException; +import duke.Message; +import duke.storage.Storage; +import duke.task.Task; +import duke.task.TaskList; +import duke.ui.Ui; + +/** + * Unmarks a task in the existing storage + */ +public class UnmarkCommand extends Command { + + private final int index; + + /** + * Creates a command to unmark a task in the existing storage + * + * @param index the index of the task to be unmarked + */ + public UnmarkCommand(int index) { + this.index = index; + } + + /** + * Determines if the command should end the program for the user + * + * @return false by default + */ + @Override + public boolean isExit() { + return false; + } + + /** + * Executes the unmarking of a task in the local storage + * + * @param tasks the list of tasks to be modified in execution + * @param ui the ui used to display messages to the user once the task is successfully unmarked + * @param storage the storage to be modified in execution + * @throws DukeException if the change cannot be saved in storage successfully or task cannot be unmarked + */ + @Override + public String execute(TaskList tasks, Ui ui, Storage storage) throws DukeException { + try { + tasks.unmarkTaskAtPos(this.index); + Task currentTask = tasks.getTask(this.index); + assert this.index <= tasks.getCount() && this.index > 0 : + "Position argument should be more than 0 and less than or equal to the task list size"; + storage.save(tasks); + return ui.showUnmarked(currentTask); + } catch (IndexOutOfBoundsException e) { + if (tasks.getCount() == 0) { + throw new DukeException(Message.INVALID_ACCESS_EMPTY_TASKLIST); + } else { + assert tasks.getCount() > 0 : "task list should have 1 or more tasks"; + throw new DukeException(Message.returnTaskNotFound(tasks)); + } + } + } + +} + + + diff --git a/src/main/java/duke/gui/DialogBox.java b/src/main/java/duke/gui/DialogBox.java new file mode 100644 index 0000000000..37a275f498 --- /dev/null +++ b/src/main/java/duke/gui/DialogBox.java @@ -0,0 +1,77 @@ +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.Rectangle; + +/** + * An example of a custom control using FXML. + * This control represents a dialog box consisting of an ImageView to represent the speaker's face and a label + * containing text from the speaker. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + //@@author Sampy147-reused + //Reused from https://se-education.org/guides/tutorials/javaFxPart4.html + //with minor modifications + 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); + + //@@author Sampy147-reused + //Reused from https://stackoverflow.com/questions/39650031/javafx-round-image-using-scenebuilderwith-clip + //with minor modifications + Rectangle clip = new Rectangle( + displayPicture.getFitWidth(), displayPicture.getFitHeight() + ); + clip.setArcWidth(40); + clip.setArcHeight(40); + displayPicture.setClip(clip); + //@@author + } + + /** + * 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; + } + //@@author +} diff --git a/src/main/java/duke/gui/MainWindow.java b/src/main/java/duke/gui/MainWindow.java new file mode 100644 index 0000000000..38df1c752c --- /dev/null +++ b/src/main/java/duke/gui/MainWindow.java @@ -0,0 +1,66 @@ +package duke.gui; + +import duke.Duke; +import duke.Message; +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; + +//@@author Sampy147-reused +//Reused from https://se-education.org/guides/tutorials/javaFxPart4.html +//with minor modifications +/** + * Controller for MainWindow. Provides the layout for the other controls. + */ +public class MainWindow extends AnchorPane { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private Duke duke; + + private Image userImage = new Image(this.getClass().getResourceAsStream("/images/DaUser.png")); + private Image dukeImage = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png")); + + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + displayInitMessage(); + } + + public void setDuke(Duke d) { + duke = d; + } + + private void displayInitMessage() { + dialogContainer.getChildren().addAll(DialogBox.getDukeDialog(Message.WELCOME_MESSAGE, dukeImage)); + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + String response = duke.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + if (duke.exitNow()) { + duke.exit(); + } + } +} +//@@author \ No newline at end of file diff --git a/src/main/java/duke/parser/Parser.java b/src/main/java/duke/parser/Parser.java new file mode 100644 index 0000000000..d081e0dc00 --- /dev/null +++ b/src/main/java/duke/parser/Parser.java @@ -0,0 +1,136 @@ +package duke.parser; + +import duke.DukeException; +import duke.Message; +import duke.command.*; +import duke.task.Deadline; +import duke.task.Event; +import duke.task.ToDo; + +import java.time.LocalDate; +import java.util.Arrays; + +public class Parser { + + public static Command parse(String fullCommand) throws DukeException { + String[] commandList = fullCommand.strip().split(" "); + String command = commandList[0].toLowerCase(); + if (command.equals("bye") && commandList.length == 1) { + return new ByeCommand(); + } else if (command.equals("list") && commandList.length == 1) { + return new ListCommand(); + } else if (command.equals("help") && commandList.length == 1) { + return new HelpCommand(); + } else if (command.equals("help!") && commandList.length == 1) { + return new AdvancedHelpCommand(); + } else if (command.equals("mark")) { + return markTask(fullCommand); + } else if (command.equals("unmark")) { + return unmarkTask(fullCommand); + } else if (command.equals("deadline")) { + return addDeadline(fullCommand); + } else if (command.equals("event")) { + return addEvent(fullCommand); + } else if (command.equals("todo")) { + return addToDo(fullCommand); + } else if (command.equals("delete")) { + return deleteTask(fullCommand); + } else if (command.equals("find")) { + return findTask(fullCommand); + } + throw new DukeException(Message.INVALID_USER_INPUT); + } + + + private static AddCommand addToDo(String input) throws DukeException { + String description = input.substring("todo".length()).strip(); + if (!description.equals("")) { + ToDo newToDo = new ToDo(description); + return new AddCommand(newToDo); + } else { + throw new DukeException(Message.INVALID_TODO_INPUT); + } + } + + private static AddCommand addDeadline(String input) throws DukeException { + String[] stringArray = input.substring("deadline".length()).strip().split("/by"); + try { + String description = stringArray[0].strip(); + String by = stringArray[1].strip(); + LocalDate deadlineDate = LocalDate.parse(by); + if (deadlineDate.isBefore(LocalDate.now())) { + throw new DukeException(Message.INVALID_DATE_INPUT); + } + if (description.equals("")) { + throw new DukeException(Message.INVALID_DEADLINE_INPUT); + } + Deadline newDeadline = new Deadline(description, deadlineDate); + return new AddCommand(newDeadline); + } catch (ArrayIndexOutOfBoundsException e) { + throw new DukeException(Message.INVALID_DEADLINE_INPUT); + } catch (java.time.format.DateTimeParseException e) { + throw new DukeException(Message.INVALID_DATE_FORMAT); + } + } + + private static AddCommand addEvent(String input) throws DukeException { + try { + String[] stringArray = input.substring("event".length()).strip().split("/at"); + String description = stringArray[0].strip(); + String at = stringArray[1].strip(); + if (description.equals("") || at.equals("")) { + throw new DukeException(Message.INVALID_EVENT_INPUT); + } + Event newEvent = new Event(description, at); + return new AddCommand(newEvent); + } catch (IndexOutOfBoundsException e) { + throw new DukeException(Message.INVALID_EVENT_INPUT); + } + } + + private static MarkCommand markTask(String input) throws DukeException { + String[] commandList = input.strip().split(" "); + try { + int taskIndexNum = Integer.parseInt(commandList[1]); + return new MarkCommand(taskIndexNum); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + throw new DukeException(Message.INVALID_MARK_TASK_FORMAT); + } + } + + private static UnmarkCommand unmarkTask(String input) throws DukeException { + String[] commandList = input.strip().split(" "); + try { + int taskIndexNum = Integer.parseInt(commandList[1]); + return new UnmarkCommand(taskIndexNum); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + throw new DukeException(Message.INVALID_UNMARK_TASK_FORMAT); + } + } + + private static DeleteCommand deleteTask(String command) throws DukeException { + String[] commandList = command.strip().split(" "); + try { + int taskIndexNum = Integer.parseInt(commandList[1]); + return new DeleteCommand(taskIndexNum); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + throw new DukeException(Message.INVALID_DELETE_TASK_FORMAT); + } + } + + private static FindCommand findTask(String command) throws DukeException { + String[] commandList = command.strip().split(" ", 2); + try { + String keyword = commandList[1].strip(); + if (keyword.equals("")) { + throw new DukeException(Message.INVALID_FIND_TASK_FORMAT); + } + assert !keyword.equals("") : "keyword cannot be empty"; + return new FindCommand(keyword); + } catch (IndexOutOfBoundsException e) { + assert commandList.length < 2 : "Command list should contain at most 1 element"; + throw new DukeException(Message.INVALID_FIND_TASK_FORMAT); + } + } + +} diff --git a/src/main/java/duke/storage/Storage.java b/src/main/java/duke/storage/Storage.java new file mode 100644 index 0000000000..6f5c729c5e --- /dev/null +++ b/src/main/java/duke/storage/Storage.java @@ -0,0 +1,131 @@ +package duke.storage; + +import duke.DukeException; +import duke.Message; +import duke.task.*; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.Scanner; +import java.time.LocalDate; + +public class Storage { + private final String filePath; + + public Storage (String filePath) { + this.filePath = filePath; + File f = new File(filePath); + } + + public TaskList load() throws DukeException { + TaskList tasks = new TaskList(); + try { + File localFile = new File(this.filePath); + Scanner s = new Scanner(localFile); + while (s.hasNext()) { + String taskString = s.nextLine(); + if (taskString.strip().equals("")) { + continue; + } + tasks.add(makeTask(taskString)); + } + } catch (FileNotFoundException e) { + makeNewFile(this.filePath); + } + return tasks; + } + + private void makeNewFile(String filePath) throws DukeException { + String[] pathArray = filePath.split("/"); + String fileName = pathArray[pathArray.length - 1]; + String[] directories = Arrays.copyOfRange(pathArray, 0, pathArray.length - 1); + String directoryPath = String.join("/", directories); + File directory = new File(directoryPath); + File newFile = new File(fileName); + try { + directory.mkdirs(); + newFile.createNewFile(); + } catch (Exception e) { + throw new DukeException(Message.FILE_CREATE_ERROR); + } + } + + private Event makeEvent(String markStatus, String description, String at) { + Event newEvent = new Event(description, at); + newEvent.setMarkBasedOnSimpleStatus(markStatus); + return newEvent; + } + + private Deadline makeDeadline(String markStatus, String description, String by) throws DukeException { + try { + Deadline newDeadline = new Deadline(description, LocalDate.parse(by)); + newDeadline.setMarkBasedOnSimpleStatus(markStatus); + return newDeadline; + } catch (java.time.format.DateTimeParseException e) { + throw new DukeException(Message.FILE_READ_ERROR); + } + } + + private ToDo makeToDo(String markStatus, String description) { + ToDo newToDo = new ToDo(description); + newToDo.setMarkBasedOnSimpleStatus(markStatus); + return newToDo; + } + + //@@author Sampy147-reused + //Reused from https://github.com/nus-cs2103-AY1920S1/duke/pull/266/files + //with minor modifications + public Task makeTask(String taskString) throws DukeException { + Task newTask; + try { + String[] taskSegments = taskString.split("\\|"); + String taskIdentifier = taskSegments[0].strip(); + String taskMarkStatus = taskSegments[1].strip(); + String taskDescription = taskSegments[2].strip(); + switch (taskIdentifier) { + case "E": + newTask = makeEvent(taskMarkStatus, taskDescription, taskSegments[3].strip()); + break; + case "D": + newTask = makeDeadline(taskMarkStatus, taskDescription, taskSegments[3].strip()); + break; + case "T": + newTask = makeToDo(taskMarkStatus, taskDescription); + break; + default: + throw new DukeException(Message.FILE_READ_ERROR); + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new DukeException(Message.FILE_READ_ERROR); + } + return newTask; + } + //@@author + + //@@author Sampy147-reused + //Reused from https://github.com/nus-cs2103-AY1920S1/duke/pull/266/files + //with minor modifications + public void save(String taskString) throws DukeException { + try { + FileWriter fw = new FileWriter(this.filePath, true); + fw.write(taskString + "\n"); + fw.close(); + } catch (IOException e) { + throw new DukeException(Message.FILE_NOT_FOUND); + } + } + + public void save(TaskList tasks) throws DukeException { + try { + FileWriter fw = new FileWriter(this.filePath); + fw.write(tasks.toSimpleStrings()); + fw.close(); + } catch (IOException e) { + throw new DukeException(Message.FILE_NOT_FOUND); + } + } + //@@author +} diff --git a/src/main/java/duke/task/Deadline.java b/src/main/java/duke/task/Deadline.java new file mode 100644 index 0000000000..d2804d2b1d --- /dev/null +++ b/src/main/java/duke/task/Deadline.java @@ -0,0 +1,45 @@ +package duke.task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Represents a deadline task + */ +public class Deadline extends Task { + + protected LocalDate by; + + /** + * Creates a new deadline + * + * @param description the description of the deadline + * @param by the date of the deadline + */ + public Deadline(String description, LocalDate by) { + super(description); + this.by = by; + } + + /** + * Returns a string representation of this deadline + * + * @return string representation + */ + @Override + public String toString() { + return "[D]" + super.toString() + + " (by: " + this.by.format(DateTimeFormatter.ofPattern("MMM dd yyyy")) + ")"; + } + + /** + * Returns a simplified string representation of this deadline + * + * @return simplified string representation + */ + @Override + public String toSimpleString() { + return "D | " + super.toSimpleString() + " | " + this.by; + } + +} diff --git a/src/main/java/duke/task/Event.java b/src/main/java/duke/task/Event.java new file mode 100644 index 0000000000..4128abbee4 --- /dev/null +++ b/src/main/java/duke/task/Event.java @@ -0,0 +1,41 @@ +package duke.task; + +/** + * Represents a event task + */ +public class Event extends Task { + + protected String at; + + /** + * Creates a new event task + * + * @param description the description of the event + * @param at the place or time of the event + */ + public Event(String description, String at) { + super(description); + this.at = at; + } + + /** + * Returns a string representation of this event + * + * @return string representation + */ + @Override + public String toString() { + return "[E]" + super.toString() + " (at: " + this.at + ")"; + } + + /** + * Returns a simplified string representation of this event + * + * @return simplified string representation + */ + @Override + public String toSimpleString() { + return "E | " + super.toSimpleString() + " | " + this.at; + } + +} diff --git a/src/main/java/duke/task/Task.java b/src/main/java/duke/task/Task.java new file mode 100644 index 0000000000..9dbd6b51bb --- /dev/null +++ b/src/main/java/duke/task/Task.java @@ -0,0 +1,95 @@ +package duke.task; + +/** + * Represents a task unit. + * A task unit can be an event, deadline or to-do + */ +public class Task { + + protected String description; + protected boolean isDone; + + /** + * Creates a new task that is not done yet + * + * @param description the description of the task + */ + public Task(String description) { + this.description = description; + this.isDone = false; + } + + /** + * Returns a representation of whether a task is completed + * A "X" indicates a task is completed while a " " indicates an incomplete task + * + * @return whether task is done + */ + public String getStatusIcon() { + return (this.isDone ? "X" : " "); + } + + /** + * Returns a simple representation of whether a task is completed + * A "1" indicates a task is completed while a "0" indicates an incomplete task + * + * @return whether task is done + */ + public String getSimpleStatus() { + return (this.isDone ? "1": "0"); + } + + /** + * Marks the task as completed + */ + public void markAsDone() { + this.isDone = true; + } + + /** + * Unmarks the task which suggests task has yet to be completed + */ + public void unmark() { + this.isDone = false; + } + + /** + * Marks the task if the simple status index is "1" + * + * @param statusIndex the simple status index + */ + public void setMarkBasedOnSimpleStatus(String statusIndex) { + if (statusIndex.equals("1")) { + markAsDone(); + } + } + + /** + * Returns the description of the task + * + * @return task description + */ + public String getDescription() { + return this.description; + } + + /** + * Returns a string representation of this task + * + * @return string representation + */ + @Override + public String toString() { + return "[" + getStatusIcon() + "] " + this.description; + } + + /** + * Returns a simple string representation of this task + * + * @return simple string representation + */ + public String toSimpleString() { + return getSimpleStatus() + " | " + this.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..fa33943ddb --- /dev/null +++ b/src/main/java/duke/task/TaskList.java @@ -0,0 +1,160 @@ +package duke.task; + +import java.util.ArrayList; + +/** + * Represents a list of tasks + */ +public class TaskList { + + private ArrayList taskArray; + private int count = 0; + + /** + * Creates a new list of tasks + */ + public TaskList() { + this.taskArray = new ArrayList<>(); + } + + /** + * Creates a new list of tasks using an existing ArrayList of tasks + * + * @param taskArray the list of tasks represented as an ArrayList + */ + public TaskList(ArrayList taskArray) { + this.taskArray = taskArray; + } + + /** + * Adds a task into the list of tasks + * + * @param task the task to be added + */ + public void add(Task task){ + this.taskArray.add(task); + this.count += 1; + } + + /** + * Returns the task at a particular index in the list of tasks + * + * @param position the index of the task in the list of tasks + * @return the task at the given position + * @throws IndexOutOfBoundsException if given position > length of list of tasks or given position < 1 + */ + public Task getTask(int position) throws IndexOutOfBoundsException{ + return taskArray.get(position - 1); + } + + /** + * Returns the total number of tasks in the list of tasks + * + * @return number of tasks in list + */ + public int getCount(){ + return this.count; + } + + /** + * Marks a task at a particular position in the list of tasks as completed + * + * @param position the index of the task in the list of tasks + * @throws IndexOutOfBoundsException if given position > length of list of tasks or given position < 1 + */ + public void markTaskAtPos(int position) throws IndexOutOfBoundsException{ + Task currTask = getTask(position); + assert position <= this.count && position > 0 : + "Position argument should be more than 0 and less than or equal to the task list size"; + currTask.markAsDone(); + } + + /** + * Unmarks a task at a particular position in the list of tasks, representing the task is incomplete + * + * @param position the index of the task in the list of tasks + * @throws IndexOutOfBoundsException if given position > length of list of tasks or given position < 1 + */ + public void unmarkTaskAtPos(int position) throws IndexOutOfBoundsException{ + Task currTask = getTask(position); + assert position <= this.count && position > 0 : + "Position argument should be more than 0 and less than or equal to the task list size"; + currTask.unmark(); + } + + /** + * Deletes a task at a particular position in the list of tasks + * + * @param position the index of the task in the list of tasks + * @return the deleted task + * @throws IndexOutOfBoundsException the deleted task if given position > length of list of tasks + * or given position < 1 + */ + public Task deleteTaskAtPos(int position) throws IndexOutOfBoundsException { + Task deletedTask = getTask(position); + assert position <= this.count && position > 0 : + "Position argument should be more than 0 and less than or equal to the task list size"; + this.taskArray.remove(position - 1); + this.count -= 1; + return deletedTask; + } + + /** + * Returns a string listing all the tasks found that include the keyword parameters given + * in their descriptions + * + * @param keyword the word used to find tasks that have this keyword in their descriptions + * @return a list of tasks containing the keyword + */ + public String findTasks(String keyword) { + String foundTasks = ""; + int taskCount = 1; + for (Task task : this.taskArray) { + String description = task.getDescription(); + String[] words = description.split(" "); + for (String word : words) { + if (word.equals(keyword)) { + if (taskCount == 1) { + foundTasks += taskCount + ". " + task; + } else { + foundTasks += "\n" + taskCount + ". " + task; + } + taskCount++; + } + } + } + return foundTasks; + } + + /** + * Returns the simple string representation of the list of tasks + * + * @return simple string representation + */ + public String toSimpleStrings() { + String stringedList = ""; + for (int i = 0; i < this.count; i++) { + stringedList += getTask(i + 1).toSimpleString() + "\n"; + } + return stringedList; + } + + /** + * Returns the string representation of the list of tasks + * + * @return string representation + */ + @Override + public String toString() { + String stringedList = ""; + for (int i = 0; i < this.count; i++) { + if (i == this.count -1) { + stringedList += (i + 1) + ". " + getTask(i + 1).toString(); + } else { + stringedList += (i + 1) + ". " + getTask(i + 1).toString() + "\n"; + } + } + return "Here are the tasks in your list:\n" + stringedList; + } + +} diff --git a/src/main/java/duke/task/ToDo.java b/src/main/java/duke/task/ToDo.java new file mode 100644 index 0000000000..6d4c22e557 --- /dev/null +++ b/src/main/java/duke/task/ToDo.java @@ -0,0 +1,37 @@ +package duke.task; + +/** + * Represents a to-do task + */ +public class ToDo extends Task{ + + /** + * Creates a new to-do task + * + * @param description the description of this to-do + */ + public ToDo(String description) { + super(description); + } + + /** + * Returns a string representation of this to-do + * + * @return string representation + */ + @Override + public String toString(){ + return "[T]" + super.toString(); + } + + /** + * Returns a simplified string representation of this to-do + * + * @return string representation + */ + @Override + public String toSimpleString() { + return "T | " + super.toSimpleString(); + } + +} diff --git a/src/main/java/duke/ui/Ui.java b/src/main/java/duke/ui/Ui.java new file mode 100644 index 0000000000..8e3ac7c5e8 --- /dev/null +++ b/src/main/java/duke/ui/Ui.java @@ -0,0 +1,141 @@ +package duke.ui; + +import duke.Message; +import duke.task.Task; +import duke.task.TaskList; + +/** + * Represents a user interface to return messages to user + */ +public class Ui { + + private final TaskList tasks; + + /** + * Creates a user interface to display messages to user + * + * @param tasks the list of tasks + */ + public Ui(TaskList tasks) { + this.tasks = tasks; + } + + /** + * Returns a horizontal divider line + * + * @return the string representing the line + */ + public String showLine() { + return Message.HORIZONTAL_BORDER; + } + + /** + * Returns the message with horizontal dividers + * + * @param mainMessage the message to be displayed + * @return the message string + */ + public String showFullMessage(String mainMessage) { + return showLine() + "\n" + mainMessage + "\n" + showLine(); + } + + /** + * Returns the full welcome message + * + * @return the welcome message string + */ + public String showWelcome() { + return showFullMessage(Message.WELCOME_MESSAGE); + } + + /** + * Returns the full bye message + * + * @return the bye message string + */ + public String showBye() { + return showFullMessage(Message.BYE_MESSAGE); + } + + public String showHelp() { + return showFullMessage(Message.HELP_MESSAGE); + } + + public String showAdvancedHelp() { + return showFullMessage(Message.ADVANCED_HELP_MESSAGE); + } + + /** + * Returns the formatted full error message + * + * @param errorMessage the error message + * @return the formatted error message string + */ + public String showError(String errorMessage) { + return showFullMessage("OOPS :( !!! " + errorMessage); + } + + /** + * Returns the list of tasks + * + * @return the string of the list of tasks + */ + public String showList() { + return showFullMessage(this.tasks.toString()); + } + + /** + * Returns the addition message indicating a task has been successfully added + * + * @param task the task to be added + * @param totalTasks the total number of tasks + * @return the formatted addition message string + */ + public String showAddition(Task task, int totalTasks) { + return showFullMessage("Got it. I've added this task:\n" + task + + "\nNow you have " + totalTasks + " tasks in the list."); + } + + /** + * Returns the marked task message indicating a task has been successfully marked + * + * @param task the task to be marked + * @return the marked task message string + */ + public String showMarked(Task task) { + return showFullMessage("Nice! I've marked this task as done:\n" + task); + } + + /** + * Returns the unmarked task message indicating a task has been successfully unmarked + * + * @param task the task to be unmarked + * @return the un-marked task message string + */ + public String showUnmarked(Task task) { + return showFullMessage("OK, I've marked this task as not done yet:\n" + task); + } + + /** + * Returns the deleted task message indicating a task has been successfully deleted + * + * @param task the task to be deleted + * @return the deleted task message string + */ + public String showDeleted(Task task) { + return showFullMessage("Noted. I've removed this task:\n" + task + + "\nNow you have " + this.tasks.getCount() + " tasks in the list."); + } + + /** + * Returns the tasks that are found and formatting them in a message + * + * @param foundTasks the string of found tasks + * @return the found task message string + */ + public String showFound(String foundTasks) { + return showFullMessage("Here are the matching tasks in your list:\n" + foundTasks); + } + + +} diff --git a/src/main/resources/images/DaDuke.png b/src/main/resources/images/DaDuke.png new file mode 100644 index 0000000000..d893658717 Binary files /dev/null and b/src/main/resources/images/DaDuke.png differ diff --git a/src/main/resources/images/DaUser.png b/src/main/resources/images/DaUser.png new file mode 100644 index 0000000000..3c82f45461 Binary files /dev/null and b/src/main/resources/images/DaUser.png differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..e851d389b5 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ 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..262b34a83f --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + +