diff --git a/.gitignore b/.gitignore index f69985ef1f..fe8925d278 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.txt text-ui-test/EXPECTED-UNIX.TXT +text-ui-test/* +Duke_Tasks diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..2e0733d270 --- /dev/null +++ b/build.gradle @@ -0,0 +1,53 @@ +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'com.github.johnrengelman.shadow' version '5.1.0' +} + +checkstyle { + toolVersion = '10.2' +} + +run { + enableAssertions = true +} + +group 'org.example' +version '0.2' +mainClassName = 'duke.gui.Launcher' + +// Output to build/libs/Duke.jar +shadowJar { + archiveBaseName.set('Duke') + archiveClassifier.set('') + archiveVersion.set('') +} + +repositories { + mavenCentral() +} + +test { + useJUnitPlatform() +} + +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' +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..fb88cedfc2 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..135ea49ee0 --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 8077118ebe..8e0cc72ae9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,193 @@ -# User Guide +# Duke: User Guide -## Features +## Features -### Feature-ABC +### Commands: -Description of the feature. -### Feature-XYZ +| Command | Description | +| --- | --- | +| [todo](#create-a-todo-todo) | Adds a new Todo task | +| [deadline](#create-a-deadline-deadline) | Adds a new Deadline task | +| [event](#create-an-event-event) | Adds a new Event task | +| [list](#list-out-all-tasks-list) | Shows info on all stored tasks | +| [mark](#mark-a-task-as-done-mark) | Indicates a task is done | +| [unmark](#mark-a-finished-task-as-incomplete-unmark) | Indicates a task is incomplete | +| [delete](#delete-a-task-delete) | Remove a task | +| [find](#find-tasks-containing-a-keyword-find) | List out the tasks whose description contains a certain keyword | +| [sort](#sort-tasks-in-chronological-order-sort) | Sort tasks in ascending order of time value, and number of time attributes | +| [SAVE](#ensure-data-on-tasks-is-saved-save ) | Make Duke save data on exit | +| [WIPE](#ensure-data-on-tasks-is-deleted-wipe) | Make Duke clear data on exit | +| [bye](#quit-bye) | Exit the app | -Description of the feature. ## Usage -### `Keyword` - Describe action +### Format Annotations: -Describe the action and its outcome. +[] indicates a required argument, [[]] indicates an optional argument -Example of usage: +### Create a Todo: `todo` -`keyword (optional arguments)` +Adds a new Todo task + +Format: `todo [DESCRIPTION]` + +Example of usage: `todo TP team meeting` + +Expected outcome: +``` +Got it. I've added this task: + [T][ ] TP team meeting +Now you have 1 task in the list. +``` + +### Create a Deadline: `deadline` + +Adds a new Deadline task + +Format: `deadline [DESCRIPTION] /by [D/M/YYYY] [[HH:MM(AM/PM)]]` + +Example of usage: `deadline IP /by 19/9/2022 11:59PM` + +Expected outcome: +``` +Got it. I've added this task: + [D][ ] IP (by 19 Sep 2022, 11:59PM) +Now you have 2 tasks in the list. +``` +### Create an Event: `event` + +Adds a new Event task + +Format: `event [DESCRIPTION] /at [D/M/YYYY] [[HH:MM(AM/PM)]]` + +Example of usage: `event TP tutorials /at 21/9/2022` + +Expected outcome: +``` +Got it. I've added this task: + [E][ ] TP tutorials (by 21 Sep 2022) +Now you have 3 tasks in the list. +``` +### List out all tasks: `list` + +Shows info on all stored tasks + +Format: `list` + +Example of usage: `list` + +Expected outcome: +``` +Ok, here are your tasks: + 1. [T][ ] TP team meeting + 2. [D][ ] IP (by 19 Sep 2022, 11:59PM) + 3. [E][ ] TP tutorials (by 21 Sep 2022) +``` +### Mark a task as done: `mark` + +Indicates a task is done + +Format: `mark [TASK NUMBER]` + +Example of usage: `mark 1` + +Expected outcome: +``` +Nice! I've marked this task as done: + [T][X] TP team meeting +``` +### Mark a finished task as incomplete: `unmark` + +Indicates a task is incomplete + +Format: `unmark [TASK NUMBER]` + +Example of usage: `unmark 1` + +Expected outcome: +``` +OK, I've marked this task as not done yet: + [T][ ] TP team meeting +``` +### Delete a task: `delete` + +Remove a task + +Format: `delete [TASK NUMBER]` + +Example of usage: `delete 3` + +Expected outcome: +``` +Noted. I've removed this task: + [E][ ] TP tutorials (at: 21 Sep 2022) +Now you have 2 tasks in the list. +``` +### Find tasks containing a keyword: `find` + +List out the tasks whose description contains a certain keyword + +Format: `find [KEYWORD]` + +Example of usage: `find TP` Expected outcome: +``` +Here are the tasks containing the keyword "TP" : + 1. [T][ ] TP team meeting + 3. [E][ ] TP tutorials (at: 21 Sep 2022) +``` +### Sort tasks in chronological order: `sort` + +Sort tasks in ascending order of time value, and number of time attributes + +Format: `sort` -Description of the outcome. +Example of usage: `sort` +Expected outcome: +``` +Ok, here are your tasks: + 1. [T][ ] TP team meeting + 2. [E][ ] TP tutorials (at: 21 Sep 2022) + 3. [D][ ] IP (by: 19 Sep 2022, 11:59PM) ``` -expected output +### Ensure data on tasks is saved: `SAVE` + +Make Duke save data on exit + +Format: `SAVE` + +Example of usage: `SAVE` + +Expected outcome: +``` +Data will be saved on exit +``` +### Ensure data on tasks is deleted: `WIPE` + +Make Duke clear data on exit + +Format: `WIPE` + +Example of usage: `WIPE` + +Expected outcome: +``` +Data will be wiped on exit +``` +### Quit: `bye` + +Exit the app + +Format: `bye` + +Example of usage: `bye` + +Expected outcome: ``` +Bye. Hope to see you again soon! +[App closes after a few seconds] +``` \ No newline at end of file diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..31e11f1500 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..41d9927a4d 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..41dfb87909 --- /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-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000000..1b6c787337 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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 POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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 "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..107acd32c4 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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 execute + +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 execute + +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 + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java deleted file mode 100644 index 5d313334cc..0000000000 --- a/src/main/java/Duke.java +++ /dev/null @@ -1,10 +0,0 @@ -public class Duke { - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - } -} diff --git a/src/main/java/duke/gui/DialogBox.java b/src/main/java/duke/gui/DialogBox.java new file mode 100644 index 0000000000..c9504117c5 --- /dev/null +++ b/src/main/java/duke/gui/DialogBox.java @@ -0,0 +1,62 @@ +package duke.gui; + +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +/** + * 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) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(img); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text on the right. + * + * @return The box + */ + private DialogBox flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + return this; + } + + public static DialogBox getUserDialog(String text, Image img) { + return new DialogBox(text, img); + } + + public static DialogBox getDukeDialog(String text, Image img) { + return new DialogBox(text, img).flip(); + } +} diff --git a/src/main/java/duke/gui/Launcher.java b/src/main/java/duke/gui/Launcher.java new file mode 100644 index 0000000000..2bd944a7b2 --- /dev/null +++ b/src/main/java/duke/gui/Launcher.java @@ -0,0 +1,12 @@ +package duke.gui; + +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/duke/gui/Main.java b/src/main/java/duke/gui/Main.java new file mode 100644 index 0000000000..048c557815 --- /dev/null +++ b/src/main/java/duke/gui/Main.java @@ -0,0 +1,32 @@ +package duke.gui; + +import java.io.IOException; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +/** + * A GUI for Duke using FXML. + */ +public class Main extends Application { + + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + Scene scene = new Scene(ap); + stage.setScene(scene); + stage.setTitle("Duke"); + stage.setResizable(false); + stage.setMinHeight(600.0); + stage.setMinWidth(400.0); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/duke/gui/MainWindow.java b/src/main/java/duke/gui/MainWindow.java new file mode 100644 index 0000000000..685dd707b1 --- /dev/null +++ b/src/main/java/duke/gui/MainWindow.java @@ -0,0 +1,69 @@ +package duke.gui; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +import duke.services.Duke; +import duke.services.Ui; +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 final Image userImage = new Image(this.getClass().getResourceAsStream("/images/User.png")); + private final Image dukeImage = new Image(this.getClass().getResourceAsStream("/images/Duke.png")); + + @FXML + public void initialize() throws IOException { + //Scroll down to the end every time dialogContainer's height changes + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + + Duke.activate(); + //Show Duke's greeting + dialogContainer.getChildren().add(DialogBox.getDukeDialog(Ui.getReply(), dukeImage)); + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + String response = Duke.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + + if (!Duke.isActive()) { + userInput.setDisable(true); + sendButton.setDisable(true); + new Timer().schedule(new TimerTask() { + public void run() { + Platform.exit(); + System.exit(0); + } + }, 2000); + } + } +} diff --git a/src/main/java/duke/services/Duke.java b/src/main/java/duke/services/Duke.java new file mode 100644 index 0000000000..ac46302483 --- /dev/null +++ b/src/main/java/duke/services/Duke.java @@ -0,0 +1,91 @@ +package duke.services; + +import java.io.IOException; + +/** + * Provides Duke's high-level functionality + */ +public class Duke { + + /** Is Duke interacting with the user? */ + private static boolean isActive; + + /** + * Duke performs its activation behaviour. Save data is loaded. + * + * @throws IOException From IO errors when loading save data + */ + public static void activate() throws IOException { + isActive = true; + Storage.loadData(); + Ui.setReply(new String[] { + "Hello! I'm Duke", + "What can I do for you?", + }); + } + + /** + * Responds to the command and gives a reply + * + * @param command User's inputted command + * @return Duke's reply + */ + public static String getResponse(String command) { + String[] words = Parser.convertToWords(command); + if (words.length > 0) { + try { + //could try grouping words.length == 1 cases + if (words.length == 1 && words[0].equals("bye")) { + Duke.deactivate(); + } else if (words.length == 1 && words[0].equals("list")) { + TaskList.listTasks(); + } else if (words.length == 1 && words[0].equals("SAVE")) { + Storage.wipeDataOnExit(false); + } else if (words.length == 1 && words[0].equals("WIPE")) { + Storage.wipeDataOnExit(true); + } else if (words[0].equals("todo")) { + TaskList.addTodo(words); + } else if (words[0].equals("deadline")) { + TaskList.addDeadline(words); + } else if (words[0].equals("event")) { + TaskList.addEvent(words); + } else if (words[0].equals("mark")) { + TaskList.markTaskAsDone(words); + } else if (words[0].equals("unmark")) { + TaskList.markTaskAsNotDone(words); + } else if (words[0].equals("delete")) { + TaskList.deleteTask(words); + } else if (words[0].equals("find")) { + TaskList.findTasksContainingKeyword(words); + } else if (words[0].equals("sort")) { + TaskList.sortByTimeAsc(); + } else { + Ui.setReply(new String[]{"I'm sorry, I don't know that command"}); + } + } catch (IllegalArgumentException | IOException e) { + Ui.setReply(new String[]{e.getMessage()}); + } + } + assert !Ui.getReply().isEmpty() : "Reply can't be empty"; + return Ui.getReply(); + } + + + + /** + * Duke performs its deactivation behaviour. Data is saved. + * + * @throws IOException From IO errors when saving data + */ + public static void deactivate() throws IOException { + Storage.saveData(); + Ui.setReply(new String[] { + "Bye. Hope to see you again soon!", + }); + isActive = false; + } + + public static boolean isActive() { + return isActive; + } +} diff --git a/src/main/java/duke/services/Parser.java b/src/main/java/duke/services/Parser.java new file mode 100644 index 0000000000..f97822e4a6 --- /dev/null +++ b/src/main/java/duke/services/Parser.java @@ -0,0 +1,163 @@ +package duke.services; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Locale; + +/** Handles reading of user commands and string manipulation */ +public class Parser { + + /** Points to the current word being read in the current command */ + private static int currWordIndex = 0; + + /** + * Converts the command to an array of words split by " ". + * + * @param command User's inputted command + * @return An array of the command's words, split by " ". + */ + public static String[] convertToWords(String command) { + return Arrays.stream(command.strip().split(" ")).toArray(String[]::new); + } + + /** + * Retrieves the description argument in the command + * @param words The words of the command entered, first is some valid command name + * @param stop The word before which the description ends + * @return The description specified in words + * @throws IllegalArgumentException If words specifies an empty description + */ + public static String getDescription(String[] words, String stop) { + currWordIndex = 1; + StringBuilder descBuilder = new StringBuilder(); + boolean descIsEmpty = true; + + while (currWordIndex < words.length && !words[currWordIndex].equals(stop)) { + if (words[currWordIndex].isEmpty()) { + descBuilder.append(" "); + } else { + descBuilder.append(words[currWordIndex]).append(" "); + descIsEmpty = false; + } + ++currWordIndex; + } + + if (descIsEmpty) { + throw new IllegalArgumentException("OOPS!!! Description can't be empty"); + } + + return descBuilder.deleteCharAt(descBuilder.length() - 1).toString(); //remove last whitespace + } + + /** + * Retrieves the timing argument in the command, which must be of the form d/M/yyyy + * followed by an optional (h:mm)am/pm + * @param words The words of the command entered, first is some valid command name + * @param flag The flag that the timing belongs to + * @return The timing specified in words + * @throws IllegalArgumentException If words is missing flag or the timing is empty/incorrect format + */ + public static String getTiming(String[] words, String flag) { + if (currWordIndex >= words.length) { + throw new IllegalArgumentException("OOPS!!! " + flag + " not found"); + } else if (currWordIndex == words.length - 1) { + throw new IllegalArgumentException("OOPS!!! Timing for " + flag + " can't be empty"); + } + + ++currWordIndex; + try { + return (currWordIndex == words.length - 1) + ? reformatDate(words[currWordIndex], "d/M/yyyy", "d MMM yyyy") + : reformatDateTime(words[currWordIndex] + " " + words[++currWordIndex], + "d/M/yyyy h:mma", "d MMM yyyy, h:mma"); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("OOPS!!! I don't understand that date or time"); + } + } + + /** + * Converts the date into a LocalDate + * @param date The date to convert + * @param format The date format + * @return The date as a LocalDate + * @throws IllegalArgumentException If format is incorrect + */ + public static LocalDate convertToLocalDate(String date, String format) { + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .appendPattern(format) + .toFormatter(Locale.getDefault()); + return LocalDate.parse(date, formatter); + } + + /** + * Converts the date to a new format + * @param date The date to reformat + * @param inFormat The current format + * @param outFormat The new format + * @return The reformatted date + * @throws IllegalArgumentException If format is incorrect + */ + public static String reformatDate(String date, String inFormat, String outFormat) { + return convertToLocalDate(date, inFormat) + .format(DateTimeFormatter.ofPattern(outFormat, Locale.getDefault())); + } + + /** + * Converts the datetime into a LocalDateTime + * @param dateTime The datetime to convert + * @param format The datetime format + * @return The datetime as a LocalDateTime + * @throws IllegalArgumentException If format is incorrect + */ + public static LocalDateTime convertToLocalDateTime(String dateTime, String format) { + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .appendPattern(format) + .toFormatter(Locale.getDefault()); + return LocalDateTime.parse(dateTime, formatter); + } + + /** + * Converts the datetime to a new format + * @param dateTime The datetime to reformat + * @param inFormat The current format + * @param outFormat The new format + * @return The reformatted datetime + * @throws IllegalArgumentException If datetime has incorrect format + */ + public static String reformatDateTime(String dateTime, String inFormat, String outFormat) { + return convertToLocalDateTime(dateTime, inFormat) + .format(DateTimeFormatter.ofPattern(outFormat, Locale.getDefault())); + } + + /** + * Gets the task number (integer pointing to a task) specified in the command + * + * @param words The words of the command entered, first is always some valid command name + * @return The task number specified + * @throws IllegalArgumentException If words has an invalid number of arguments or invalid argument value + */ + public static int getTaskNumber(String[] words) { + if (TaskList.getTasks().size() == 0) { + throw new IllegalArgumentException("OOPS!!! No tasks stored for me to do that"); + } + + int taskNumber = 0; + + try { + taskNumber = (words.length == 2) ? Integer.parseInt(words[1]) : 0; + if (taskNumber <= 0 || taskNumber > TaskList.getTasks().size()) { + throw new IllegalArgumentException(); + } + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("OOPS!!! The task number must be from 1 to " + + TaskList.getTasks().size()); + } + + return taskNumber; + } +} diff --git a/src/main/java/duke/services/Storage.java b/src/main/java/duke/services/Storage.java new file mode 100644 index 0000000000..3ac9c63c24 --- /dev/null +++ b/src/main/java/duke/services/Storage.java @@ -0,0 +1,106 @@ +package duke.services; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import duke.tasks.Deadline; +import duke.tasks.Event; +import duke.tasks.Task; +import duke.tasks.Todo; + +/** Handles saving and loading data */ +public class Storage { + + /** Saved data on the stored tasks */ + private static File dataSaved; + + /** Is data going to be wiped on exit? */ + private static boolean willWipeData = false; + + /** + * Loads saved data on stored tasks from Duke_Tasks.txt if it exists, otherwise creates it + * + * @throws IOException If an IO error occurs or the parent directory doesn't exist + */ + public static void loadData() throws IOException { + Path path = Paths.get("Duke_Tasks"); + boolean directoryExists = Files.exists(path); + if (directoryExists) { + dataSaved = path.toFile(); + BufferedReader br = new BufferedReader(new FileReader(dataSaved)); + String line = br.readLine(); + String[] words; + Task task; + while (line != null) { + //format of savedata lines: [typeSymbol][1 or 0] [desc] [flag] [timing] + assert !line.isEmpty() : "Savedata lines must be non-empty"; + words = Arrays.stream(line.split(" ")).toArray(String[]::new); + if (line.charAt(0) == 'T') { + task = new Todo(Parser.getDescription(words, null)); + } else if (line.charAt(0) == 'D') { + task = new Deadline(Parser.getDescription(words, "/by"), Parser.getTiming(words, "/by")); + } else if (line.charAt(0) == 'E') { + task = new Event(Parser.getDescription(words, "/at"), Parser.getTiming(words, "/at")); + } else { + throw new IllegalArgumentException("OOPS!!! Found a stored task of unknown type"); + } + if (line.charAt(1) == '1') { + task.markAsDone(); + } + TaskList.getTasks().add(task); + line = br.readLine(); + } + } else { + dataSaved = Files.createFile(path).toFile(); + } + } + + /** + * Tells storage whether to wipe or save data on exit and displays outcome + * + * @param willWipe True/false will make data be wiped/saved on exit resp. + */ + public static void wipeDataOnExit(boolean willWipe) { + willWipeData = willWipe; + Ui.setReply(new String[] { + "Data will be " + (willWipe ? "wiped" : "saved") + " on exit"} + ); + } + + /** + * Writes saved data on stored tasks to Duke_Tasks.txt + * + * @throws IOException If an I/O error occurs or the parent directory doesn't exist + */ + public static void saveData() throws IOException { + new FileWriter(dataSaved).close(); + if (!willWipeData) { + BufferedWriter bf = new BufferedWriter(new FileWriter(dataSaved)); + StringBuilder lineBuilder = new StringBuilder(); + for (Task task : TaskList.getTasks()) { + //format of savedata lines: [typeSymbol][1 or 0] [desc] [flag] [timing] + lineBuilder.append(task.getTypeSymbol()) + .append(task.getStatusIcon().equals("X") ? '1' : '0') + .append(" ") + .append(task.getDescription()); + if (task instanceof Deadline) { + lineBuilder.append(" /by ").append(((Deadline) task).getEnteredDeadline()); + } else if (task instanceof Event) { + lineBuilder.append(" /at ").append(((Event) task).getEnteredTime()); + } + bf.write(lineBuilder.toString()); + bf.newLine(); + lineBuilder.setLength(0); + } + bf.close(); + } + } +} diff --git a/src/main/java/duke/services/TaskList.java b/src/main/java/duke/services/TaskList.java new file mode 100644 index 0000000000..7aefe0fce5 --- /dev/null +++ b/src/main/java/duke/services/TaskList.java @@ -0,0 +1,174 @@ +package duke.services; + +import java.util.ArrayList; +import java.util.List; + +import duke.tasks.Deadline; +import duke.tasks.Event; +import duke.tasks.Task; +import duke.tasks.Todo; + +/** Handles tasks */ +public class TaskList { + /** The tasks stored */ + private static List tasks = new ArrayList<>(); + + public static List getTasks() { + return tasks; + } + + /** + * Stores the task and displays outcome + */ + public static void addTask(Task task) { + tasks.add(task); + Ui.setReply(new String[] { + "Got it. I've added this task:", + " " + task, + "Now you have " + tasks.size() + " task" + (tasks.size() == 1 ? "" : "s") + " in the list." + }); + } + + /** + * Stores a Todo outlined in the command and displays outcome + * @param words The words of the command entered, first is always "todo" + * @throws IllegalArgumentException If words specifies an empty description + */ + public static void addTodo(String[] words) { + addTask(new Todo(Parser.getDescription(words, null))); + } + + /** + * Stores a Deadline outlined in the command and displays outcome + * @param words The words of the command entered, first is always "deadline" + * @throws IllegalArgumentException if words specifies an empty description or empty date or is missing /by + */ + public static void addDeadline(String[] words) { + addTask(new Deadline(Parser.getDescription(words, "/by"), Parser.getTiming(words, "/by"))); + } + + /** + * Stores an Event outlined in the command and displays outcome + * @param words The words of the command entered, first is always "event" + * @throws IllegalArgumentException If words specifies an empty description or empty date or is missing /at + */ + public static void addEvent(String[] words) { + addTask(new Event(Parser.getDescription(words, "/at"), Parser.getTiming(words, "/at"))); + } + + /** + * Marks the specified task as done and displays outcome + * @param words The words of the command entered, first is always "mark" + * @throws IllegalArgumentException If words has an invalid number of arguments or invalid argument value + */ + public static void markTaskAsDone(String[] words) { + Task task = tasks.get(Parser.getTaskNumber(words) - 1).markAsDone(); + Ui.setReply(new String[]{ + "Nice! I've marked this task as done:", + " " + task + }); + } + + /** + * Marks the specified task as not done and displays outcome + * @param words The words of the command entered, first is always "unmark" + * @throws IllegalArgumentException If words has an invalid number of arguments or invalid argument value + */ + public static void markTaskAsNotDone(String[] words) { + Task task = tasks.get(Parser.getTaskNumber(words) - 1).markAsNotDone(); + Ui.setReply(new String[]{ + "OK, I've marked this task as not done yet:", + " " + task + }); + } + + /** + * Deletes the specified task and displays outcome + * @param words The words of the command entered, first is always "delete" + * @throws IllegalArgumentException If words has an invalid number of arguments or invalid argument value + */ + public static void deleteTask(String[] words) { + Task removedTask = tasks.remove(Parser.getTaskNumber(words) - 1); + Ui.setReply(new String[]{ + "Noted. I've removed this task:", + " " + removedTask, + "Now you have " + tasks.size() + " task" + (tasks.size() == 1 ? "" : "s") + " in the list." + }); + } + + /** + * Lists out information on all tasks stored + * + * @param initialMessage The statement before the list of tasks + */ + public static void listTasks(String initialMessage) { + String[] taskDescriptions = new String[getTasks().size() + 1]; + taskDescriptions[0] = initialMessage; + for (int i = 1; i < taskDescriptions.length; ++i) { + taskDescriptions[i] = " " + i + ". " + tasks.get(i - 1); + } + Ui.setReply(taskDescriptions); + } + + public static void listTasks() { + listTasks("Here are the tasks in your list:"); + } + + /** + * Displays all tasks whose descriptions contain the keyword specified in words + * @param words The words of the command entered, first is always "find" + */ + public static void findTasksContainingKeyword(String[] words) { + StringBuilder keywordBuilder = new StringBuilder(); + for (int i = 1; i < words.length; ++i) { + keywordBuilder.append(words[i]).append(" "); + } + String keyword = words.length == 1 ? "" : keywordBuilder.deleteCharAt(keywordBuilder.length() - 1).toString(); + + ArrayList matchingTasks = new ArrayList<>(); + matchingTasks.add("Here are the tasks containing the keyword \"" + keyword + "\" :"); + for (int i = 0; i < tasks.size(); ++i) { + Task currTask = tasks.get(i); + if (currTask.getDescription().contains(keyword)) { + matchingTasks.add(" " + (i + 1) + ". " + currTask); + } + } + Ui.setReply(matchingTasks.toArray(String[]::new)); + } + + /** + * Sorts tasks sorted in ascending order of datetime and detail, + * e.g. tasks without any time are always first. + */ + public static void sortByTimeAsc() { + tasks.sort((task1, task2) -> { + if (task1 instanceof Todo) { + return -1; + } else if (task2 instanceof Todo) { + return 1; + } + String time1 = (task1 instanceof Event) ? ((Event) task1).getEnteredTime() + : ((Deadline) task1).getEnteredDeadline(); + String time2 = (task2 instanceof Event) ? ((Event) task2).getEnteredTime() + : ((Deadline) task2).getEnteredDeadline(); + String dateFormat = "d/M/yyyy"; + String dateTimeFormat = "d/M/yyyy h:mma"; + + if (time1.contains(" ")) { + if (!time2.contains(" ")) { + return 1; + } + return (Parser.convertToLocalDateTime(time1, dateTimeFormat)) + .compareTo(Parser.convertToLocalDateTime(time2, dateTimeFormat)); + } else { + if (time2.contains(" ")) { + return -1; + } + return (Parser.convertToLocalDate(time1, dateFormat)) + .compareTo(Parser.convertToLocalDate(time2, dateFormat)); + } + }); + + listTasks("Ok, here are your tasks:"); + } +} diff --git a/src/main/java/duke/services/Ui.java b/src/main/java/duke/services/Ui.java new file mode 100644 index 0000000000..5b35b54003 --- /dev/null +++ b/src/main/java/duke/services/Ui.java @@ -0,0 +1,27 @@ +package duke.services; + +/** + * Handles Duke's dialog + */ +public class Ui { + /** What Duke will tell the user after a command */ + private static String reply; + + /** + * Updates duke's reply to the given lines, separated by newline + */ + public static void setReply(String[] lines) { + StringBuilder replyBuilder = new StringBuilder(); + System.out.println("____________________________________________________________"); + for (String line : lines) { + System.out.println(line); + replyBuilder.append(line).append("\n"); + } + System.out.println("____________________________________________________________\n"); + reply = replyBuilder.toString(); + } + + public static String getReply() { + return reply; + } +} diff --git a/src/main/java/duke/tasks/Deadline.java b/src/main/java/duke/tasks/Deadline.java new file mode 100644 index 0000000000..a63f16ad2b --- /dev/null +++ b/src/main/java/duke/tasks/Deadline.java @@ -0,0 +1,37 @@ +package duke.tasks; + +import duke.services.Parser; + +/** + * Tasks with a deadline + */ +public class Deadline extends Task { + + /** Format: d MMM yyyy, h:mma */ + private String deadline; + + /** + * Constructs a new Deadline with the given description and deadline + * + * @param description The description + * @param deadline The deadline + */ + public Deadline(String description, String deadline) { + super(description, 'D'); + this.deadline = deadline; + } + + /** + * @return The deadline in the format that was entered + */ + public String getEnteredDeadline() { + return (deadline.indexOf(',') == -1) + ? Parser.reformatDate(deadline, "d MMM yyyy", "d/M/yyyy") + : Parser.reformatDateTime(deadline, "d MMM yyyy, h:mma", "d/M/yyyy h:mma"); + } + + @Override + public String toString() { + return super.toString() + " (by: " + deadline + ")"; + } +} diff --git a/src/main/java/duke/tasks/Event.java b/src/main/java/duke/tasks/Event.java new file mode 100644 index 0000000000..5c753fcaa6 --- /dev/null +++ b/src/main/java/duke/tasks/Event.java @@ -0,0 +1,37 @@ +package duke.tasks; + +import duke.services.Parser; + +/** + * Tasks that occur at a certain time + */ +public class Event extends Task { + + /** The time of occurrence. Format: d MMM yyyy, h:mma */ + private String time; + + /** + * Constructs a new Event with the given description and timing of occurrence + * + * @param description The description + * @param time The time of occurrence + */ + public Event(String description, String time) { + super(description, 'E'); + this.time = time; + } + + /** + * @return The time in the format that was entered + */ + public String getEnteredTime() { + return (time.indexOf(',') == -1) + ? Parser.reformatDate(time, "d MMM yyyy", "d/M/yyyy") + : Parser.reformatDateTime(time, "d MMM yyyy, h:mma", "d/M/yyyy h:mma"); + } + + @Override + public String toString() { + return super.toString() + " (at: " + time + ")"; + } +} diff --git a/src/main/java/duke/tasks/Task.java b/src/main/java/duke/tasks/Task.java new file mode 100644 index 0000000000..5bf68caf1b --- /dev/null +++ b/src/main/java/duke/tasks/Task.java @@ -0,0 +1,68 @@ +package duke.tasks; + +/** + * Tasks for keeping track of + */ +public abstract class Task { + + /** Describes what this task entails */ + protected String description; + + /** Indicates what kind of task this is */ + protected final char typeSymbol; + + /** Is this task done? */ + protected boolean isDone; + + /** + * Constructs a new task with the given description + * @param description The task description + * @param typeSymbol The symbol to indicate type + */ + public Task(String description, char typeSymbol) { + this.description = description; + this.typeSymbol = typeSymbol; + this.isDone = false; + } + + /** + * Marks this task as done + * @return The task + */ + public Task markAsDone() { + this.isDone = true; + return this; + } + + /** + * Marks this task as not done + * @return The task + */ + public Task markAsNotDone() { + this.isDone = false; + return this; + } + + public char getTypeSymbol() { + return typeSymbol; + } + + /** + * @return "X" if the task is done, " " otherwise + */ + public String getStatusIcon() { + return (isDone ? "X" : " "); // mark done task with X + } + + public String getDescription() { + return description; + } + + /** + * @return The task type, status and description + */ + @Override + public String toString() { + return "[" + getTypeSymbol() + "][" + getStatusIcon() + "] " + getDescription(); + } +} diff --git a/src/main/java/duke/tasks/Todo.java b/src/main/java/duke/tasks/Todo.java new file mode 100644 index 0000000000..8627da1695 --- /dev/null +++ b/src/main/java/duke/tasks/Todo.java @@ -0,0 +1,16 @@ +package duke.tasks; + +/** + * Tasks with only a description + */ +public class Todo extends Task { + + /** + * Constructs a new Todo with the given description + * + * @param description The task description + */ + public Todo(String description) { + super(description, 'T'); + } +} diff --git a/src/main/resources/images/Duke.png b/src/main/resources/images/Duke.png new file mode 100644 index 0000000000..d893658717 Binary files /dev/null and b/src/main/resources/images/Duke.png differ diff --git a/src/main/resources/images/User.png b/src/main/resources/images/User.png new file mode 100644 index 0000000000..3c82f45461 Binary files /dev/null and b/src/main/resources/images/User.png differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..5c328f0d0e --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..3f15e94377 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + +