diff --git a/README.md b/README.md index 8715d4d915..74a398fa33 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,43 @@ -# Duke project template +# Duke -This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. +Duke is a interactive chatbot with UI. +> "Your mind is for having ideas, not holding them" - David Allen -## Setting up in Intellij +Duke is a task tracker that can track three categories of tasks: +- Todo +- Deadline +- Event -Prerequisites: JDK 11, update Intellij to the most recent version. +## Setting up -1. Open Intellij (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project first) -1. Open the project into Intellij as follows: - 1. Click `Open`. - 1. Select the project directory, and click `OK`. - 1. If there are any further prompts, accept the defaults. -1. Configure the project to use **JDK 11** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
- In the same dialog, set the **Project language level** field to the `SDK default` option. -3. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: +Setting up Duke is extremely easy. All you need to do is: +1) Download JDK 11 and above from [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk) +2) Download the latest JAR file release from the right side of this repository +3) Run the command java -jar ip.jar + +It's that simple! :) + +## Commands +There are a few commands you can run in Duke. These include: +- [x] todo +- [X] deadline +- [X] event +- [X] delete +- [X] list +- [X] find +- [X] bye + +## Using an IDE +You can further edit/modify Duke by navigating to the `src` folder which contains all the `java` files + +You can run it from Launcher.java by using __main__: +
+public class Launcher {
+    public static void main(String[] args) {
+        Application.launch(Main.class,args);
+    }
+}
+
``` Hello from ____ _ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..133badba50 --- /dev/null +++ b/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'com.github.johnrengelman.shadow' version '5.1.0' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.5.0' + String javaFxVersion = '11' + + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClassName = "duke.Duke" +} + +shadowJar { + archiveBaseName = "duke" + archiveClassifier = null +} + +checkstyle { + toolVersion = '8.29' +} + +run{ + standardInput = System.in + enableAssertions = true; +} diff --git a/docs/README.md b/docs/README.md index 8077118ebe..61cb7553bd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,135 @@ # User Guide +Duke is a interactive chatbot with UI. +> “Software and cathedrals are much the same; first we build them, then we pray.” + +Duke is a task tracker that can track three categories of tasks: +- Todo +- Deadline +- Event ## Features +1. `todo` +2. `deadline` +3. `event` +4. `list` +5. `delete` +6. `mark` +7. `unmark` +8. `snooze` +9. `bye` -### Feature-ABC +## Usage -Description of the feature. +## `todo NAME` - creates a Todo task with name NAME -### Feature-XYZ +Example of usage: -Description of the feature. +`todo work` -## Usage +Expected outcome: -### `Keyword` - Describe action +Expected outcome: +Creates a new Todo object called work +*Take note of the space between todo and NAME! -Describe the action and its outcome. -Example of usage: +## `deadline NAME /by DATE TIME(optional)` - creates a Deadline task with name NAME at DATE and TIME + +Example of usage: +``` +deadline report submission /by 2022-02-14 23:59 +deadline report submission /by 2022-02-14 +``` + +Expected outcome: +Creates a new Deadline object with DATE and TIME(optional) +*Format of date: yyyy-mm-dd +*Format of time: hh:mm + + +## `event NAME /at DATE TIME(optional)` - creates a Event task with name NAME at DATE and TIME + +Example of usage: +``` +event meeting /at 2022-02-12 01:00 +event meeting /at 2022-02-12 +``` + +Expected outcome: +Creates a new Event object with DATE and TIME(optional) +*Format of date: yyyy-mm-dd +*Format of time: hh:mm + + +## `list` - lists out all tasks + +Example of usage: +``` +list +``` + +Expected outcome: +A list of all tasks in their respective formats + + +## `delete INDEX` - deletes task at specific INDEX -`keyword (optional arguments)` +Example of usage: +``` +delete 1 +``` + +Expected outcome: +Delete task from tasklist and prints the deleted task +*Recommended to use alongside `list` command so that you get the right index +*Important to delete a valid index - Do not go out of bounds! + + +## `mark INDEX` - marks task as done + +Example of usage: +``` +mark 0 +``` + +Expected outcome: +Marks task and prints the marked task +*Recommended to use alongside `list` command so that you get the right index + + +## `unmark INDEX` - marks task as undone + +Example of usage: +``` +unmark 0 +``` + +Expected outcome: +Marks task and prints the marked task +*Recommended to use alongisde `list` command so that you get the right index + + +## `snooze NAME DATE /t NEWDATE NEWTIME` - changes the date & time of task + +Example of usage: +``` +snooze meeting 2022-02-12 /t 2022-03-20 23:59 +``` Expected outcome: +Sets new task date and time and prints new date and time for task + +_ _Keeping track of the exact time is difficult_ _, which is why all you need to input is the date of the task and not the time. +*Getting the exact name and date of the task is important + -Description of the outcome. +## `bye` - ends the program +Example of usage: ``` -expected output +bye ``` + +Expected outcome: +Duke sends a goodbye message and exits the program, closing the UI + diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..342abd51f1 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/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..918fec1efb --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: duke.duke.Duke + diff --git a/src/main/java/duke/duke/Duke.java b/src/main/java/duke/duke/Duke.java new file mode 100644 index 0000000000..6944549bd7 --- /dev/null +++ b/src/main/java/duke/duke/Duke.java @@ -0,0 +1,176 @@ +package duke.duke; + +import duke.ui.DukeException; +import duke.ui.InputHandler; +import duke.ui.DialogBox; + + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import javafx.scene.layout.Region; +import javafx.scene.image.Image; + +import java.io.IOException; +import java.util.Scanner; + +public class Duke extends Application { + + //For GUI + private ScrollPane scrollPane; + private VBox dialogBox; + private TextField userInput; + private Button sendButton; + private Scene scene; + + //Images for Duke & user + private Image user = new Image(this.getClass().getResourceAsStream("/images/human.jpg")); + private Image duke = new Image(this.getClass().getResourceAsStream("/images/bear.jpg")); + + + public static void main(String[] args) throws IOException { + String dukeGreeting = "Hello! I'm Duke \nWhat can I do for you?"; + String endMessage = "Bye. Hope to see you again soon!"; + + System.out.println(dukeGreeting); + Scanner sc = new Scanner(System.in); + InputHandler inputHandler = new InputHandler(); + String response = ""; + while (!response.equals(endMessage)) { + try { + String input = sc.nextLine(); + System.out.println(response); + response = inputHandler.handleInput(input); + } catch (DukeException e) { + System.out.println(e.getMessage()); + } + } + } + + + /** + * Start the application + * @param stage Stage object + */ + @Override + public void start(Stage stage) { + //Setting up required components + Label dukeLabel = new Label("Duke"); + + //Creating container for the chat to scroll + scrollPane = new ScrollPane(); + dialogBox = new VBox(); + scrollPane.setContent(dialogBox); + + userInput = new TextField(); + sendButton = new Button("Send"); + + AnchorPane mainLayout = new AnchorPane(); + mainLayout.getChildren().addAll(scrollPane, userInput, sendButton); + + Scene scene = new Scene(mainLayout); + stage.setScene(scene); + stage.show(); + + //Formatting window + stage.setTitle("Duke"); + stage.setResizable(false); + stage.setMinHeight(600.0); + stage.setMinWidth(400.0); + + mainLayout.setPrefSize(400.0, 600.0); + scrollPane.setPrefSize(385, 535); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS); + + scrollPane.setVvalue(1.0); + scrollPane.setFitToWidth(true); + + dialogBox.setPrefHeight(Region.USE_COMPUTED_SIZE); + + userInput.setPrefWidth(325.0); + userInput.setPrefHeight(55.0); + + AnchorPane.setTopAnchor(scrollPane, 1.0); + AnchorPane.setBottomAnchor(sendButton, 1.0); + AnchorPane.setRightAnchor(sendButton, 1.0); + AnchorPane.setLeftAnchor(userInput, 1.0); + AnchorPane.setBottomAnchor(userInput, 1.0); + + //Set functionality on User Input + sendButton.setOnMouseClicked((event) -> { + handleUserInput(); + }); + + userInput.setOnAction((event) -> { + handleUserInput(); + }); + + //scroll down if dialogBox's height changes + dialogBox.heightProperty().addListener((observable) -> scrollPane.setVvalue(1.0)); + + } + + /** + * Creates the dialogboxes as well as handles user and duke's inputs + */ + private void handleUserInput() { + + String userText = userInput.getText(); + String dukeText = getResponse(userInput.getText()); + dialogBox.getChildren().addAll( + DialogBox.getUserDialog(userText, user), + DialogBox.getDukeDialog(dukeText, duke) + ); + userInput.clear(); + } + + /** + * Returns the Response of Duke to user input + * + * @param input user's input + * @return Duke's reply to user's input + */ + public String getResponse(String input) { + String output = ""; + try { + InputHandler inputHandler = new InputHandler(); + output = inputHandler.handleInput(input); + } catch (DukeException e) { + return e.getMessage(); + } catch (IOException e) { + return e.getMessage(); + } + + return output; + } + + /** + * Returns a Label around the text + * + * @param text text for the dialog + * @return Label object with text + */ + private Label getDialogLabel(String text) { + Label textToAdd = new Label(text); + textToAdd.setWrapText(true); + return textToAdd; + } + + +} + + + + + + + + + diff --git a/src/main/java/duke/duke/Launcher.java b/src/main/java/duke/duke/Launcher.java new file mode 100644 index 0000000000..c4f9be716e --- /dev/null +++ b/src/main/java/duke/duke/Launcher.java @@ -0,0 +1,12 @@ +package duke.duke; + +import javafx.application.Application; + +/** + * Launches the Application for Duke + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class,args); + } +} diff --git a/src/main/java/duke/duke/Main.java b/src/main/java/duke/duke/Main.java new file mode 100644 index 0000000000..ee17ea41d4 --- /dev/null +++ b/src/main/java/duke/duke/Main.java @@ -0,0 +1,38 @@ +package duke.duke; + +import duke.ui.MainWindow; + +import java.io.IOException; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +/** + * A GUI for Duke using FXML. + */ +public class Main extends Application { + + private Duke duke = new Duke(); + + /** + * Starts the application + * + * @param stage stage object passed in + */ + @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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/duke/storage/Storage.java b/src/main/java/duke/storage/Storage.java new file mode 100644 index 0000000000..7b1c48b8f7 --- /dev/null +++ b/src/main/java/duke/storage/Storage.java @@ -0,0 +1,229 @@ +package duke.storage; + +import duke.ui.DukeException; +import duke.task.Event; +import duke.task.Task; +import duke.task.Todo; +import duke.task.Deadline; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.nio.file.Files; +import java.nio.file.Path; +import java.io.FileWriter; + +import java.util.Scanner; + +public class Storage { + + private TaskList taskList; + + final String symbolForMarked = "[1]"; + final String symbolForUnMarked = "[0]"; + final String symbolForTask = "[T]"; + final String symbolForDeadline = "[D]"; + final String symbolForEvent = "[E]"; + final String FILEPATH = "data/data.txt"; + final String FILEDIRECTORY = "data"; + + /** + * Constructs a Storage object from data in data.txt file. + * + * @throws IOException Issue when writing or reading from data.txt + */ + public Storage() throws IOException { + try { + File dataFile = new File(FILEPATH); + Scanner sc = new Scanner(dataFile); + TaskList newTaskList = new TaskList(); + while (sc.hasNextLine()) { + String nextLine = sc.nextLine(); + String[] taskLineSplitBySpace = nextLine.split(" "); + String taskType = taskLineSplitBySpace[0]; + String[] taskLineSplitBySlash = nextLine.split(" / "); + + newTaskList.add(convertToTask(taskType, taskLineSplitBySlash, taskLineSplitBySpace)); + } + this.taskList = newTaskList; + } catch (IOException e){ + Path filePath = Paths.get(FILEDIRECTORY); + boolean dataDirectoryExists = Files.exists(filePath); + if (!dataDirectoryExists) { + new File(FILEDIRECTORY).mkdir(); + } + new File(FILEPATH).createNewFile(); + this.taskList = new TaskList(); + } catch (DukeException e) { + //Wrong format of tasks in data.txt + System.out.println(e.getMessage()); + } + } + + /** + * Comprehends and converts input from data.txt into Task object + * + * @param taskType Type of Task: Deadline, Event or Todo + * @param taskLineSplitBySlash Format of the line in a String array format split by slashes + * @param taskLineSplitBySpace Format of the line in a String array format split by spaces + * @return Task object to be put into TaskList + * @throws DukeException If tasks are stored in wrong format in data.txt + */ + public Task convertToTask(String taskType, String[] taskLineSplitBySlash, String[] taskLineSplitBySpace) throws DukeException { + + String wrongFormatError = "Tasks stored in wrong format"; + + switch (taskType) { + case symbolForTask: + Todo newTodo = new Todo(taskLineSplitBySlash[1]); + if (taskLineSplitBySpace[1].equals(symbolForMarked)) { + newTodo.setMarkedTask(); + } + return newTodo; + + case symbolForDeadline: + //Checks whether there is a time component for the stored Deadline + Deadline newDeadline = (taskLineSplitBySlash[3].equals("null")) + ? new Deadline(taskLineSplitBySlash[1], taskLineSplitBySlash[2]) + : new Deadline(taskLineSplitBySlash[1], taskLineSplitBySlash[2], taskLineSplitBySlash[3]); + + if (taskLineSplitBySpace[1].equals(symbolForMarked)) { + newDeadline.setMarkedTask(); + } + return newDeadline; + + case symbolForEvent: + //Checks whether there is a time component for the stored Event + Event newEvent = (taskLineSplitBySlash[3].equals("null")) + ? new Event(taskLineSplitBySlash[1], taskLineSplitBySlash[2]) + : new Event(taskLineSplitBySlash[1], taskLineSplitBySlash[2], taskLineSplitBySlash[3]); + + if (taskLineSplitBySpace[1].equals(symbolForMarked)) { + newEvent.setMarkedTask(); + } + return newEvent; + + default: + throw new DukeException(wrongFormatError); + } + } + + /** + * Converts task to string format for storage in the data.txt file for writing into the txt file + * + * @param task Task to be converted and written + * @return String format of the task eg: [D] [1] / deadline / duedate / duetime + */ + public String taskToStringConverter(Task task) { + String output = ""; + if (task instanceof Todo) { + String mark = (task.hasBeenMarked()) ? symbolForMarked : symbolForUnMarked; + output = symbolForTask + " " + mark + " / " + task.name + "\n"; + } else if (task instanceof Deadline) { + Deadline deadline = (Deadline) task; + String mark = (deadline.hasBeenMarked()) ? symbolForMarked : symbolForUnMarked; + output = symbolForDeadline + " " + mark + " / " + deadline.name + " / " + deadline.getDueDate() + " / " + + deadline.getDueTime() + "\n"; + } else if (task instanceof Event) { + Event event = (Event) task; + String mark = (event.hasBeenMarked()) ? symbolForMarked : symbolForUnMarked; + output = symbolForEvent + " " + mark + " / " + event.name + " / " + event.getDueDate() + " / " + event.getDueTime() + "\n"; + } + return output; + } + + /** + * Rewrites entire storage based on current taskList + * + * @throws IOException If there is an error with writing to data.txt + */ + public void rewriteData() throws IOException { + FileWriter fw = new FileWriter(this.FILEPATH); + for (int i = 0; i < this.taskListSize(); i++) { + Task task = this.taskList.get(i); + fw.write(taskToStringConverter(task)); + } + fw.close(); + } + + /** + * Appends a single task to the file + * + * @param task task to be added to the data.txt file + * @throws IOException if there is an error appending the task to data.txt + */ + public void writeData(Task task) throws IOException { + this.taskList.add(task); + FileWriter fw = new FileWriter(this.FILEPATH, true); + System.out.println(new File(this.FILEPATH).getAbsolutePath()); + fw.write(taskToStringConverter(task)); + fw.close(); + } + + /** + * Deletes the entire file and rewrites it based on the new taskList. + * Amends the current stored TaskList as well. + * + * @param idx index of task to be deleted + * @throws IOException if there is an error rewriting data.txt + */ + public void deleteData(int idx) throws IOException { + taskList.remove(idx); + FileWriter fw = new FileWriter(this.FILEPATH); + for (int i = 0; i < this.taskList.size(); i++) { + Task task = this.taskList.get(i); + fw.write(taskToStringConverter(task)); + } + fw.close(); + } + + /** + * Obtains list of tasks from this.taskList and returns it + * + * @return String that lists out the tasks currently + */ + public String list() { + String listOfTasks = ""; + for (int i = 1; i <= this.taskList.size(); i++) { + Task task = this.taskList.get(i - 1); + if (task.hasBeenMarked()) { + listOfTasks += i + ". " + task + "\n"; + } else { + listOfTasks += i + ". " + task + "\n"; + } + } + if (listOfTasks.equals("")) { + return "You currently have no tasks. Yay! :)"; + } + return listOfTasks; + } + + /** + * Gets task from Storage + * + * @param idx index of task to be gotten + * @return task that is requested + */ + public Task get(int idx) { + return this.taskList.get(idx); + } + + /** + * Size of TaskList + * + * @return size of task list + */ + public int taskListSize() { + return this.taskList.size(); + } + + /** + * Allows external access to TaskList + * + * @return TaskList + */ + public TaskList accessTaskList() { + return this.taskList; + } + +} diff --git a/src/main/java/duke/storage/TaskList.java b/src/main/java/duke/storage/TaskList.java new file mode 100644 index 0000000000..fc80253fab --- /dev/null +++ b/src/main/java/duke/storage/TaskList.java @@ -0,0 +1,53 @@ +package duke.storage; + +import duke.task.Task; + +import java.util.ArrayList; +public class TaskList { + + public ArrayList list; + + /** + * Constructor for TaskList. Initialises an empty ArrayList + */ + public TaskList() { + this.list = new ArrayList<>(); + } + + /** + * Adds Task to TaskList + * + * @param task Task to be added + */ + public void add(Task task) { + this.list.add(task); + } + + /** + * Removes Task from TaskList + * + * @param index index of Task to be removed + */ + public void remove(int index) { + this.list.remove(index); + } + + /** + * Gets task by index + * + * @param index index of Task to be retrieved + * @return Retrieved Task + */ + public Task get(int index) { + return this.list.get(index); + } + + /** + * Returns Size of TaskList + * + * @return integer size of TaskList + */ + public int size() { + return this.list.size(); + } +} diff --git a/src/main/java/duke/task/Deadline.java b/src/main/java/duke/task/Deadline.java new file mode 100644 index 0000000000..1b54c44e9f --- /dev/null +++ b/src/main/java/duke/task/Deadline.java @@ -0,0 +1,90 @@ +package duke.task; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; + +/** + * Represents a Deadline which is a subclass of Task + * Includes a dueDate attribute. Overrides toString() from Task + */ +public class Deadline extends Task { + private LocalDate dueDate; + private LocalTime dueTime; + + /** + * Constructor for Deadline with date + * + * @param name Name of Deadline + * @param date Date of deadline in yyyy-mm-dd format + * @throws DateTimeParseException If date is not in yyyy-mm-dd format + */ + public Deadline(String name, String date) throws DateTimeParseException { + super(name); + this.dueDate = LocalDate.parse(date); + this.dueTime = null; + } + + /** + * Constructor for Deadline with date and time + * + * @param name Name of Deadline + * @param date Date of deadline in yyyy-mm-dd format + * @param time Time of deadline in hh:mm format + * @throws DateTimeParseException If date is not in yyyy-mm-dd format AND/OR time is not in hh:mm format + */ + public Deadline(String name, String date, String time) throws DateTimeParseException { + super(name); + this.dueDate = LocalDate.parse(date); + this.dueTime = LocalTime.parse(time); + } + + /** + * Returns the dueDate of this Deadline object + * + * @return LocalDate object of dueDate + */ + public LocalDate getDueDate() { + return this.dueDate; + } + + /** + * Returns the dueTime of this Deadline object + * + * @return LocalTime object of dueTime + */ + public LocalTime getDueTime() { + return this.dueTime; + } + + /** + * Changes dueDate to new LocalDate + * + * @param date LocalDate object of new date + */ + public void changeDueDate(LocalDate date) { + this.dueDate = date; + } + + /** + * Changes dueTime to new LocalTime + * + * @param time LocalTIme object of new time + */ + public void changeDueTime(LocalTime time) { + this.dueTime = time; + } + + /** + * Returns String representation of Deadline + * + * @return String of Deadline task, eg [D][0] Deadline (by: 22 Feb 2022 2:22pm) vs [D][1] Deadline (by: 22 Feb 2022 2:22pm) + */ + @Override + public String toString() { + String dueDateAndTime = (this.dueTime == null) + ? dateConverterToString(this.dueDate) + : dateConverterToString(this.dueDate) + " " + timeConverterToString(this.dueTime); + return "[D]" + super.toString() + " (by: " + dueDateAndTime + ")"; + } +} \ No newline at end of file diff --git a/src/main/java/duke/task/Event.java b/src/main/java/duke/task/Event.java new file mode 100644 index 0000000000..5e5e040535 --- /dev/null +++ b/src/main/java/duke/task/Event.java @@ -0,0 +1,90 @@ +package duke.task; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; + +/** + * Represents a Event which is a subclass of Task + * Overrides toString() from Task + */ +public class Event extends Task { + private LocalDate dueDate; + private LocalTime dueTime; + + /** + * Constructor for Event. Takes in name and date but no time. + * + * @param name name of the Event + * @param date date in yyyy-mm-dd format only + * @throws DateTimeParseException If date format is not in yyyy-mm-dd format + */ + public Event (String name, String date) throws DateTimeParseException { + super(name); + this.dueDate = LocalDate.parse(date); + this.dueTime = null; + } + + /** + * Constructor for Event. Includes name, date and time + * + * @param name name of the Event + * @param date date in yyyy-mm-dd format only + * @param time time in hh:mm format + * @throws DateTimeParseException if date format is not in yyyy-mm-dd format AND/OR time is not in hh:mm format + */ + public Event (String name, String date, String time) throws DateTimeParseException { + super(name); + this.dueDate = LocalDate.parse(date); + this.dueTime = LocalTime.parse(time); + } + + /** + * Returns the LocalDate associated with the Event object + * + * @return LocalDate object of the dueDate + */ + public LocalDate getDueDate() { + return this.dueDate; + } + + /** + * Returns the dueTime of this Event object + * + * @return LocalTime object of dueTime + */ + public LocalTime getDueTime() { + return this.dueTime; + } + + /** + * Changes dueDate to new LocalDate + * + * @param date LocalDate object of new date + */ + public void changeDueDate(LocalDate date) { + this.dueDate = date; + } + + /** + * Changes dueTime to new LocalTime + * + * @param time LocalTIme object of new time + */ + public void changeDueTime(LocalTime time) { + this.dueTime = time; + } + + /** + * Returns String representation of Event + * + * @return String of Event task, eg: [E][0] Event (at: 22 Feb 2022 2:22pm) vs [E][1] Event (at: 22 Feb 2022 2:22pm) + */ + @Override + public String toString() { + String dueDateAndTime = (this.dueTime == null) + ? dateConverterToString(this.dueDate) + : dateConverterToString(this.dueDate) + " " + timeConverterToString(this.dueTime); + return "[E]" + super.toString() + " (at: " + dueDateAndTime + ")"; + } +} \ No newline at end of file diff --git a/src/main/java/duke/task/Task.java b/src/main/java/duke/task/Task.java new file mode 100644 index 0000000000..6c71c5f86c --- /dev/null +++ b/src/main/java/duke/task/Task.java @@ -0,0 +1,83 @@ +package duke.task; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +/** + * Represents a Task. Contains a Task constructor, two methods to mark and unmark tasks, toString() method as well as a isMark() method to check if Task is marked + */ +public class Task { + private boolean isMarked; + public String name; + + /** + * Constructor for Task + * + * @param name name of the task + */ + public Task (String name) { + this.name = name; + this.isMarked = false; + } + + /** + * Marks current Task object as done + */ + public void setMarkedTask () { + this.isMarked = true; + } + + /** + * Unmarks current Task object + */ + public void setUnmarkedTask() { + this.isMarked = false; + } + + /** + * Returns whether current Task object has been marked + * + * @return boolean on whether task is marked + */ + public boolean hasBeenMarked() { + return this.isMarked; + } + + /** + * Converts date to String format for display + * + * @param date LocalDate for Deadline/Event tasks + * @return String format: converts from yyyy-mm-dd format to mmm dd yyyy format, eg: Aug 21 2022 + */ + public String dateConverterToString(LocalDate date) { + return DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).format(date); + } + + /** + * Converts time to String format for display + * + * @param time LocalTime for Deadline/Event tasks + * @return String format: converts hh:mm format to hh:mm am/pm format eg: 1:30pm + */ + public String timeConverterToString(LocalTime time) { + return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(time); + } + + /** + * Returns String representation of Task + * + * @return String version of task, with marked and name. E.g. [0] Task vs [1] Task + */ + @Override + public String toString() { + if (this.isMarked) { + String marked = "[1] "; + return marked + this.name; + } else { + String unmarked = "[0] "; + return unmarked + this.name; + } + } +} \ No newline at end of file diff --git a/src/main/java/duke/task/Todo.java b/src/main/java/duke/task/Todo.java new file mode 100644 index 0000000000..55cc2e600b --- /dev/null +++ b/src/main/java/duke/task/Todo.java @@ -0,0 +1,22 @@ +package duke.task; + +/** + * Represents a Todo which is a subclass of Task + * Includes a dueDate attribute. Overrides toString() from Task + */ +public class Todo extends Task { + public Todo (String name) { + super(name); + } + + /** + * Returns String representation of Todo + * + * @return String of Todo task, eg: [T][1] Todo + */ + @Override + public String toString() { + return "[T]" + super.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/duke/ui/DialogBox.java b/src/main/java/duke/ui/DialogBox.java new file mode 100644 index 0000000000..e43bc2c8af --- /dev/null +++ b/src/main/java/duke/ui/DialogBox.java @@ -0,0 +1,80 @@ +package duke.ui; + +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; + +public class DialogBox extends HBox { + + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + final String dialogBoxFxmlLocation = "/view/DialogBox.fxml"; + + /** + * Constructor for DialogBox. Contains text and an image for Duke/User + * + * @param text Text to be displayed + * @param img Image to be displayed + */ + private DialogBox(String text, Image img) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource(dialogBoxFxmlLocation)); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(img); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + } + + /** + * Returns new DialogBox for user with text contained + * + * @param text User's text + * @param img Image for the user + * @return DialogBox to be displayed + */ + public static DialogBox getUserDialog(String text, Image img) { + return new DialogBox(text, img); + } + + /** + * Returns new DialogBox for Duke with response contained + * + * @param text User's text + * @param img Image for Duke + * @return DialogBox to be displayed + */ + public static DialogBox getDukeDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + return db; + } +} \ No newline at end of file diff --git a/src/main/java/duke/ui/DukeException.java b/src/main/java/duke/ui/DukeException.java new file mode 100644 index 0000000000..cc9a84cc04 --- /dev/null +++ b/src/main/java/duke/ui/DukeException.java @@ -0,0 +1,38 @@ +package duke.ui; +import duke.duke.Duke; +import duke.ui.Parser; +import duke.ui.DukeException; +import duke.ui.InputHandler; +import duke.storage.Storage; +import duke.storage.TaskList; +import duke.task.Event; +import duke.task.Task; +import duke.task.Todo; +import duke.task.Deadline; + +/** + * Custom DukeException to be handled by InputHandler + */ +public class DukeException extends Exception{ + + private String errorMessage; + + /** + * Constructs a DukeException. DukeException handles wrong inputs by user + * + * @param errorMessage Error message to be printed + */ + public DukeException (String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * Gets error message + * + * @override Returns customised error message for DukeException when input is incorrect + * @return String errorMessage + */ + public String getMessage() { + return this.errorMessage; + } +} diff --git a/src/main/java/duke/ui/InputHandler.java b/src/main/java/duke/ui/InputHandler.java new file mode 100644 index 0000000000..6264dacc0b --- /dev/null +++ b/src/main/java/duke/ui/InputHandler.java @@ -0,0 +1,184 @@ +package duke.ui; + +import duke.storage.Storage; + +import duke.task.Event; +import duke.task.Task; +import duke.task.Todo; +import duke.task.Deadline; + +import java.io.IOException; + +/** + * Handles input from user + * Processes the input into 7 categories: Todo, Event, Deadline, list, mark, unmark, bye + */ +public class InputHandler { + private Storage storage; + private Parser parser; + + //Error messages + final String unableToSnoozeErrorMessage = "Wrong usage of snooze! Correct usage: snooze [name]" + + " [time](required if deadline/event"; + final String unableToFindErrorMessage = "Uh oh! It seems like you did not specify what to find"; + final String unableToDeleteErrorMessage = "Wrong usage of delete! Correct usage: delete [index]"; + final String unableToUnmarkErrorMessage = "Wrong usage of unmark! Correct usage: unmark [index]"; + final String unableToMarkErrorMessage = "Wrong usage of mark! Correct usage: mark [index]"; + final String unableToListErrorMessage = "Wrong usage of list! Correct usage: list"; + + /** + * Constructs an InputHandler and loads data into Storage object + * + * @throws IOException If Storage class fails to initialise + */ + public InputHandler() throws IOException { + this.storage = new Storage(); + this.parser = new Parser(); + } + + + /** + * Handles input from Duke.java. Breaks up the String input into proper parts and parses them. + * + * @param input String input from user input. + * @return String output from Duke as response to user. + * @throws DukeException For invalid input types, or unrecognisable commands. + */ + public String handleInput(String input) throws DukeException, IOException { + String endMessage = "Bye. Hope to see you again soon!"; + + String[] splitInput = input.split(" "); + String inputCommand = splitInput[0]; + + switch (inputCommand) { + case "todo": + return taskCaseHandler(CommandType.TODO, splitInput ); + case "event": + return taskCaseHandler(CommandType.EVENT, splitInput); + case "deadline": + return taskCaseHandler(CommandType.DEADLINE, splitInput); + case "list": + //Confirms that input command is simply "list" + if (splitInput.length == 1) { + return parser.parse(CommandType.LIST, this.storage, splitInput); + } else { + throw new DukeException(unableToListErrorMessage); + } + case "mark": + //Confirms that input is in the format mark [index] + if (splitInput.length == 2) { + return this.parser.parse(CommandType.MARK, this.storage, splitInput); + } else { + throw new DukeException(unableToMarkErrorMessage); + } + case "unmark": + //Confirms that input is in the format mark [index] + if (splitInput.length == 2) { + return this.parser.parse(CommandType.UNMARK, this.storage, splitInput); + } else { + throw new DukeException(unableToUnmarkErrorMessage); + } + case "delete": + //Confirms that input is in the format mark [index] + if (splitInput.length == 2) { + return this.parser.parse(CommandType.DELETE, this.storage, splitInput); + } else { + throw new DukeException(unableToDeleteErrorMessage); + } + case "find": + if (splitInput.length > 1) { + return this.parser.parse(CommandType.FIND, this.storage, splitInput); + } else { + throw new DukeException(unableToFindErrorMessage); + } + case "snooze": + String splitInputBySlash[] = input.split(" /t "); + if (splitInputBySlash.length > 1 && splitInput.length > 5) { + return parser.parse(CommandType.SNOOZE, this.storage, splitInputBySlash); + } else { + throw new DukeException(unableToSnoozeErrorMessage); + } + case "bye": + return endMessage; + default: + throw new DukeException(":( OOPS!!! I'm sorry, but I don't know what that means! Possible commands: " + + "todo [task], event [task] /at [time]," + + " deadline [task] /by [time], mark [index], unmark [index], delete [index], bye"); + } + + } + + /** + * Handles Todo, Event and Deadline CommandTypes. Writes the task to storage and returns Duke's reply. + * + * @param typeOfTask Type of Task, Todo, Event or Deadline. + * @param splitInput String array of user input, split by spaces. + * @return String format of Duke's reply. + * @throws DukeException If CommandType passed in is wrong. + * @throws IOException If there is an issue writing to Storage. + */ + public String taskCaseHandler(CommandType typeOfTask, String[] splitInput) throws DukeException, IOException { + String emptyDescription = ":( OOPS!!! The description of a todo cannot be empty. Correct usage: "; + String todoFormat = "todo [name]"; + String eventFormat = "event [name] /at [date] [time(optional)]"; + String deadlineFormat = "deadline [name] /by /by [date] [time(optional)]"; + switch(typeOfTask) { + case TODO: + //Confirms that input is in the format: todo [task] + if (splitInput.length > 1) { + Todo newTodo = (Todo) parser.parse(CommandType.TODO, splitInput); + this.storage.writeData(newTodo); + return addTaskMessage(newTodo); + } else { + throw new DukeException(emptyDescription + todoFormat); + } + case DEADLINE: + //Confirms that input is in the format: deadline [task] /by [date] [time(optional)] + if (splitInput.length > 3) { + Deadline newDeadline = (Deadline) parser.parse(CommandType.DEADLINE, splitInput); + this.storage.writeData(newDeadline); + return addTaskMessage(newDeadline); + } else { + throw new DukeException(emptyDescription + deadlineFormat); + } + case EVENT: + //Confirms that input is in the format: event [task] /at [date] [time(optional)] + if (splitInput.length > 3) { + Event newEvent = (Event) parser.parse(CommandType.EVENT, splitInput); + this.storage.writeData(newEvent); + return addTaskMessage(newEvent); + + } else { + throw new DukeException(emptyDescription + eventFormat); + } + default: + throw new DukeException("Incorrect format. Should never reach this stage"); + } + } + + /** + * Prints out the task name that has been added as well as the number of tasks in the list. + * + * @param task The task that has been added + */ + public String addTaskMessage(Task task) { + return "Got it. I've added this task:\n" + task + "\nNow you have " + this.storage.taskListSize() + + " tasks in the list." ; + } + + /** + * Types of commands accepted by Duke + */ + enum CommandType { + TODO, + EVENT, + DEADLINE, + LIST, + MARK, + UNMARK, + DELETE, + FIND, + SNOOZE + } +} + diff --git a/src/main/java/duke/ui/MainWindow.java b/src/main/java/duke/ui/MainWindow.java new file mode 100644 index 0000000000..abd97d7df3 --- /dev/null +++ b/src/main/java/duke/ui/MainWindow.java @@ -0,0 +1,71 @@ +package duke.ui; + +import duke.duke.Duke; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +/** + * 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; + + //setting the images + private Image userImage = new Image(this.getClass().getResourceAsStream("/images/human.jpg")); + private Image dukeImage = new Image(this.getClass().getResourceAsStream("/images/bear.jpg")); + + /** + * Initialises scrollpane + */ + @FXML + public void initialize() { + String dukeGreeting = "Hello! I'm Duke ^^ \nWhat can I do for you?"; + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + dialogContainer.getChildren().addAll(DialogBox.getDukeDialog(dukeGreeting, dukeImage)); + } + + /** + * Sets duke + * + * @param d Duke object passed in + */ + public void setDuke(Duke d) { + duke = d; + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + @FXML + private void handleUserInput() { + String endMessage = "Bye. Hope to see you again soon!"; + String sleepErrorMessage = "Issue shutting down Duke! Force shutting down..."; + + String input = userInput.getText(); + String response = duke.getResponse(input); + dialogContainer.getChildren().addAll(DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage)); + + if (response.equals(endMessage)) { + Platform.exit(); + } + + userInput.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/duke/ui/Parser.java b/src/main/java/duke/ui/Parser.java new file mode 100644 index 0000000000..25fdff2afb --- /dev/null +++ b/src/main/java/duke/ui/Parser.java @@ -0,0 +1,290 @@ +package duke.ui; + +import duke.storage.Storage; +import duke.task.Event; +import duke.task.Task; +import duke.task.Todo; +import duke.task.Deadline; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; + +import java.util.Arrays; +import java.util.ArrayList; + +import java.io.IOException; + +public class Parser { + + String defaultErrorMessage = ":( OOPS!!! I'm sorry, but I don't know what that means! Possible commands: todo [task]," + + " event [task] /at [time], deadline [task] /by [time], mark [index], unmark [index], delete [index], bye"; + String dateAndTimeErrorMessage = ":( OOPS!!! The correct format for date and time is yyyy-mm-dd and hh:mm"; + String snoozeErrorMessage = "Unable to find a matching task with that name and date :( Check again?"; + + /** + * Parses input from InputHandler and returns a new Task to be added to TaskList. Handles event, deadline, todo commands + * + * @param type CommandType of input, including (TODO, DEADLINE, EVENT) + * @param splitInput SplitInput from InputHandler is user's input, split by empty spaces for processing + * @return Task object of new task to be added to TaskList + * @throws DukeException If format is wrong + */ + public Task parse(InputHandler.CommandType type, String[] splitInput) throws DukeException { + switch (type) { + case TODO: + //Removes the todo command word: i.e. todo task -> task + String[] nameArray = Arrays.copyOfRange(splitInput, 1, splitInput.length); + String todoName = String.join(" ", nameArray); + return new Todo(todoName); + case EVENT: + return parseEvent(splitInput); + case DEADLINE: + return parseDeadline(splitInput); + default: + throw new DukeException(defaultErrorMessage); + } + } + + /** + * Parses input from InputHandler and writes/deletes/prints from storage accordingly. Handles list, mark, unmark, + * delete commands + * + * @param type CommandType of input, including (LIST, MARK, UNMARK, DELETE) + * @param storage Storage object in InputHandler to write/delete/get data from + * @param splitInput SplitInput from InputHandler is user's input, split by empty spaces for processing + * @throws DukeException Handles unrecognised commands + * @throws IOException Handles IO Errors + */ + public String parse(InputHandler.CommandType type, Storage storage, String[] splitInput) throws DukeException, IOException { + + String listCommandStringIntro = "Here are the tasks in your list:\n"; + + switch (type) { + case LIST: + return listCommandStringIntro + storage.list(); + case MARK: + return parseMarkAndUnmark(InputHandler.CommandType.MARK, storage, splitInput); + case UNMARK: + return parseMarkAndUnmark(InputHandler.CommandType.UNMARK, storage, splitInput); + case DELETE: + return parseDelete(storage, splitInput); + case FIND: + return parseFind(storage, splitInput); + case SNOOZE: + return parseSnooze(storage, splitInput); + default: + throw new DukeException(defaultErrorMessage); + } + } + + /** + * Parses the EVENT CommandType and user input to create the Event object using the relevant data. + * + * @param splitInput User's input, split by spaces + * @return Event object to be added to storage. + * @throws DukeException if input is in the wrong datetime format. + */ + private Event parseEvent(String[] splitInput) throws DukeException { + //Removes the event command word and separates into date and time (optional) + String[] stringArrayExcludingEvent = Arrays.copyOfRange(splitInput, 1, splitInput.length); + String stringExcludingEvent = String.join(" ", stringArrayExcludingEvent); + String[] eventNameAndTimeArray = stringExcludingEvent.split("/at "); + String eventNameWithExtraSpace = eventNameAndTimeArray[0]; + String eventName = eventNameWithExtraSpace.substring(0, eventNameWithExtraSpace.length() -1); + String eventTime = eventNameAndTimeArray[1]; + String[] eventTimeArray = eventTime.split(" "); + + try { + Event newEvent = (eventTimeArray.length > 1) ? new Event(eventName, eventTimeArray[0], eventTimeArray[1]) : new Event(eventName, eventTimeArray[0]); + return newEvent; + } catch (DateTimeParseException e) { + //Datetime unable to be parsed + throw new DukeException(dateAndTimeErrorMessage); + } + } + + /** + * Parses the DEADLINE CommandType and user input to create the Deadline object using the relevant data. + * + * @param splitInput User's input, split by spaces. + * @return Deadline object to be added to storage. + * @throws DukeException If input is in the wrong datetime format/ + */ + private Deadline parseDeadline(String[] splitInput) throws DukeException { + //Removes the deadline command word and separates into date and time (optional) + String[] stringArrayExcludingDeadline = Arrays.copyOfRange(splitInput, 1, splitInput.length); + String stringExcludingDeadline = String.join(" ", stringArrayExcludingDeadline); + String[] deadlineNameAndTimeArray = stringExcludingDeadline.split("/by "); + String deadlineNameWithSpace = deadlineNameAndTimeArray[0]; + String deadlineName = deadlineNameWithSpace.substring(0, deadlineNameWithSpace.length() - 1); + String deadlineTime = deadlineNameAndTimeArray[1]; + String[] deadlineTimeArray = deadlineTime.split(" "); + + try { + Deadline newDeadline = (deadlineTimeArray.length > 1) ? new Deadline(deadlineName, deadlineTimeArray[0], deadlineTimeArray[1]) : new Deadline(deadlineName, deadlineTimeArray[0]); + return newDeadline; + } catch (DateTimeParseException e) { + //Datetime unable to be parsed + throw new DukeException(dateAndTimeErrorMessage); + } + } + + /** + * Parses the FIND CommandType and user input to find a list of tasks with matching descriptions + * + * @param storage Storage contains the task. This storage is iterated through to search. + * @param splitInput User's input split by spaces. + * @return String format of the task. + * @throws DukeException Unable to find the task. + */ + private String parseFind(Storage storage, String[] splitInput) throws DukeException{ + //Removes the find command and iterates through the TaskList to find a task name that contains the keyword + String cannotFindTaskMessage = "Uh oh! No task matches the description you've given :("; + String[] stringArrayExcludingFind = Arrays.copyOfRange(splitInput, 1, splitInput.length); + String nameOfKeyWord = String.join(" ", stringArrayExcludingFind); + ArrayList arrayOfTasks = storage.accessTaskList().list; + ArrayList indexOfFoundObjects = new ArrayList<>(); + + for (int i = 0; i < arrayOfTasks.size(); i++) { + Task currentTask = arrayOfTasks.get(i); + if (currentTask.name.contains(nameOfKeyWord)) { + indexOfFoundObjects.add(i); + } + } + + String outputString = ""; + if (!indexOfFoundObjects.isEmpty()) { + //Task found and print + for (int j = 0; j < indexOfFoundObjects.size(); j++) { + outputString += (j + 1) + "." + storage.get(indexOfFoundObjects.get(j)) + "\n"; + } + return outputString; + } else { + //Unable to find + throw new DukeException(cannotFindTaskMessage); + } + } + + /** + * Parses the CommandType DELETE and deletes the task from storage. + * + * @param storage Storage contains the task to be deleted. + * @param splitInput User's input split by spaces. + * @return String of Duke's reply that task has been deleted. + * @throws DukeException If there is a NumberFormatException with the index. + * @throws IOException If there is error by Storage in reading/writing to data.txt. + */ + private String parseDelete(Storage storage, String[] splitInput) throws DukeException, IOException { + //Delete task by index + String wrongDeleteFormatErrorMessage = "Make sure delete is in the format delete [index]!"; + String indexTooLarge = "Index too large!"; + + try { + int idxOfTaskToBeDeleted = Integer.parseInt(splitInput[1]) - 1; + System.out.println(idxOfTaskToBeDeleted); + assert idxOfTaskToBeDeleted < storage.taskListSize() : indexTooLarge; + Task taskToBeDeleted = storage.get(idxOfTaskToBeDeleted); + storage.deleteData(idxOfTaskToBeDeleted); + return "Noted. I've removed this task:\n" + taskToBeDeleted + "\nNow you have " + storage.taskListSize() + + " tasks in the list"; + } catch (NumberFormatException e) { + //Addresses the issue of a non-integer being passed in + throw new DukeException(wrongDeleteFormatErrorMessage); + } + } + + /** + * Marks or Unmarks task. + * + * @param type CommandType of task + * @param storage Storage to be iterated through for task + * @param splitInput User's input split by spaces except for snooze which is split by /t + * @return String of Duke's reply that task has been marked/unmarked + * @throws DukeException NumberFormatError due to index or invalid CommandType + * @throws IOException If there is an issue reading/writing from data by Storage + */ + private String parseMarkAndUnmark(InputHandler.CommandType type, Storage storage, String[] splitInput) throws DukeException, IOException { + String markedMessage = "Nice! I've marked this task as done:\n"; + String unmarkedMessage = "OK, I've marked this task as not done yet:\n"; + String wrongMarkFormatErrorMessage = "Make sure mark is in the format: mark [index]!"; + String wrongUnmarkFormatErrorMessage = "Make sure unmark is in the format: unmark [index]!"; + String invalidCommandTypeErrorMessage = "Invalid CommandType. Should not reach here"; + + switch (type) { + case UNMARK: + try { + int taskToBeUnmarkedIndex = Integer.parseInt(splitInput[1]) - 1; + Task taskToBeUnmarked = storage.get(taskToBeUnmarkedIndex); + taskToBeUnmarked.setUnmarkedTask(); + storage.rewriteData(); + return unmarkedMessage + taskToBeUnmarked; + } catch (NumberFormatException e) { + //Addresses the issue of a non-integer being passed in + throw new DukeException(wrongUnmarkFormatErrorMessage); + } + case MARK: + //Marks task by index + try { + int taskToBeMarkedIndex = Integer.parseInt(splitInput[1]) - 1; + Task taskToBeMarked = storage.get(taskToBeMarkedIndex); + taskToBeMarked.setMarkedTask(); + storage.rewriteData(); + return markedMessage + taskToBeMarked; + } catch (NumberFormatException e) { + //Addresses the error of a non-integer being passed in + throw new DukeException(wrongMarkFormatErrorMessage); + } + default: + throw new DukeException(invalidCommandTypeErrorMessage); + } + } + + private String parseSnooze(Storage storage, String[] splitInput) throws DukeException, IOException { + String[] nameAndPrevTimeArr = splitInput[0].split(" "); + String[] newDateAndTimeArr = splitInput[1].split(" "); + + String previousDateString = nameAndPrevTimeArr[nameAndPrevTimeArr.length - 1]; + LocalDate previousDate = LocalDate.parse(previousDateString); + String newDateString = newDateAndTimeArr[newDateAndTimeArr.length - 2]; + LocalDate newDate = LocalDate.parse(newDateString); + String newTimeString = newDateAndTimeArr[newDateAndTimeArr.length - 1]; + LocalTime newTime = LocalTime.parse(newTimeString); + + String taskName = ""; + for (int i = 1; i < nameAndPrevTimeArr.length - 1; i++) { + if (i == nameAndPrevTimeArr.length - 2) { + taskName += nameAndPrevTimeArr[i]; + } else { + taskName += nameAndPrevTimeArr[i] + " "; + } + } + + for (int j = 0; j < storage.taskListSize(); j++) { + if (storage.get(j).name.equals(taskName) && storage.get(j) instanceof Deadline) { + Deadline deadlineToBeChanged = (Deadline) storage.get(j); + if (deadlineToBeChanged.getDueDate().equals(previousDate)) { + deadlineToBeChanged.changeDueDate(newDate); + deadlineToBeChanged.changeDueTime(newTime); + storage.rewriteData(); + return "Changed Deadline " + taskName + " from " + deadlineToBeChanged.dateConverterToString(previousDate) + + " " + deadlineToBeChanged.dateConverterToString(newDate) + + " " + deadlineToBeChanged.timeConverterToString(newTime); + } + } + if (storage.get(j).name.equals(taskName) && storage.get(j) instanceof Event) { + Event eventToBeChanged = (Event) storage.get(j); + if (eventToBeChanged.getDueDate().equals(previousDate)) { + eventToBeChanged.changeDueDate(newDate); + eventToBeChanged.changeDueTime(newTime); + storage.rewriteData(); + return "Changed Event " + taskName + " from " + eventToBeChanged.dateConverterToString(previousDate) + + " " + eventToBeChanged.dateConverterToString(newDate) + + " " + eventToBeChanged.timeConverterToString(newTime); + } + } + } + throw new DukeException(snoozeErrorMessage); + + } +} \ No newline at end of file diff --git a/src/main/resources/images/bear.jpg b/src/main/resources/images/bear.jpg new file mode 100644 index 0000000000..306f938454 Binary files /dev/null and b/src/main/resources/images/bear.jpg differ diff --git a/src/main/resources/images/human.jpg b/src/main/resources/images/human.jpg new file mode 100644 index 0000000000..a7c2e69ebf Binary files /dev/null and b/src/main/resources/images/human.jpg differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..ede775d4f9 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..83fddd291c --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + +