diff --git a/README.md b/README.md index 90aa7f092a..b2033aaa29 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Duke project template +# Espresso 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. @@ -13,7 +13,7 @@ Prerequisites: JDK 17, update Intellij to the most recent version. 1. If there are any further prompts, accept the defaults. 1. Configure the project to use **JDK 17** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
In the same dialog, set the **Project language level** field to the `SDK default` option. -3. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: +3. After that, locate the `src/main/java/Espresso.java` file, right-click it, and choose `Run Espresso.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: ``` Hello from ____ _ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..2e03d787b1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,59 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'checkstyle' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + String javaFxVersion = '17.0.7' + 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 { + mainClass.set("espresso.Launcher") +} + +shadowJar { + archiveBaseName = 'espresso' + archiveClassifier = null + archiveFileName = 'espresso.jar' +} + +run { + standardInput = System.in +} \ No newline at end of file diff --git a/data/Espresso.txt b/data/Espresso.txt new file mode 100644 index 0000000000..59f141af70 --- /dev/null +++ b/data/Espresso.txt @@ -0,0 +1,12 @@ +E | 0 | meet tp | 12-12-2025 | 12-12-2026 +T | 0 | ip +D | 0 | ip | 16-09-2024 +T | 0 | dance +T | 0 | recess week +T | 0 | task +T | 0 | task +T | 0 | task +T | 0 | task +T | 0 | homework +D | 0 | submit report | 21-09-2024 +T | 0 | task diff --git a/docs/README.md b/docs/README.md index 47b9f984f7..cb4bd54aab 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,83 @@ -# Duke User Guide +# Espresso User Guide -// Update the title above to match the actual product name +Welcome to Espresso User Guide. Espresso is a task management application which makes handling your tasks easier.With its straightforward commands and handy graphical user interface, users can add, delete, mark, unmark, and search for tasks easily. Espresso helps you manage deadlines, events, and to-do items, ensuring that you stay productive and organised. +![Product Screenshot](Ui.png) -// Product screenshot goes here +## List of Features -// Product intro goes here +1. **Add Tasks** +Espresso allows user to add three types of tasks : _Todo, Deadline and Event_ +For example : +``` +todo team project +deadline submission /by 20-09-2024 +event recess /from 23-09-2024 /to 30-09-2024 +``` -## Adding deadlines +2. **Delete Tasks** +Espresso also allows users to remove tasks from their list. +For example : +``` +delete 2 +``` -// Describe the action and its outcome. +3. **Mark Tasks as Done** +Users can mark tasks as done using the respective task number. +For example : +``` +mark 1 +``` -// Give examples of usage +4. **Unmark Tasks** +Users can unmark tasks using the respective task number. +For example : +``` +unmark 1 +``` -Example: `keyword (optional arguments)` +5. **List Tasks** +Users can also view their task list with the task types, their statuses and dates. +For example : +``` +list +``` -// A description of the expected outcome goes here +6. **Find Tasks** +Users can search for tasks using a specific phrase or keyword. +For example : +``` +find meet +``` + +7. **Exiting the application** +Users can exit the application with the `bye` command. ``` -expected output +bye ``` -## Feature ABC +## Exception Handling +Espresso is designed with a strong error-handling system to ensure smooth operation. When any issues arise during task processing, Espresso handles them gracefully, providing clear and helpful error messages. + +### Common Type of Errors + +1. **Invalid Command** +If a user enters a command that Espresso does not recognize, it will notify with an error message, helping to correct the command. -// Feature details +2. **File Parsing Issues** +In case Espresso encounters issues in accessing files, it will print an error message. +## Saving Functionality +Espresso saves all tasks after you exit the application for future reference. + +### Here is what your task list might look like + +``` +[D][ ] Finish project (by: 20 Sep 2024) +``` -## Feature XYZ +## Conflict Functionality +Espresso is equipped to detect if a task being added clashes with another task in the list and inform the user of the same. -// Feature details \ No newline at end of file +## Exiting Espresso Bot +In order to exit the bot and save your changes just make use of the `bye` command. diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..0011aa8fc0 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..033e24c4cd 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..66c01cfeba --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..fcb6fca147 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/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/HEAD/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 + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + 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 + + +# 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"' + +# 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 \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# 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..6689b85bee --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@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=. +@rem This is normally unused +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% equ 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% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java/Espresso.class b/java/Espresso.class new file mode 100644 index 0000000000..ca8442c2f7 Binary files /dev/null and b/java/Espresso.class differ diff --git a/java/Task.class b/java/Task.class new file mode 100644 index 0000000000..2627090212 Binary files /dev/null and b/java/Task.class differ diff --git a/java/deadlineTask.class b/java/deadlineTask.class new file mode 100644 index 0000000000..c5ca77e0e5 Binary files /dev/null and b/java/deadlineTask.class differ diff --git a/java/eventTask.class b/java/eventTask.class new file mode 100644 index 0000000000..7f82ea75e0 Binary files /dev/null and b/java/eventTask.class differ diff --git a/java/todoTask.class b/java/todoTask.class new file mode 100644 index 0000000000..360ec2306e Binary files /dev/null and b/java/todoTask.class differ 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/TaskType.java b/src/main/java/TaskType.java new file mode 100644 index 0000000000..99c8b7a8ab --- /dev/null +++ b/src/main/java/TaskType.java @@ -0,0 +1,3 @@ +public enum TaskType { + TODO, DEADLINE, EVENT; +} diff --git a/src/main/java/espresso/Espresso.java b/src/main/java/espresso/Espresso.java new file mode 100644 index 0000000000..1a40aa297c --- /dev/null +++ b/src/main/java/espresso/Espresso.java @@ -0,0 +1,61 @@ +//Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission +package espresso; + +import espresso.task.TaskList; +import espresso.storage.Storage; +import espresso.parser.Parser; +import espresso.command.InvalidCommandException; +import espresso.ui.Ui; + +import java.io.IOException; +import java.text.ParseException; + +/** + * Represents the main class for the Espresso chatbot. + * This class initializes the user interface, storage, and task list, and manages + * the application's command input and output. + */ +public class Espresso { + + private Ui ui; + private Storage storage; + private TaskList taskList; + + /** + * , + * The main handler of the chatbot. + * + * @throws InvalidCommandException if an invalid command is entered + * @throws ParseException if a task's date is in an invalid format + */ + public Espresso() throws InvalidCommandException, ParseException { + ui = new Ui(); + storage = new Storage("./data/Espresso.txt"); + taskList = new TaskList(); + try { + taskList = new TaskList(storage.load()); + } catch (IOException e) { + ui.printError("An error occurred while reading data from file: " + e.getMessage()); + } catch (ParseException e) { + ui.printError("An error occurred while parsing the date file: " + e.getMessage()); + } catch (InvalidCommandException e) { + ui.printError("Invalid command: " + e.getMessage()); + } + } + + public String getResponse(String input) throws ParseException { + if (input.equals("bye")) { + try { + storage.save(taskList.getTasks()); + } catch (IOException e) { + return ui.printError("An error occurred while saving the data file: " + e.getMessage()); + } + return "It was nice talking to you!" + "\n" + "Until next time...."; + } + try { + return Parser.parse(input, taskList, ui); + } catch (InvalidCommandException e) { + return ui.printError(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/espresso/Launcher.java b/src/main/java/espresso/Launcher.java new file mode 100644 index 0000000000..c71b9212a9 --- /dev/null +++ b/src/main/java/espresso/Launcher.java @@ -0,0 +1,10 @@ +package espresso; + +import espresso.gui.Main; + +public class Launcher { + + public static void main(String[] args) { + Main.main(args); + } +} \ No newline at end of file diff --git a/src/main/java/espresso/command/InvalidCommandException.java b/src/main/java/espresso/command/InvalidCommandException.java new file mode 100644 index 0000000000..5776846101 --- /dev/null +++ b/src/main/java/espresso/command/InvalidCommandException.java @@ -0,0 +1,7 @@ +package espresso.command; + +public class InvalidCommandException extends Exception { + public InvalidCommandException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/espresso/gui/DialogBox.java b/src/main/java/espresso/gui/DialogBox.java new file mode 100644 index 0000000000..b067e67b26 --- /dev/null +++ b/src/main/java/espresso/gui/DialogBox.java @@ -0,0 +1,92 @@ +//Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission +package espresso.gui; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.layout.HBox; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.control.Label; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +public class DialogBox extends HBox { + + private Label message; + private ImageView avatar; + + /** + * Private constructor to create a DialogBox. + * Initializes the message and avatar, sets up the layout, and configures text and image properties. + * + * @param message The message text to be displayed. + * @param avatar The ImageView containing the avatar to be displayed. + */ + //Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission + private DialogBox(String message, ImageView avatar, Boolean bot) { + this.message = new Label(message); + this.avatar = avatar; + + // Set text wrapping and maximum width to control layout + this.message.setWrapText(true); + this.message.setMaxWidth(280); + this.message.setPadding(new Insets(5)); + + // Adjust the avatar dimensions + this.avatar.setFitHeight(50.0); + this.avatar.setFitWidth(50.0); + + // Create a container for the message and display picture + VBox messageContainer = new VBox(this.message); + messageContainer.setMaxWidth(300); + VBox.setVgrow(messageContainer, Priority.ALWAYS); + + this.setAlignment(Pos.TOP_RIGHT); + if (bot) { + getChildren().addAll(avatar, messageContainer); + } else { + getChildren().addAll(messageContainer, avatar); + } + } + + /** + * Creates a DialogBox representing the user. + * Configures the avatar for the user and aligns the dialog to the right. + * + * @param message The message text to be displayed in the user dialog. + * @return A DialogBox containing the user message and avatar. + */ + //Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission + public static DialogBox createUserDialog(String message) { + ImageView userAvatar = new ImageView(new Image(DialogBox.class.getResourceAsStream("/images/EspressoUser.png"))); + DialogBox userDialog = new DialogBox(message, userAvatar, false); + // Set margin + HBox.setMargin(userDialog.avatar, new Insets(0, 0, 0, 3)); + HBox.setMargin(userDialog.message, new Insets(0, 3, 0, 0)); + userDialog.setMinHeight(Region.USE_PREF_SIZE); + + userDialog.setAlignment(Pos.CENTER_RIGHT); // Align user dialog to the CENTER RIGHT + return userDialog; + } + + /** + * Creates a DialogBox representing the bot. + * Configures the avatar for the bot and aligns the dialog to the left. + * @param message The message text to be displayed in the bot dialog. + * @return A DialogBox containing the bot message and avatar. + */ + //Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission + public static DialogBox createBotDialog(String message) { + ImageView botAvatar = new ImageView(new Image(DialogBox.class.getResourceAsStream("/images/Espresso.png"))); + DialogBox botDialog = new DialogBox(message, botAvatar, true); + + // Set margin + HBox.setMargin(botDialog.avatar, new Insets(0, 0, 0, 4)); + HBox.setMargin(botDialog.message, new Insets(0, 0, 0, 4)); + botDialog.setMinHeight(Region.USE_PREF_SIZE); + + botDialog.setAlignment(Pos.TOP_LEFT); // Align bot dialog to the center left + return botDialog; + } +} \ No newline at end of file diff --git a/src/main/java/espresso/gui/Main.java b/src/main/java/espresso/gui/Main.java new file mode 100644 index 0000000000..953a1ada40 --- /dev/null +++ b/src/main/java/espresso/gui/Main.java @@ -0,0 +1,40 @@ +package espresso.gui; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +public class Main extends Application { + + /** + * Loads the FXML layout, sets up the primary stage (window), and shows it. + * + * @param primaryStage The main window of the JavaFX application. + */ + @Override + public void start(Stage primaryStage) { + try { + // Load the FXML file + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/view/MainWindow.fxml")); + AnchorPane mainLayout = fxmlLoader.load(); + Scene scene = new Scene(mainLayout); + primaryStage.setTitle("Espresso"); + primaryStage.setScene(scene); + MainWindow controller = fxmlLoader.getController(); + primaryStage.show(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * The main method that launches the application. + * + * @param args Command-line arguments passed to the program. + */ + public static void main(String[] args) { + launch(args); + } +} \ No newline at end of file diff --git a/src/main/java/espresso/gui/MainWindow.java b/src/main/java/espresso/gui/MainWindow.java new file mode 100644 index 0000000000..2e1a0ddf21 --- /dev/null +++ b/src/main/java/espresso/gui/MainWindow.java @@ -0,0 +1,74 @@ +package espresso.gui; + +import espresso.command.InvalidCommandException; +import espresso.Espresso; +import javafx.animation.PauseTransition; +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.layout.VBox; +import java.text.ParseException; +import javafx.util.Duration; + +public class MainWindow { + + @FXML + private ScrollPane scrollPane; + + @FXML + private VBox dialogBoxContainer; + + @FXML + private TextField userInputMessage; + + @FXML + private Button sendButton; + + private Espresso espresso; + + /** + * Initializes the controller class when the FXML layout is loaded. + * + * @throws InvalidCommandException if a command in Espresso is invalid + * @throws ParseException if there is an error parsing user input + */ + @FXML + public void initialize() throws InvalidCommandException, ParseException { + espresso = new Espresso(); + assert espresso != null : "Espresso instance should be initialized"; + scrollPane.setFitToWidth(true); + + dialogBoxContainer.setPrefWidth(scrollPane.getWidth()); + + scrollPane.widthProperty().addListener((observable, oldVal, newVal) -> { + dialogBoxContainer.setPrefWidth(newVal.doubleValue()); + }); + } + //Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission + + @FXML + private void handleUserInput() throws ParseException { + String input = userInputMessage.getText(); + assert input != null : "User input should not be null"; + if (!input.isEmpty()) { + String response = espresso.getResponse(input); + assert response != null : "Response from espresso should not be null"; + dialogBoxContainer.getChildren().addAll( + DialogBox.createUserDialog(input), + DialogBox.createBotDialog(response) + ); + userInputMessage.clear(); + + scrollPane.vvalueProperty().unbind(); + Platform.runLater(() -> scrollPane.setVvalue(1.0)); + + if (input.equalsIgnoreCase("bye")) { + PauseTransition delay = new PauseTransition(Duration.seconds(2)); + delay.setOnFinished(event -> Platform.exit()); + delay.play(); + } + } + } +} diff --git a/src/main/java/espresso/parser/Parser.java b/src/main/java/espresso/parser/Parser.java new file mode 100644 index 0000000000..a81111710c --- /dev/null +++ b/src/main/java/espresso/parser/Parser.java @@ -0,0 +1,74 @@ +package espresso.parser; + +import espresso.command.InvalidCommandException; +import espresso.task.TaskList; +import espresso.task.TodoTask; +import espresso.task.DeadlineTask; +import espresso.task.EventTask; +import espresso.ui.Ui; +import java.text.ParseException; + +/** + * This class is responsible for parsing user inputs and executing the appropriate. + * commands on the task list. The class interprets the input and is responsible for performing. + * tasks such as adding and removing tasks as well as marking and unmarking tasks. + */ +public class Parser { + + /** + * Parses the user's input and executes the corresponding actions + * + * @param input The string input by user containing the command. + * @param taskList The current task list where tasks are being stored. + * @param ui The UI object displayed to interact with the user. + * @throws InvalidCommandException If the command format is invalid. + * @throws ParseException If there is an error in parsing task data. + */ + //Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission + public static String parse(String input, TaskList taskList, Ui ui) throws InvalidCommandException, ParseException { + if (input.equals("list")) { + return ui.printTasks(taskList); + } else if (checkInput(input, "mark ")) { + int i = Integer.parseInt(input.substring(5)) - 1; + taskList.getTask(i).mark(); + return ui.printTaskMarked(taskList.getTask(i)); + } else if (checkInput(input, "unmark ")) { + int i = Integer.parseInt(input.substring(7)) - 1; + taskList.getTask(i).unmark(); + return ui.printTaskUnmarked(taskList.getTask(i)); + } else if (checkInput(input, "todo ")) { + return ui.printTaskAdded(taskList.addTask(new TodoTask(input.substring(5))), "todo task"); + } else if (checkInput(input, "deadline ")) { + String[] split = input.substring(9).split(" /by "); + if (split.length != 2) { + throw new InvalidCommandException("Invalid deadline format."); + } + return ui.printTaskAdded(taskList.addTaskWithAnomalyCheck(new DeadlineTask(split[0], split[1])), "deadline"); + } else if (checkInput(input, "event ")) { + String[] split = input.substring(6).split(" /from | /to "); + if (split.length != 3) { + throw new InvalidCommandException("Invalid event format."); + } + return ui.printTaskAdded(taskList.addTaskWithAnomalyCheck(new EventTask(split[0], split[1], split[2])), "event"); + } else if (checkInput(input, "delete ")) { + int i = Integer.parseInt(input.substring(7)) - 1; + String res = ui.printTaskRemoved(taskList.getTask(i)); + taskList.removeTask(i); + return res; + } else if (checkInput(input, "find ")) { + String[] split = input.split(" "); + if (split.length != 2) { + throw new InvalidCommandException("Invalid search term."); + } + String searchTerm = split[1]; + TaskList foundTasks = taskList.find(searchTerm); + return ui.printFoundTasks(foundTasks); + } else { + return ui.printError("Command not found."); + } + } + + private static boolean checkInput(final String a, final String b) { + return a.length() >= b.length() && a.substring(0, b.length()).equals(b); + } +} diff --git a/src/main/java/espresso/storage/Storage.java b/src/main/java/espresso/storage/Storage.java new file mode 100644 index 0000000000..13505643b1 --- /dev/null +++ b/src/main/java/espresso/storage/Storage.java @@ -0,0 +1,89 @@ +package espresso.storage; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Scanner; +import espresso.task.Task; +import espresso.task.TodoTask; +import espresso.task.DeadlineTask; +import espresso.task.EventTask; +import espresso.command.InvalidCommandException; + +/** + * This class is responsible for Handling the loading and saving of tasks to a file + * It also Manages the interaction between task data and the file system by creating + * objects of the correct Task Type. + */ + +public class Storage { + private File file; + + /** + * Constructor for Storage. + * Initializes a new file with the given file path. + * + * @param filePath The file path where tasks will be stored. + */ + public Storage(String filePath) { + this.file = new File(filePath); + } + + /** + * Loads the tasks from the file into an ArrayList. + * It creates a new file in case it doesn't already exist. + * + * @return An ArrayList of tasks loaded from the file. + * @throws IOException If there is an error accessing the file. + * @throws ParseException If the file content is in an invalid format. + * @throws InvalidCommandException If an invalid task type is encountered in the file. + */ + //Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission + public ArrayList load() throws IOException, ParseException, InvalidCommandException { + ArrayList tasks = new ArrayList<>(); + + if (!file.exists()) { + file.getParentFile().mkdirs(); + file.createNewFile(); + return tasks; + } + + Scanner fileScanner = new Scanner(file); + while (fileScanner.hasNextLine()) { + String line = fileScanner.nextLine(); + String[] split = line.split(" \\| "); + + switch (split[0]) { + case "T": + tasks.add(new TodoTask(split[2])); + break; + case "D": + tasks.add(new DeadlineTask(split[2], split[3])); + break; + case "E": + tasks.add(new EventTask(split[2], split[3], split[4])); + break; + default: + throw new InvalidCommandException("Invalid task type: " + split[0]); + } + } + fileScanner.close(); + return tasks; + } + + /** + * Saves the tasks from an ArrayList to the file. + * + * @param tasks The ArrayList of tasks to be saved to the file. + * @throws IOException If there is an error writing to the file. + */ + public void save(ArrayList tasks) throws IOException { + PrintWriter printWriter = new PrintWriter(file); + for (Task task : tasks) { + printWriter.println(task.toText()); + } + printWriter.close(); + } +} \ No newline at end of file diff --git a/src/main/java/espresso/task/DeadlineTask.java b/src/main/java/espresso/task/DeadlineTask.java new file mode 100644 index 0000000000..a9e41ea80c --- /dev/null +++ b/src/main/java/espresso/task/DeadlineTask.java @@ -0,0 +1,35 @@ +//Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission +package espresso.task; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Represents a task which has a deadline. + * Extends the {@link Task} class to include a deadline date. + */ +public class DeadlineTask extends Task { + private Date dl; + private static final SimpleDateFormat inputFormat = new SimpleDateFormat("dd-MM-yyyy"); + private static final SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMM yyyy"); + + public DeadlineTask(String description, String dl) throws ParseException { + super(description); + this.dl = inputFormat.parse(dl); + } + + public Date getDeadline() { + return this.dl; + } + + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + outputFormat.format(dl) + ")"; + } + + @Override + public String toText() { + return "D | " + super.toText() + " | " + inputFormat.format(dl); + } +} \ No newline at end of file diff --git a/src/main/java/espresso/task/EventTask.java b/src/main/java/espresso/task/EventTask.java new file mode 100644 index 0000000000..5b64034946 --- /dev/null +++ b/src/main/java/espresso/task/EventTask.java @@ -0,0 +1,44 @@ +//Solution below inspired by https://github.com/nus-cs2103-AY2425S1/ip/pull/557 with permission +package espresso.task; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import espresso.command.InvalidCommandException; + +/** + * Represents a task which is an event. + * Extends the {@link Task} class to include an event date. + */ +public class EventTask extends Task { + private Date starts; + private Date ends; + private static final SimpleDateFormat inputFormat = new SimpleDateFormat("dd-MM-yyyy"); + private static final SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM-yyyy"); + + public EventTask(String description, String starts, String ends) throws ParseException, InvalidCommandException { + super(description); + this.starts = inputFormat.parse(starts); + this.ends = inputFormat.parse(ends); + + if (this.starts.after(this.ends)) { + throw new InvalidCommandException("Invalid event time."); + } + } + public Date getStarts() { + return this.starts; + } + public Date getEnds() { + return this.ends; + } + + @Override + public String toString() { + return "[E]" + super.toString() + " (from: " + outputFormat.format(starts) + " to: " + outputFormat.format(ends) + ")"; + } + + @Override + public String toText() { + return "E | " + super.toText() + " | " + inputFormat.format(starts) + " | " + inputFormat.format(ends); + } +} \ No newline at end of file diff --git a/src/main/java/espresso/task/Task.java b/src/main/java/espresso/task/Task.java new file mode 100644 index 0000000000..b4cc71d534 --- /dev/null +++ b/src/main/java/espresso/task/Task.java @@ -0,0 +1,76 @@ +package espresso.task; + +/** + * Represents a generic task with a description and a status indicating whether the task + * is completed or not. + */ +public class Task { + protected String description; + protected boolean isDone; + + /** + * Constructs a new Task with the specified description. + * Task is initially unmarked. + * + * @param description The description of the task. + */ + public Task(String description) { + this.description = description; + this.isDone = false; + } + + /** + * Checks if the task description contains the specified substring. + * + * @param substring The substring to search for in the task description. + * @return True if the description contains the substring, otherwise false. + */ + public boolean contains(String substring) { + return description.contains(substring); + } + + /** + * Returns the status icon of the task. "X" if the task is marked done, + * and an empty space otherwise. + * + * @return The status icon representing whether the task is done. + */ + public String getStatusIcon() { + return (isDone ? "X" : " "); + } + + /** + * Marks the task as done. + */ + public void mark() { + this.isDone = true; + } + + /** + * Unmarks the task. + */ + public void unmark() { + this.isDone = false; + } + + /** + * Returns a string representation of the task, including its status and description. + * + * @return A string representing the task. + */ + @Override + public String toString() { + return "[" + getStatusIcon() + "] " + description; + } + + /** + * Returns a string representation of the task formatted for saving to a text file. + * The format includes whether the task is done (1 for done, 0 for not done) + * and the description of the task too. + * + * @return A string representing the task for saving to a text file. + */ + public String toText() { + return (isDone ? "1" : "0") + " | " + description; + } +} \ No newline at end of file diff --git a/src/main/java/espresso/task/TaskList.java b/src/main/java/espresso/task/TaskList.java new file mode 100644 index 0000000000..eb1c7c17c1 --- /dev/null +++ b/src/main/java/espresso/task/TaskList.java @@ -0,0 +1,170 @@ +package espresso.task; +import java.util.ArrayList; +import espresso.command.InvalidCommandException; + +/** + * Represents a list of tasks. + * It Manages adding, removing, retrieving tasks, and provides other helpful methods + * that can help make use of the task list. + */ +public class TaskList { + private ArrayList tasks; + + /** + * Constructs an empty TaskList. + */ + public TaskList() { + this.tasks = new ArrayList<>(); + } + + /** + * Constructs a TaskList with an existing list of tasks. + * + * @param tasks The list of tasks to initialize the TaskList with. + */ + public TaskList(ArrayList tasks) { + this.tasks = tasks; + } + + /** + * Adds a task to the TaskList. + * + * @param task The task to be added. + * @return The added task. + */ + public Task addTask(Task task) { + tasks.add(task); + return task; + } + + /** + * Removes a task from the TaskList by using provided index. + * + * @param index The index of the task to be removed. + * @throws InvalidCommandException If the index is out of range. + */ + public void removeTask(int index) throws InvalidCommandException{ + if (index >= 0 && index < tasks.size()) { + tasks.remove(index); + } else { + throw new InvalidCommandException("Invalid task index."); + } + } + + /** + * Retrieves a task from the TaskList by using provided index. + * + * @param index The index of the task to be retrieved. + * @return The task at the specified index. + * @throws InvalidCommandException If the index is out of range. + */ + public Task getTask(int index) throws InvalidCommandException { + if (index >= 0 && index < tasks.size()) { + return tasks.get(index); + } else { + throw new InvalidCommandException("Invalid task index."); + } + } + + /** + * Returns the number of tasks in the TaskList. + * + * @return number of tasks + */ + public int size() { + return tasks.size(); + } + + /** + * Returns the list of tasks in the TaskList. + * + * @return An ArrayList containing all tasks. + */ + public ArrayList getTasks() { + return tasks; + } + + /* Finds all tasks that contain the specified substring. + * + * @param substring the substring to search for. + * @return the list of tasks that contain the substring. + */ + public TaskList find(String str) { + ArrayList matchingTasks = new ArrayList<>(); + + for (Task task : tasks) { + if (task.contains(str)) { + matchingTasks.add(task); + } + } + TaskList res = new TaskList(matchingTasks); + return res; + } + + public boolean detectAnomaly(Task newTask) { + for (Task task : tasks) { + if (task instanceof EventTask && newTask instanceof EventTask) { + EventTask existingTask = (EventTask) task; + EventTask eventTask = (EventTask) newTask; + + // Check for overlapping event times + if (eventTask.getEnds().after(existingTask.getStarts()) && eventTask.getStarts().before(existingTask.getEnds())) { + return true; // Conflict found + } + + } else if (task instanceof DeadlineTask && newTask instanceof DeadlineTask) { + DeadlineTask existingTask = (DeadlineTask) task; + DeadlineTask deadlineTask = (DeadlineTask) newTask; + + // Check if two DeadlineTasks have the same date + if (existingTask.getDeadline().equals(deadlineTask.getDeadline())) { + return true; // Conflict found + } + + } else if (task instanceof EventTask && newTask instanceof DeadlineTask) { + EventTask eventTask = (EventTask) task; + DeadlineTask deadlineTask = (DeadlineTask) newTask; + + // Check if the deadline falls within the event's time range + if (deadlineTask.getDeadline().after(eventTask.getStarts()) && deadlineTask.getDeadline().before(eventTask.getEnds())) { + return true; // Conflict found + } + + } else if (task instanceof DeadlineTask && newTask instanceof EventTask) { + DeadlineTask deadlineTask = (DeadlineTask) task; + EventTask eventTask = (EventTask) newTask; + + // Check if the deadline falls within the event's time range + if (deadlineTask.getDeadline().after(eventTask.getStarts()) && deadlineTask.getDeadline().before(eventTask.getEnds())) { + return true; // Conflict found + } + } + } + return false; // No conflict + } + + public Task addTaskWithAnomalyCheck(Task task) throws InvalidCommandException { + if (detectAnomaly(task)) { + throw new InvalidCommandException("Scheduling conflict detected."); + } + return addTask(task); + } + + /** + * Returns a string representation of the TaskList, where each task is preceded + * by its position in the list. + * + * @return A string representation of the TaskList. + */ + @Override + public String toString() { + String res = ""; + for (int i = 0; i < tasks.size(); i++) { + res += i + 1 + "." + tasks.get(i); + if (i < tasks.size() - 1) { + res += "\n"; + } + } + return res; + } +} \ No newline at end of file diff --git a/src/main/java/espresso/task/TodoTask.java b/src/main/java/espresso/task/TodoTask.java new file mode 100644 index 0000000000..75b3a58e1e --- /dev/null +++ b/src/main/java/espresso/task/TodoTask.java @@ -0,0 +1,20 @@ +package espresso.task; + +/** + * Represents a task which is a todo task. + */ +public class TodoTask extends Task { + public TodoTask(String description) { + super(description); + } + + @Override + public String toString() { + return "[T]" + super.toString(); + } + + @Override + public String toText() { + return "T | " + super.toText(); + } +} \ No newline at end of file diff --git a/src/main/java/espresso/ui/Ui.java b/src/main/java/espresso/ui/Ui.java new file mode 100644 index 0000000000..bc9451758e --- /dev/null +++ b/src/main/java/espresso/ui/Ui.java @@ -0,0 +1,83 @@ +package espresso.ui; +import espresso.task.TaskList; +import espresso.task.Task; +import espresso.command.InvalidCommandException; + +public class Ui { + + public String printLine() { + return "__________________________________\n"; + } + + public String printWelcome() { + + String res = printLine() + " ____ \n" + + " / __/__ ___ _______ ___ ___ ___ \n" + + " / _/(_- + + + + + + + + +