diff --git a/README.md b/README.md index 9d95025bce..b9e11953ef 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Duke project template +# duke.Duke project template 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. @@ -15,7 +15,7 @@ Prerequisites: JDK 11, update Intellij to the most recent version. 1. Click `Open or Import`. 1. Select the project directory, and click `OK` 1. If there are any further prompts, accept the defaults. -1. After the importing is complete, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()`. If the setup is correct, you should see something like the below: +1. After the importing is complete, locate the `src/main/java/duke.Duke.java` file, right-click it, and choose `Run duke.Duke.main()`. If the setup is correct, you should see something like the below: ``` Hello from ____ _ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..e7d24dbafa --- /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 { + String javaFxVersion = '11' + + 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' + + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClassName = "Launcher" +} + +shadowJar { + archiveBaseName = "DukeLUL" + archiveClassifier = null +} + +checkstyle { + toolVersion = '8.23' +} + +run{ + standardInput = System.in +} diff --git a/data/tasks.txt b/data/tasks.txt new file mode 100644 index 0000000000..ca84b69d41 --- /dev/null +++ b/data/tasks.txt @@ -0,0 +1,7 @@ +duke.task.Task list (Last updated Sep 18 2020, 22:56:14 PM): +1. [T][✗] homewok +2. [T][✗] test1 +3. [T][✗] test2 +4. [T][✗] test3 +5. [T][✗] test4 +6. [T][✗] test6 diff --git a/docs/README.md b/docs/README.md index fd44069597..dc3c78bd7d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,20 +1,218 @@ -# User Guide +# DukeLUL User Guide + +DukeLUL is a desktop app for managing things that you need to get done. +Chat with an incredibly annoying LULCAT and get so irritated that you'll +complete your tasks in record time and close the app ASAP. + +* [Quick Start](#quick-start) +* [Features](#features) + * [Viewing help: `help`](#viewing-help-help) + * [Adding a todo task: `todo`](#adding-a-todo-task-todo) + * [Adding a task with a deadline: `deadline`](#adding-a-task-with-a-deadline-deadline) + * [Adding an event task: `event`](#adding-an-event-task-event) + * [Listing all tasks: `list`](#listing-all-tasks-list) + * [Finding tasks: `find`](#finding-tasks-find) + * [Marking a task as done: `done`](#marking-a-task-as-done-done) + * [Deleting a task: `delete`](#deleting-a-task-delete) + * [Updating a task: `update`](#updating-a-task-update) + * [Exiting the program: `bye`](#exiting-the-program-bye) + * [Saving the data](#saving-the-data) +* [FAQ](#FAQ) +* [Command summary](#command-summary) +
+ +--- + +## Quick Start + +1. Ensure you have Java 11 or above installed on your computer. + +2. Download the latest `DukeLUL.jar` from [here](https://github.com/kaitlynng/ip/releases/tag/v0.1). + +3. Copy the file to the folder you want to use as the home folder of your DukeLUL app. + +4. Double-click the file to start the app. The GUI similar to the one below should appear in a few seconds. + +5. Chat with the LULCAT by typing into the chat box and pressing Enter. Typing `help` and pressing Enter will bring up +a list of commands you can try. + +6. Refer to the Features below for details of available commands. + ## Features -### Feature 1 -Description of feature. +This section describes a list of commands that you can use when chatting with LULCAT. + +| :memo: Take note! | +|:------------------------------------------------------------------------------------------------------| +| Words in `UPPER_CASE` are parameters to be supplied by you. | +| * e.g. In `todo TASK_DESCRIPTION`, `TASK_DESCRIPTION` is a parameter which you can supply, like so: `todo Homework` | +| | +| Items in square brackets are optional. | +| * e.g. `list [\by DATE_TIME]` can be used as `list` or `list \by 2012-02-02` | + + +### Viewing help: `help` + +
+ Provides a link to the user guide for additional help. + + Format: `help` +
+ +### Adding a todo task: `todo` + +
+ Adds a todo task to the tasklist + + Format: `todo TASK_DESCRIPTION` + Examples: + * `todo Finish homework` + * `todo Clean the house` +
+ +### Adding a task with a deadline: `deadline` + +
+ Adds a task to the tasklist with a deadline. + + Format: `deadline TASK_DESCRIPTION \by DATE_TIME` + * Note that the `DATE_TIME`has to be entered with a `YYYY-MM-dd` or `YYYY-MM-ddThh:mm:ss` format to be + recognised as a date / datetime by the app. Otherwise, no filtering operations + can be performed on it. + + Examples: + * `deadline Project 3 \by 2020-10-05` + * `deadline Buy grocevires \by 2020-09-16T11:02` +
+ +### Adding an event task: `event` +
+ Adds an event to the tasklist happening at a given date/time. + + Format: `event EVENT_DESCRIPTION \at DATE_TIME` + * Note that the `DATE_TIME`has to be entered with a `YYYY-MM-dd` or `YYYY-MM-ddThh:mm:ss` format to be + recognised as a date / datetime by the app. Otherwise, no filtering operations + can be performed on it. + + Examples: + * `event Night cycling \at 2020-09-19T02:00` + * `event Dad's birthday \at 2020-10-25` +
+ +### Listing all tasks: `list` + +
+ Shows a list of all tasks in the tasklist. + + Format: `list [\by DATE_TIME]` + * You can optionally specify a `DATE_TIME`to list all the deadlines and events that are due / happening before the given datetime. + The `DATE_TIME` specified has to be of the format `YYYY-MM-dd` or `YYYY-MM-ddThh:mm:ss` + to be valid. + + Examples: + * `list \by 2020-09-19T02:00` + * `list \by 2020-10-25` +
+ +### Finding tasks: `find` + +
+ Finds tasks that contains the given keyphrase. + + Format: `find KEYPHRASE` + * The search is not case-sensitive. e.g. `Cats` will match `cats` + * Only the description of tasks / events is searched. + * Only the whole phrase would be matched. e.g. `Happy` or `days` will not match `Happy days` + + Examples: + * `find birthday` returns `John's birthday` and `Weiming's birthday` +
+ +### Marking a task as done: `done` + +
+ Marks a task in the current tasklist as done. + + Format: `done TASK_INDEX` + * `TASK_INDEX` refers to the index of the task you wish to mark as done in the tasklist. + To find the index of the task you would like to mark as done, use the `list` command. + + Examples: + * `done 3` marks the task at index 3 as done +
+ +### Deleting a task: `delete` + +
+ Deletes a task from the current tasklist. + + Format: `delete TASK_INDEX` + * `TASK_INDEX` refers to the index of the task you wish to delete in the tasklist. + To find the index of the task you would like to delete, use the `list` command. + + Examples: + * `delete 3` deletes the task at index 3 in the tasklist +
+ +### Updating a task: `update` + +
+ Updates the description or datetime of a task currently in the tasklist. + + Format: `update TASK_INDEX [\date] NEW_VALUE` + * `TASK_INDEX` refers to the index of the task you wish to update in the tasklist. + To find the index of the task you would like to update, use the `list` command. + * If `\date` is specified, the date of the task will be updated to `NEW_VALUE`, otherwise + the description of the task will be updated to `NEW_VALUE`. If the task at the given index + does not have a specified datetime, an error will be thrown. + + Examples: + * `update 3 Chinese homework` updates the description of the task at index 3 to "Chinese homework" + * `update 3 \date 2020-09-02T12:00:05` updates the datetime of the task at index 3 to "Sep 2 2020, 12:00:05 PM" +
+ +### Exiting the program: `bye` + +
+ Exits the program. + + Format: `bye` +
+ +### Saving the data + +DukeLUL automatically saves the current tasklist to the hard drive after any command that +modifies the tasklist. There is no need to save manually. +
+ +--- + +## FAQ +
+ How can I inform the developers of bugs in the program? -## Usage + **A:** You can open a new issue in this project's Github repository. These issues will be + reviewed on a regular basis. Click [here](https://docs.github.com/en/enterprise/2.15/user/articles/creating-an-issue) + to find out how to create a Github issue. +
-### `Keyword` - Describe action +--- -Describe action and its outcome. +## Command Summary -Example of usage: +| **Action** | **Format** | +|---------------| ---------------------------------------------| +| Help | `help` | +| Add todo | `todo TASK_DESCRIPTION` | +| Add deadline | `deadline TASK_DESCRIPTION \by DATE_TIME` | +| Add event | `event EVENT_DESCRIPTION \at DATE_TIME` | +| List | `list [\by DATE_TIME]` | +| Find | `find KEYPHRASE` | +| Mark done | `done TASK_INDEX` | +| Delete | `delete TASK_INDEX` | +| Update | `update TASK_INDEX [\date] NEW_VALUE` | +| Exit | `bye` | -`keyword (optional arguments)` -Expected outcome: -`outcome` diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..c1dc7075c5 Binary files /dev/null and b/docs/Ui.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..f3d88b1c2f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..b7c8c5dbf5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..2fe81a7d95 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..62bd9b9cce --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/DialogBox.java b/src/main/java/DialogBox.java new file mode 100644 index 0000000000..213e0e8424 --- /dev/null +++ b/src/main/java/DialogBox.java @@ -0,0 +1,66 @@ +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +/** + * An example of a custom control using FXML. + * This control represents a dialog box consisting of an ImageView to represent the speaker's face and a label + * containing text from the speaker. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + private DialogBox(String text, Image img, boolean isDuke) { + String fxmlUrl; + if (isDuke) { + fxmlUrl = "/view/DukeDialogBox.fxml"; + } else { + fxmlUrl = "/view/UserDialogBox.fxml"; + } + + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource(fxmlUrl)); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(img); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + } + + public static DialogBox getUserDialog(String text, Image img) { + return new DialogBox(text, img, false); + } + + public static DialogBox getDukeDialog(String text, Image img) { + var db = new DialogBox(text, img, true); + db.flip(); + return db; + } +} \ No newline at end of file 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/Launcher.java b/src/main/java/Launcher.java new file mode 100644 index 0000000000..43d64c26fb --- /dev/null +++ b/src/main/java/Launcher.java @@ -0,0 +1,10 @@ +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..6e864153e8 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: duke.Duke + diff --git a/src/main/java/Main.java b/src/main/java/Main.java new file mode 100644 index 0000000000..37c92bb7aa --- /dev/null +++ b/src/main/java/Main.java @@ -0,0 +1,31 @@ +import duke.Duke; + +import java.io.IOException; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +/** + * A GUI for Duke using FXML. + */ +public class Main extends Application { + + private Duke duke = new Duke(); + + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + Scene scene = new Scene(ap); + stage.setScene(scene); + fxmlLoader.getController().setDuke(duke); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/MainWindow.java b/src/main/java/MainWindow.java new file mode 100644 index 0000000000..8b94606507 --- /dev/null +++ b/src/main/java/MainWindow.java @@ -0,0 +1,62 @@ +import duke.Duke; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import javafx.scene.layout.Region; + +/** + * 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")); + + private String welcomeMessage = "Oh hai kittehs!"; + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + dialogContainer.getChildren().add(DialogBox.getDukeDialog(welcomeMessage, dukeImage)); + } + + public void setDuke(Duke d) { + duke = d; + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + String response = duke.getResponse(input); + DialogBox userDialog = DialogBox.getUserDialog(input, userImage); + DialogBox dukeDialog = DialogBox.getDukeDialog(response, dukeImage); + dukeDialog.setMinHeight(Region.USE_PREF_SIZE); + dialogContainer.getChildren().addAll( + userDialog, dukeDialog + ); + userInput.clear(); + + if (!duke.getRunState()) { + System.out.println("good"); + System.exit(0); + } + } +} \ No newline at end of file diff --git a/src/main/java/duke/Duke.java b/src/main/java/duke/Duke.java new file mode 100644 index 0000000000..fcacf6918d --- /dev/null +++ b/src/main/java/duke/Duke.java @@ -0,0 +1,71 @@ +package duke; + +import duke.command.Command; +import duke.parser.Parser; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + + +public class Duke { + private static final String LOG_DIRPATH = "data"; + private static final String LOG_FILENAME = "tasks.txt"; + private Storage storage; + private TaskList tasks; + private Ui ui; + + private boolean isRunning = true; + + public Duke() { + this.ui = new Ui(); + this.storage = new Storage(LOG_DIRPATH, LOG_FILENAME); + try { + tasks = new TaskList(storage.load()); + ui.showLoadedTasks(tasks); + } catch (DukeException e) { + ui.showLoadingError(e.getMessage()); + tasks = new TaskList(); + } + } + + public boolean getRunState() { + return this.isRunning; + } + + public String getResponse(String input) { + assert !input.isEmpty() : "input cannot be empty"; + + try { + Command c = Parser.parse(input); + this.isRunning = !c.isExit(); + return c.getResponse(tasks, storage); + } catch (DukeException e) { + return e.getMessage(); + } + } + + public void run() { + ui.showWelcome(); + boolean isExit = false; + while (!isExit) { + try { + String fullCommand = ui.readCommand(); + ui.showLine(); + Command c = Parser.parse(fullCommand); + c.execute(tasks, ui, storage); + isExit = c.isExit(); + } catch (DukeException e) { + ui.showError(e.getMessage()); + } finally { + ui.showLine(); + } + } + } + + public static void main(String[] args) { + new Duke().run(); + } + + + +} diff --git a/src/main/java/duke/DukeException.java b/src/main/java/duke/DukeException.java new file mode 100644 index 0000000000..3fcd0f5ea8 --- /dev/null +++ b/src/main/java/duke/DukeException.java @@ -0,0 +1,7 @@ +package duke; + +public class DukeException extends Exception { + public DukeException(String message) { + super(message); + } +} diff --git a/src/main/java/duke/command/ByeCommand.java b/src/main/java/duke/command/ByeCommand.java new file mode 100644 index 0000000000..43abd5579d --- /dev/null +++ b/src/main/java/duke/command/ByeCommand.java @@ -0,0 +1,25 @@ +package duke.command; + +import duke.*; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class ByeCommand extends Command { + + public ByeCommand() { + super(); + this.cmd = CMD.BYE; + this.isExit = true; + } + + @Override + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + return "OKAIS I IZ GOIN 2 NOM BYEEEEE C U !!!1!1!!"; + } + + @Override + public String toString() { + return this.cmd.toString(); + } +} diff --git a/src/main/java/duke/command/CMD.java b/src/main/java/duke/command/CMD.java new file mode 100644 index 0000000000..b4f2cb098c --- /dev/null +++ b/src/main/java/duke/command/CMD.java @@ -0,0 +1,15 @@ +package duke.command; + +public enum CMD { + HELP, + BYE, + LIST, + TODO, + DEADLINE, + EVENT, + DONE, + DELETE, + FIND, + UPDATE, + DEFAULT +} \ No newline at end of file diff --git a/src/main/java/duke/command/Command.java b/src/main/java/duke/command/Command.java new file mode 100644 index 0000000000..6c6fa11435 --- /dev/null +++ b/src/main/java/duke/command/Command.java @@ -0,0 +1,42 @@ +package duke.command; + +import duke.*; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class Command { + protected CMD cmd; + protected boolean isExit; + + public Command() { + this.cmd = CMD.DEFAULT; + this.isExit = false; + } + + public boolean isExit() { + return this.isExit; + } + + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + return "CAN I HAZ CHEEZBURGER?"; + } + + /** + * Executes the default command, which displays a standard string regardless + * of input. + * + * @param taskList + * @param ui + * @param storage + * @throws DukeException + */ + public void execute(TaskList taskList, Ui ui, Storage storage) throws DukeException { + ui.display(getResponse(taskList, storage)); + } + + @Override + public String toString() { + return this.cmd.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/duke/command/DeadlineCommand.java b/src/main/java/duke/command/DeadlineCommand.java new file mode 100644 index 0000000000..bc931e6f8d --- /dev/null +++ b/src/main/java/duke/command/DeadlineCommand.java @@ -0,0 +1,31 @@ +package duke.command; + +import duke.*; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class DeadlineCommand extends Command { + private String taskName; + private String by; + + public DeadlineCommand(String taskName, String by) { + super(); + this.cmd = CMD.DEADLINE; + this.taskName = taskName; + this.by = by; + } + + @Override + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + String msg = "I PUT NEW TING IN DA LIST\n " + tasklist.addDeadline(this.taskName, this.by) + + "\nNAO U HAS " + tasklist.getNumberOfTasks() + " FINGS IN DA LIST LULZIES"; + storage.save(tasklist); + return msg; + } + + @Override + public String toString() { + return this.cmd.toString() + ": " + this.taskName + "(" + this.by + ")"; + } +} diff --git a/src/main/java/duke/command/DeleteCommand.java b/src/main/java/duke/command/DeleteCommand.java new file mode 100644 index 0000000000..b00600fcd1 --- /dev/null +++ b/src/main/java/duke/command/DeleteCommand.java @@ -0,0 +1,29 @@ +package duke.command; + +import duke.*; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class DeleteCommand extends Command { + private int taskIdx; + public DeleteCommand(int idx) { + super(); + this.cmd = CMD.DONE; + this.taskIdx = idx; + } + + @Override + public String getResponse(TaskList taskList, Storage storage) throws DukeException { + String msg = "TASK IZ NAO DELETZ!!!!1!11!\n" + " " + + taskList.popTask(this.taskIdx) + + "\nNAO U HAS " + taskList.getNumberOfTasks() + " FINGS IN DA LIST LULZIES"; + storage.save(taskList); + return msg; + } + + @Override + public String toString() { + return this.cmd.toString() + ": " + this.taskIdx; + } +} diff --git a/src/main/java/duke/command/DoneCommand.java b/src/main/java/duke/command/DoneCommand.java new file mode 100644 index 0000000000..ea325c2944 --- /dev/null +++ b/src/main/java/duke/command/DoneCommand.java @@ -0,0 +1,28 @@ +package duke.command; + +import duke.*; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class DoneCommand extends Command { + private int idx; + public DoneCommand(int idx) { + super(); + this.cmd = CMD.DONE; + this.idx = idx; + } + + @Override + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + tasklist.markTaskAsDone(this.idx); + storage.save(tasklist); + return "TASK IZ NAO DUNZ!!!!1!11!\n" + " " + tasklist.getTaskByIdx(this.idx); + } + + @Override + public String toString() { + return this.cmd.toString() + ": " + this.idx; + } +} + diff --git a/src/main/java/duke/command/EventCommand.java b/src/main/java/duke/command/EventCommand.java new file mode 100644 index 0000000000..599f291018 --- /dev/null +++ b/src/main/java/duke/command/EventCommand.java @@ -0,0 +1,31 @@ +package duke.command; + +import duke.*; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class EventCommand extends Command { + private String taskName; + private String by; + + public EventCommand(String taskName, String by) { + super(); + this.cmd = CMD.EVENT; + this.taskName = taskName; + this.by = by; + } + + @Override + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + String msg = "I PUT NEW TING IN DA LIST\n " + tasklist.addEvent(this.taskName, this.by) + + "\nNAO U HAS " + tasklist.getNumberOfTasks() + " FINGS IN DA LIST LULZIES"; + storage.save(tasklist); + return msg; + } + + @Override + public String toString() { + return this.cmd.toString() + ": " + this.taskName + "(" + this.by + ")"; + } +} diff --git a/src/main/java/duke/command/FindCommand.java b/src/main/java/duke/command/FindCommand.java new file mode 100644 index 0000000000..5f00353ad5 --- /dev/null +++ b/src/main/java/duke/command/FindCommand.java @@ -0,0 +1,29 @@ +package duke.command; + +import duke.DukeException; +import duke.datetime.DateTimeUtility; +import duke.storage.Storage; +import duke.task.TaskList; + +public class FindCommand extends Command { + private String filter; + + public FindCommand(String filter) { + super(); + this.cmd = CMD.FIND; + this.filter = filter; + } + + @Override + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + String ret = tasklist.filterTasksByDescription(this.filter); + if (ret.isEmpty()) { + return "U HAZ NUTHIN DAT GOT " + + this.filter + " INSIDE LULZIES"; + } else { + return "U HAS DEES TINGS IN UR LIST DAT MATCH " + + this.filter + ": \n" + + ret; + } + } +} diff --git a/src/main/java/duke/command/HelpCommand.java b/src/main/java/duke/command/HelpCommand.java new file mode 100644 index 0000000000..becafb562e --- /dev/null +++ b/src/main/java/duke/command/HelpCommand.java @@ -0,0 +1,42 @@ +package duke.command; + +import duke.*; +import duke.storage.Storage; +import duke.task.TaskList; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +public class HelpCommand extends Command { + + public HelpCommand() { + super(); + this.cmd = CMD.HELP; + } + + @Override + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + String url = "https://kaitlynng.github.io/ip/"; + try { + if (System.getProperty("os.name").contains("Windows")) { + Desktop desktop = java.awt.Desktop.getDesktop(); + URI userGuideSite = new URI(url); + desktop.browse(userGuideSite); + } else { + Runtime runtime = Runtime.getRuntime(); + runtime.exec("xdg-open " + url); + } + } catch (IOException | URISyntaxException e) { + return "Bark. (Sorry, we couldn't open the website.)"; + } + + return "GOES HERE 4 MOAR INFO!!!!11!!\n" + url; + } + + @Override + public String toString() { + return this.cmd.toString(); + } +} diff --git a/src/main/java/duke/command/ListCommand.java b/src/main/java/duke/command/ListCommand.java new file mode 100644 index 0000000000..9fe2b4b8a4 --- /dev/null +++ b/src/main/java/duke/command/ListCommand.java @@ -0,0 +1,55 @@ +package duke.command; + +import duke.*; +import duke.datetime.DateTimeUtility; +import duke.storage.Storage; +import duke.task.TaskList; +import duke.ui.Ui; + +public class ListCommand extends Command { + private String by; + + public ListCommand() { + super(); + this.by = ""; + this.cmd = CMD.LIST; + } + + /** + * Overloaded class constructor if a deadline is spceified for filtering out + * tasks after the deadline. + * + * @param by + */ + + public ListCommand(String by) { + this(); + this.by = DateTimeUtility.formatString(by); + } + + @Override + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + if (tasklist.isEmpty()) { + return "UR LIST HAZ NUTHIN LOLOL"; + } else { + if (this.by.isEmpty()) { + return ("U HAS DEES TINGS IN UR LIST.\n" + tasklist.toString()); + } else { + String ret = tasklist.filterTasksByDate(this.by); + if (ret.isEmpty()) { + return "U HAZ NUTHIN DUE/HAPPENIN BY " + + DateTimeUtility.formatString(this.by) + "!! LULZIES"; + } else { + return "U HAS DEES TINGS IN UR LIST DAT R DUE/HAPPENIN BY " + + DateTimeUtility.formatString(this.by) + ": \n" + + ret; + } + } + } + } + + @Override + public String toString() { + return this.cmd.toString() + (this.by.isEmpty() ? "" : " (" + this.by + ")"); + } +} diff --git a/src/main/java/duke/command/TodoCommand.java b/src/main/java/duke/command/TodoCommand.java new file mode 100644 index 0000000000..79c3385388 --- /dev/null +++ b/src/main/java/duke/command/TodoCommand.java @@ -0,0 +1,28 @@ +package duke.command; + +import duke.*; +import duke.storage.Storage; +import duke.task.TaskList; + +public class TodoCommand extends Command { + private String taskName; + + public TodoCommand(String taskName) { + super(); + this.cmd = CMD.TODO; + this.taskName = taskName; + } + + @Override + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + String msg = "I PUT NEW TING IN DA LIST\n " + tasklist.addTodo(this.taskName) + + "\nNAO U HAS " + tasklist.getNumberOfTasks() + " FINGS IN DA LIST LULZIES"; + storage.save(tasklist); + return msg; + } + + @Override + public String toString() { + return this.cmd.toString() + ": " + this.taskName; + } +} diff --git a/src/main/java/duke/command/UpdateCommand.java b/src/main/java/duke/command/UpdateCommand.java new file mode 100644 index 0000000000..45dc8d229d --- /dev/null +++ b/src/main/java/duke/command/UpdateCommand.java @@ -0,0 +1,38 @@ +package duke.command; + +import duke.DukeException; +import duke.storage.Storage; +import duke.task.TaskList; + +public class UpdateCommand extends Command { + private int taskIdx; + private String newValue; + private boolean isUpdateDate; + + public UpdateCommand(int idx, String newValue, boolean isUpdateDate) { + super(); + this.cmd = CMD.UPDATE; + this.taskIdx = idx; + this.newValue = newValue; + this.isUpdateDate = isUpdateDate; + } + + @Override + public String getResponse(TaskList tasklist, Storage storage) throws DukeException { + String msg = "TASK IZ NAO UPDATEZ!!1!11!!\n" + " "; + + if (this.isUpdateDate) { + msg += tasklist.updateTaskDate(this.taskIdx, this.newValue); + } else { + msg += tasklist.updateTaskDescription(this.taskIdx, this.newValue); + } + + storage.save(tasklist); + return msg; + } + + @Override + public String toString() { + return this.cmd.toString() + ": " + this.taskIdx; + } +} diff --git a/src/main/java/duke/datetime/DateTimeFormat.java b/src/main/java/duke/datetime/DateTimeFormat.java new file mode 100644 index 0000000000..3ad07591df --- /dev/null +++ b/src/main/java/duke/datetime/DateTimeFormat.java @@ -0,0 +1,7 @@ +package duke.datetime; + +public enum DateTimeFormat { + String, + Date, + DateTime, +} \ No newline at end of file diff --git a/src/main/java/duke/datetime/DateTimeUtility.java b/src/main/java/duke/datetime/DateTimeUtility.java new file mode 100644 index 0000000000..28c8fdc88d --- /dev/null +++ b/src/main/java/duke/datetime/DateTimeUtility.java @@ -0,0 +1,156 @@ +package duke.datetime; +import java.util.List; +import java.util.ArrayList; + +import duke.DukeException; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class DateTimeUtility { + private static DateTimeFormatter OUTPUT_DATE_FMT = DateTimeFormatter.ofPattern("MMM d yyyy"); + private static DateTimeFormatter OUTPUT_DATETIME_FMT = DateTimeFormatter.ofPattern("MMM d yyyy, HH:mm:ss a"); + + /** + * Checks whether the input dataString is of the correct Date or DateTime format, or is just a String. + * + * @param dateStr + * @return the format of the input dataString (whether it's of a Date, DateTime or normal String format) + */ + public static DateTimeFormat checkDateTimeType(String dateStr) { + assert !dateStr.isEmpty() : "dateStr should not be empty"; + try { + LocalDateTime.parse(dateStr); + return DateTimeFormat.DateTime; + } catch (DateTimeException e) {} + try { + LocalDateTime.parse(dateStr, OUTPUT_DATETIME_FMT); + return DateTimeFormat.DateTime; + } catch (DateTimeException e) {} + try { + LocalDate.parse(dateStr); + return DateTimeFormat.Date; + } catch (DateTimeException e) {} + try { + LocalDate.parse(dateStr, OUTPUT_DATE_FMT); + return DateTimeFormat.Date; + } catch (DateTimeException e) {} + + return DateTimeFormat.String; + } + + /** + * Formats the input string according to the DateTimeFormat specified. If it is of a Date or DateTime + * format, output a String that changes the input string to a specified output format. If it is of a + * String format, formatString returns the same string. + * + * @param dateStr + * @param format format of the input string that can be obtained with checkDateTimeType() + * @return Another string of the correct standardised output format + */ + + public static String formatString(String dateStr, DateTimeFormat format) throws DukeException { + assert !dateStr.isEmpty() : "dateStr should not be empty"; + assert format != null : "format cannot be null"; + + switch(format) { + case DateTime: + try { + return LocalDateTime.parse(dateStr).format(OUTPUT_DATETIME_FMT); + } catch (DateTimeException e) { + throw new DukeException("Datetime format " + format + + " does not match that of input string " + dateStr); + } + case Date: + try { + return LocalDate.parse(dateStr).format(OUTPUT_DATE_FMT); + } catch (DateTimeException e) { + throw new DukeException("Datetime format " + format + + " does not match that of input string " + dateStr); + } + + default: + return dateStr; + } + } + + /** + * Formats the input string according to the DateTimeFormat that is Automatically detected + * using checkDateTimeType(). + * + * @param dateStr + * @return + */ + public static String formatString(String dateStr) { + try { + return DateTimeUtility.formatString(dateStr, DateTimeUtility.checkDateTimeType(dateStr)); + } catch (DukeException e) { + return dateStr; + } + + } + + public static int compare(LocalDate a, LocalDate b) { + assert a != null : "a cannot be null"; + assert b != null : "b cannot be null"; + return a.compareTo(b); + } + + public static int compare(LocalDateTime a, LocalDateTime b) { + assert a != null : "a cannot be null"; + assert b != null : "b cannot be null"; + return a.compareTo(b); + } + + public static int compare(LocalDate a, LocalDateTime b) { + assert a != null : "a cannot be null"; + assert b != null : "b cannot be null"; + return a.compareTo(b.toLocalDate()); + } + + public static int compare(LocalDateTime a, LocalDate b) { + assert a != null : "a cannot be null"; + assert b != null : "b cannot be null"; + return a.toLocalDate().compareTo(b); + } + + /** + * Overloaded comparator between two input DateTime strings. Checks the DateTimeFormat for each + * string and makes the right comparisons between Date and DateTime. If either one of the strings + * cannot be parsed as a Date or DateTime object, throws an exception. + * + * @param a + * @param b + * @return + * @throws DateTimeException + */ + public static int compare(String a, String b) throws DateTimeException { + assert !a.isEmpty() : "a cannot be empty"; + assert !b.isEmpty() : "b cannot be empty"; + + DateTimeFormat aType = DateTimeUtility.checkDateTimeType(a); + DateTimeFormat bType = DateTimeUtility.checkDateTimeType(b); + + if (aType == DateTimeFormat.Date) { + if (bType == DateTimeFormat.Date) { + return DateTimeUtility.compare(LocalDate.parse(a, OUTPUT_DATE_FMT), LocalDate.parse(b, OUTPUT_DATE_FMT)); + } else if (bType == DateTimeFormat.DateTime) { + return DateTimeUtility.compare(LocalDate.parse(a, OUTPUT_DATE_FMT), LocalDateTime.parse(b, OUTPUT_DATETIME_FMT)); + } else { + throw new DateTimeException("Cannot compare Datetime with string!"); + } + } else if (aType == DateTimeFormat.DateTime) { + if (bType == DateTimeFormat.Date) { + return DateTimeUtility.compare(LocalDateTime.parse(a, OUTPUT_DATE_FMT), LocalDate.parse(b, OUTPUT_DATE_FMT)); + } else if (bType == DateTimeFormat.DateTime) { + return DateTimeUtility.compare(LocalDateTime.parse(a, OUTPUT_DATETIME_FMT), LocalDateTime.parse(b, OUTPUT_DATETIME_FMT)); + } else { + throw new DateTimeException("Cannot compare Datetime with string!"); + } + } else { + throw new DateTimeException("Cannot compare Datetime with string!"); + } + } +} diff --git a/src/main/java/duke/parser/Parser.java b/src/main/java/duke/parser/Parser.java new file mode 100644 index 0000000000..24fdaab7b4 --- /dev/null +++ b/src/main/java/duke/parser/Parser.java @@ -0,0 +1,166 @@ +package duke.parser; + +import duke.Duke; +import duke.DukeException; +import duke.command.*; +import duke.datetime.DateTimeFormat; +import duke.datetime.DateTimeUtility; + +public class Parser { + + /** + * Parses a user input and returns the right command with other relevant arguments for the command. + * Performs input validation and throws a DukeException if input is not valid. + * + * @param userInput + * @return + * @throws DukeException + */ + public static Command parse(String userInput) throws DukeException { + assert !userInput.isEmpty() : "userInput cannot be empty"; + + int space_idx = userInput.indexOf(' '); + CMD cmd; + + try { + cmd = CMD.valueOf((space_idx == -1 ? userInput : userInput.substring(0, space_idx)).toUpperCase()); + } catch (IllegalArgumentException e) { + cmd = CMD.DEFAULT; + } + + String rest = space_idx == -1 ? "" : userInput.substring(space_idx + 1).trim(); + int dateStrIdx; + String dateStr; + String item; + + switch(cmd) { + + case HELP: + return new HelpCommand(); + + case BYE: + return new ByeCommand(); + + case LIST: + dateStrIdx = rest.indexOf("/by"); + if (dateStrIdx == -1) { + return new ListCommand(); + } + + dateStr = rest.substring(dateStrIdx + 3).trim(); + if (DateTimeUtility.checkDateTimeType(dateStr) != DateTimeFormat.String) { + return new ListCommand(dateStr); + } else { + throw new DukeException("U NID 2 GIV CORRECT DATE FOMAT!"); + } + + case TODO: + if (!rest.isEmpty()) { + return new TodoCommand(rest); + } else { + throw new DukeException("ME FINKZ DAT U NED 2 ENTR NAYM 4 UR TODO ITEM LULZ"); + } + + case DEADLINE: + if (rest.isEmpty()) { + throw new DukeException("ME FINKZ DAT U NED 2 ENTR NAYM 4 UR DEDLINE ITEM LULZ"); + } + + if (rest.indexOf("/by") == -1) { + throw new DukeException("ME FINKZ U NED 2 GIV DATE 4 TIEM 4 DA DEDLINE USIN /by"); + } + + dateStrIdx = rest.indexOf("/by"); + item = rest.substring(0, dateStrIdx).trim(); + dateStr = rest.substring(dateStrIdx + 3).trim(); + + if (!item.isEmpty() && !dateStr.isEmpty()) { + return new DeadlineCommand(item, dateStr); + } else if (item.isEmpty()) { + throw new DukeException("ME FINKZ U NED 2 GIV DA DEDLINE A NAEM"); + } else { + throw new DukeException("ME FINKZ U NED 2 PUT SUMTHIN 4 DA DATE OR TIEM"); + } + + + case EVENT: + if (rest.isEmpty()) { + throw new DukeException("ME FINKZ DAT U NED 2 ENTR NAYM 4 UR EVENT ITEM LULZ"); + } + + if (rest.indexOf("/at") == -1) { + throw new DukeException("ME FINKZ U NED 2 GIV DATE 4 TIEM 4 DA EVENT USIN /at"); + } + + dateStrIdx = rest.indexOf("/at"); + item = rest.substring(0, dateStrIdx).trim(); + dateStr = rest.substring(dateStrIdx + 3).trim(); + + if (!item.isEmpty() && !dateStr.isEmpty()) { + return new EventCommand(item, dateStr); + } else if (item.isEmpty()) { + throw new DukeException("ME FINKZ U NED 2 GIV DA EVENT A NAEM"); + } else { + throw new DukeException("ME FINKZ U NED 2 PUT SUMTHIN 4 DA DATE OR TIEM"); + } + + + case DONE: + try { + int idx = Integer.parseInt(rest) - 1; + return new DoneCommand(idx); + } catch (NumberFormatException e) { + throw new DukeException("U MUST ONLY PUT INDEX OV TASK LULS"); + } + + case DELETE: + try { + int idx = Integer.parseInt(rest) - 1; + return new DeleteCommand(idx); + } catch (NumberFormatException e) { + throw new DukeException("U MUST ONLY PUT INDEX OV TASK LULS"); + } + + case FIND: + if (!rest.isEmpty()) { + return new FindCommand(rest); + } else { + throw new DukeException("ME FINKZ DAT U NED 2 ENTR NAYM 4 UR TODO ITEM LULZ"); + + } + + case UPDATE: + if (rest.isEmpty()) { + throw new DukeException("U MUST PUT DA INDEX OV TASK AN' NEW NAME LULZ"); + } + + int taskIdx; + String newInput; + + try { + String parts[] = rest.split(" ", 2); + taskIdx = Integer.parseInt(parts[0]) - 1; + newInput = parts[1]; + } catch (NumberFormatException e) { + throw new DukeException("U MUST PUT INDEX OV TASK LULS"); + } + + dateStrIdx = newInput.indexOf("/date"); + + if (dateStrIdx == -1) { + return new UpdateCommand(taskIdx, newInput, false); + } + + dateStr = newInput.substring(dateStrIdx + 5).trim(); + if (!dateStr.isEmpty()) { + return new UpdateCommand(taskIdx, dateStr, true); + } else { + throw new DukeException("U CANNOT GIV EMPTY DATE!"); + } + + case DEFAULT: + return new Command(); + } + return new Command(); + } +} diff --git a/src/main/java/duke/storage/Storage.java b/src/main/java/duke/storage/Storage.java new file mode 100644 index 0000000000..556d1827b9 --- /dev/null +++ b/src/main/java/duke/storage/Storage.java @@ -0,0 +1,83 @@ +package duke.storage; + +import duke.DukeException; +import duke.task.TaskList; + +import java.io.BufferedWriter; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +public class Storage { + + private Path dirPath; + private Path filePath; + + public Storage(String dirPath, String fileName) { + this.dirPath = Paths.get(dirPath); + this.filePath = Paths.get(dirPath, fileName); + File dataDir = this.dirPath.toAbsolutePath().toFile(); + File dataFile = this.filePath.toAbsolutePath().toFile(); + + try { + if (!dataDir.exists()) { + dataDir.mkdirs(); + } + if (!dataFile.exists()) { + dataFile.createNewFile(); + } + } catch (IOException e) { + e.printStackTrace();; + } + + } + + /** + * Saves the existing tasklist to a log file specified with filePath in the class constructor along + * with a timestamp. Creates the necessary directories and files if they do not exist. + * + * @param taskList + * @throws DukeException + */ + public void save(TaskList taskList) throws DukeException { + try (BufferedWriter writer = Files.newBufferedWriter(this.filePath)) { + LocalDateTime now = LocalDateTime.now(); + String msg = "duke.task.Task list (Last updated " + + now.format(DateTimeFormatter.ofPattern("MMM d yyyy, HH:mm:ss a")) + + "):\n"; + writer.write(msg + taskList.toString()); + } catch (IOException e) { + throw new DukeException("CANNOT SAVE TASKLIST TO FILE: " + e.getMessage()); + } + } + + /** + * loads a saved tasklist from a log file and parses it into an arrayList of Strings that describe + * the task. + * + * @return an arrayList of Strings that describe the tasks saved in the tasklist + * @throws DukeException + */ + public ArrayList load() throws DukeException { + try (BufferedReader reader = Files.newBufferedReader(this.filePath)) { + ArrayList tasksStr = new ArrayList<>(); + String str; + reader.readLine(); + while((str = reader.readLine()) != null) { + tasksStr.add(str.substring(str.indexOf(".") + 1).trim()); + } + + return tasksStr; + } catch (IOException e) { + throw new DukeException("GOT ERROR LOADING TASKS! " + e.getMessage()); + } + } + + +} diff --git a/src/main/java/duke/task/Deadline.java b/src/main/java/duke/task/Deadline.java new file mode 100644 index 0000000000..8f8cdd3e1c --- /dev/null +++ b/src/main/java/duke/task/Deadline.java @@ -0,0 +1,15 @@ +package duke.task; + +import duke.datetime.DateTimeUtility; + +public class Deadline extends TimedTask { + public static final String TASK_ICON = "D"; + public Deadline(String description, String by) { + super(description, DateTimeUtility.formatString(by)); + } + + @Override + public String toString() { + return "["+ this.TASK_ICON +"]" + super.toString() + " (by: " + super.formatBy() + ")"; + } +} diff --git a/src/main/java/duke/task/Event.java b/src/main/java/duke/task/Event.java new file mode 100644 index 0000000000..1d006c3751 --- /dev/null +++ b/src/main/java/duke/task/Event.java @@ -0,0 +1,15 @@ +package duke.task; + +import duke.datetime.DateTimeUtility; + +public class Event extends TimedTask { + public static final String TASK_ICON = "E"; + public Event(String description, String by) { + super(description, DateTimeUtility.formatString(by)); + } + + @Override + public String toString() { + return "["+ this.TASK_ICON +"]" + super.toString() + " (at: " + super.formatBy() + ")"; + } +} diff --git a/src/main/java/duke/task/Task.java b/src/main/java/duke/task/Task.java new file mode 100644 index 0000000000..7ea8fc33a2 --- /dev/null +++ b/src/main/java/duke/task/Task.java @@ -0,0 +1,31 @@ +package duke.task; + +public class Task { + public static final String ICON_TICK = "✓"; + public static final String ICON_CROSS = "✗"; + + protected String description; + protected boolean isDone; + + public Task(String description) { + this.description = description; + this.isDone = false; + } + + public String getDescription() { return this.description; }; + + public void setDescription(String description) { this.description = description; } + + public void setDone(boolean status) { + this.isDone = status; + } + + public String getStatusIcon() { + return (isDone ? ICON_TICK : ICON_CROSS); //return tick or X symbols + } + + @Override + public String toString() { + return "[" + getStatusIcon() + "] " + 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..9207a3920a --- /dev/null +++ b/src/main/java/duke/task/TaskList.java @@ -0,0 +1,265 @@ +package duke.task; + +import duke.Duke; +import duke.DukeException; +import duke.datetime.DateTimeFormat; +import duke.datetime.DateTimeUtility; + +import java.time.DateTimeException; +import java.util.ArrayList; + +public class TaskList { + private ArrayList tasks; + + public TaskList() { + this.tasks = new ArrayList<>(); + } + + /** + * Overloaded constructor to initialise taskList with tasks loaded from an existing log file. + * + * @param tasksStr + * @throws DukeException + */ + public TaskList(ArrayList tasksStr) throws DukeException { + assert !tasksStr.isEmpty() : "tasksStr cannot be empty"; + + this.tasks = new ArrayList<>(); + for (String taskStr : tasksStr) { + this.tasks.add(TaskList.parseTaskFromString(taskStr)); + } + } + + public boolean isEmpty() { + return tasks.isEmpty(); + } + + public int getNumberOfTasks() { + return tasks.size(); + } + + public String getTaskByIdx(int idx) { + return tasks.get(idx).toString(); + } + + /** + * Returns a string describing all the tasks in the taskList with a valid + * Date or DateTime that is before the Date or DateTime specified. + * + * Performs input validation on the input Date or DateTime string and throws a + * DukeException if it is of an invalid format. + * + * @param by + * @return + * @throws DukeException + */ + public String filterTasksByDate(String by) throws DukeException { + assert !by.isEmpty() : "by cannot be empty"; + + if (DateTimeUtility.checkDateTimeType(by) != DateTimeFormat.String) { + ArrayList filtered = new ArrayList<>(); + + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i) instanceof TimedTask) { + String taskBy = ((TimedTask)tasks.get(i)).getByString(); + try { + if (DateTimeUtility.compare(by, taskBy) >= 0) { + filtered.add(tasks.get(i)); + } + } catch (DateTimeException e) {} + } + } + + return tasks2String(filtered); + + } else { + throw new DukeException("U NID 2 GIV CORRECT DATE FOMAT!"); + } + } + + public String filterTasksByDescription(String description) { + assert !description.isEmpty() : "description cannot be empty"; + ArrayList filtered = new ArrayList<>(); + String descriptionLower = description.toLowerCase(); + + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i).getDescription().toLowerCase().contains(descriptionLower)) { + filtered.add(tasks.get(i)); + } + } + + return tasks2String(filtered); + } + + /** + * Adds a TODO task to the existing taskList. The task is marked as undone by default. + * Returns a string representation of the added task. + * + * @param name + * @return + */ + public String addTodo(String name) { + assert !name.isEmpty() : "name cannot be empty"; + + Todo newTask = new Todo(name); + tasks.add(newTask); + return newTask.toString(); + } + + /** + * Adds a Deadline task to the existing taskList. The task is marked as undone by default. + * Returns a string representation of the added task. + * + * @param name + * @return + */ + public String addDeadline(String name, String by) { + assert !name.isEmpty() : "name cannot be empty"; + assert !by.isEmpty() : "by cannot be empty"; + + Deadline newTask = new Deadline(name, by); + tasks.add(newTask); + return newTask.toString(); + } + + /** + * Adds an Event task to the existing taskList. The task is marked as undone by default. + * Returns a string representation of the added task. + * + * @param name + * @return + */ + public String addEvent(String name, String by) { + assert !name.isEmpty() : "name cannot be empty"; + assert !by.isEmpty() : "by cannot be empty"; + + Event newTask = new Event(name, by); + tasks.add(newTask); + return newTask.toString(); + } + + /** + * Marks an existing task in the taskList as done, accessed by index. Performs input + * validation on the task index and throws a DukeException if the input is invalid. + * + * @param idx + * @throws DukeException + */ + public void markTaskAsDone(int idx) throws DukeException { + if (idx < 0 || idx >= tasks.size()) { + throw new DukeException("U DOAN HAS TASK WIF DIS LABEL"); + } else { + tasks.get(idx).setDone(true); + } + } + + /** + * Updates an existing task in the taskList with a new description, accessed by index. Performs input + * validation on the task index and throws a DukeException if the input is invalid. + * + * @param idx + * @param newDescription + * @throws DukeException + */ + public String updateTaskDescription(int idx, String newDescription) throws DukeException { + if (idx < 0 || idx >= tasks.size()) { + throw new DukeException("U DOAN HAS TASK WIF DIS LABEL"); + } else { + tasks.get(idx).setDescription(newDescription); + return tasks.get(idx).toString(); + } + } + + /** + * Updates an existing task in the tasklist with a new date, accessed by index. Performs input + * validation on the task index and date and throws a DukeException if the input index is invalid, + * or if the task does not take in a date. + * + * @param idx + * @param newDate + * @throws DukeException + */ + + public String updateTaskDate(int idx, String newDate) throws DukeException { + if (idx < 0 || idx >= tasks.size()) { + throw new DukeException("U DOAN HAS TASK WIF DIS LABEL"); + } else if (!(tasks.get(idx) instanceof TimedTask)) { + throw new DukeException("DIS TASK GOTS NO DATE FER UPDATE LULZ"); + } else { + ((TimedTask)tasks.get(idx)).setByString(newDate); + return tasks.get(idx).toString(); + } + } + + + /** + * Removes an existing task from the taskList by index and returns its string representation. + * Performs input validation on the specified index and throws a DukeException when index + * is invalid. + * + * @param idx + * @return + * @throws DukeException + */ + public String popTask(int idx) throws DukeException { + if (tasks.isEmpty()) { + throw new DukeException("U CANT DELET ANYTHIN COZ U HAS NO TASKZ NAO LOLOL"); + } + if (idx < 0 || idx >= tasks.size()) { + throw new DukeException("U DOAN HAS TASK WIF DIS LABEL"); + } else { + return tasks.remove(idx).toString(); + } + } + + /** + * Parses an input string and returns a Task of the correct type, name, status and + * deadline (if available), otherwise throws a DukeException if input string is + * of the wrong format. + * + * @param str + * @return + * @throws DukeException + */ + public static Task parseTaskFromString(String str) throws DukeException { + assert !str.isEmpty() : "str cannot be empty"; + + Task task; + String[] temp = str.split("]"); + String taskType = String.valueOf(temp[0].charAt(1)); + boolean isDone = (String.valueOf(temp[1].charAt(1)).equals(Task.ICON_TICK)); + + if (taskType.equals(Todo.TASK_ICON)) { + task = new Todo(temp[2].trim()); + } else if (taskType.equals(Deadline.TASK_ICON)) { + String taskName = temp[2].split("\\(by:")[0].trim(); + String by = temp[2].split(" \\(by:")[1].trim(); + by = by.substring(0, by.length()-1); + task = new Deadline(taskName, by); + } else if (taskType.equals(Event.TASK_ICON)) { + String taskName = temp[2].split(" \\(at:")[0].trim(); + String by = temp[2].split(" \\(at:")[1].trim(); + by = by.substring(0, by.length()-1); + task = new Event(taskName, by); + } else { + throw new DukeException("TASK ICON NOT RECOGNISED"); + } + + task.setDone(isDone); + return task; + } + + private static String tasks2String(ArrayList tasks) { + String ret = ""; + for (int i = 0; i < tasks.size(); i++) { + ret += (i + 1) + ". " + tasks.get(i) + "\n"; + } + return ret; + } + + @Override + public String toString() { + return TaskList.tasks2String(this.tasks); + + } +} diff --git a/src/main/java/duke/task/TimedTask.java b/src/main/java/duke/task/TimedTask.java new file mode 100644 index 0000000000..eb2046b0bf --- /dev/null +++ b/src/main/java/duke/task/TimedTask.java @@ -0,0 +1,32 @@ +package duke.task; + + +import duke.datetime.DateTimeFormat; +import duke.datetime.DateTimeUtility; + +public class TimedTask extends Task { + protected DateTimeFormat format; + protected String byString; + + public TimedTask(String description, String by) { + super(description); + this.byString = by; + } + + public void setByString(String byString) { + this.byString = byString; + } + + public String formatBy() { + return DateTimeUtility.formatString(this.byString); + } + + public String getByString() { + return byString; + } + + @Override + public String toString() { + return super.toString(); + } +} diff --git a/src/main/java/duke/task/Todo.java b/src/main/java/duke/task/Todo.java new file mode 100644 index 0000000000..8d8474e2ec --- /dev/null +++ b/src/main/java/duke/task/Todo.java @@ -0,0 +1,14 @@ +package duke.task; + +public class Todo extends Task { + public static final String TASK_ICON = "T"; + + public Todo(String description) { + super(description); + } + + @Override + public String toString() { + return "[" + this.TASK_ICON + "]" + super.toString(); + } +} diff --git a/src/main/java/duke/ui/Ui.java b/src/main/java/duke/ui/Ui.java new file mode 100644 index 0000000000..663ad128ef --- /dev/null +++ b/src/main/java/duke/ui/Ui.java @@ -0,0 +1,139 @@ +package duke.ui; + +import duke.task.TaskList; + +import java.util.Scanner; + +public class Ui { + private static final String LOGO = + " ,\n" + + " \\`-._ __\n" + + " \\\\ `-..____,.' `.\n" + + " :`. / \\`.\n" + + " : ) : : \\\n" + + " ;' ' ; | :\n" + + " ).. .. .:.`.; :\n" + + " /::... .:::... ` ;\n" + + " ; _ ' __ /:\\\n" + + " `:o> /\\o_> ;:. `.\n" + + " `-`.__ ; __..--- /:. \\\n" + + " === \\_/ ;=====_.':. ;\n" + + " ,/'`--'...`--.... ;\n" + + " ; ;\n" + + " .' ;\n" + + " .' ;\n" + + " .' .. , . ;\n" + + " : ::.. / ;::. |\n" + + " / `.;::. | ;:.. ;\n" + + " : |:. : ;:. ;\n" + + " : :: ;:.. |. ;\n" + + " : :; :::....| |\n" + + " /\\ ,/ \\ ;:::::; ;\n" + + " .:. \\:..| : ; '.--| ;\n" + + " ::. :'' `-.,,; ;' ; ;\n" + + ".-'. _.'\\ / `; \\,__: \\\n" + + "`---' `----' ; / \\,.,,,/\n" + + " `----` "; + + private static final String LINE = "-------------------------------------------------------------------------------"; + + private String botName = "duke.Duke"; + private String userName = "You"; + + private Scanner scanner; + + public Ui() { + this.scanner = new Scanner(System.in); + } + + /** + * Overloaded constructor for specifying name of the bot and user. + * + * @param botName + * @param userName + */ + public Ui(String botName, String userName) { + this(); + this.botName = botName; + this.userName = userName; + } + + /** + * Prints the tasks currently existing in the taskList that is loaded from + * a log file. + * + * @param taskList + */ + public void showLoadedTasks(TaskList taskList) { + System.out.println("\nTASKS LOADED FROM DATA FILE!!"); + System.out.println(taskList); + this.showLine(); + } + + /** + * Prints a welcome message for the chat application. + * + */ + public void showWelcome() { + System.out.println("\nOh hai kittehs! I r lolcatus. reziztents is fu... fut...\n" + + this.LOGO + "\n\n" + "reziztents dun werk.\n" + this.LINE); + } + + /** + * Prompts the user to enter a command and returns a string for the user's input. + * + * @return + */ + public String readCommand() { + System.out.print(this.userName + " sed: "); + return scanner.nextLine(); + } + + /** + * Prints a horizontal line. + */ + public void showLine() { + System.out.println(this.LINE); + } + + /** + * Formats and prints an error in the chat application. + * + * @param e + */ + public void showError(String e) { + System.out.println(this.fmtMsg(botName + " HAZ FINDED ERRRR!\n" + "=> " + e)); + } + + /** + * Formats and displays desired output to the chat application. + * + * @param msg + */ + public void display(String msg) { + System.out.println(this.fmtMsg(msg)); + } + + /** + * Displays error when tasks cannot be read from log file. + * + * @param e + */ + public void showLoadingError(String e) { + System.out.println(this.fmtMsg("CANNOT LOAD TASKS FROM FILE uwu owo \n" + " " + e)); + } + + /** + * Formats messages to be sent to the chat application. + * + * @param msg + * @return + */ + public String fmtMsg(String msg) { + return this.botName + " sed: " + msg; + } + + + + +} diff --git a/src/main/resources/images/DaDuke.jpeg b/src/main/resources/images/DaDuke.jpeg new file mode 100644 index 0000000000..8c640b4526 Binary files /dev/null and b/src/main/resources/images/DaDuke.jpeg differ diff --git a/src/main/resources/images/DaDuke.png b/src/main/resources/images/DaDuke.png new file mode 100644 index 0000000000..ec536ad73b 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/DukeDialogBox.fxml b/src/main/resources/view/DukeDialogBox.fxml new file mode 100644 index 0000000000..fe05f81e35 --- /dev/null +++ b/src/main/resources/view/DukeDialogBox.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..1941abc15e --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + +