diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000000..fbe40591c9 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,34 @@ +name: Java CI + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + + steps: + - name: Set up repository + uses: actions/checkout@master + + - name: Set up repository + uses: actions/checkout@master + with: + ref: master + + - name: Merge to master + run: git checkout --progress --force ${{ github.sha }} + + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Setup JDK 17 + uses: actions/setup-java@v1 + with: + java-version: '17' + java-package: jdk+fx + + - name: Build and check with Gradle + run: ./gradlew check diff --git a/.gitignore b/.gitignore index 2873e189e1..b8c129b663 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Gradle build files /.gradle/ /build/ +*.jar src/main/resources/docs/ # MacOS custom attributes files created by Finder @@ -13,5 +14,7 @@ src/main/resources/docs/ *.iml bin/ -/text-ui-test/ACTUAL.TXT -text-ui-test/EXPECTED-UNIX.TXT +# Other +*.class +data/ +/text-ui-test/**/ACTUAL*.TXT diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..d44833ee31 --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +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' + + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.2' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.15.2' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClass.set("rover.main.Launcher") +} + +shadowJar { + archiveFileName = "Rover.jar" +} + +checkstyle { + toolVersion = "10.2" +} + +run{ + standardInput = System.in +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..acac1a8e28 --- /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 47b9f984f7..d2e3601063 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,174 @@ -# Duke User Guide +# Rover User Guide -// Update the title above to match the actual product name +Rover is a simple desktop app that act as a command-based to-do list manager with an intuitive interface for adding, listing, and managing tasks efficiently. -// Product screenshot goes here +## Quick Start +1. Ensure you have Java 17 or above installed in your Computer. +**Mac users**: Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html). -// Product intro goes here +2. Download the latest Rover.jar file from [here](https://github.com/kumar2215/ip/releases/download/A-Release/Rover.jar). -## Adding deadlines +3. Copy the file to the folder you want to use as the home folder for Rover. -// Describe the action and its outcome. +4. Open a command terminal, cd into the folder you put the jar file in, and use the java -jar Rover.jar command to run the application. +A GUI similar to the below should appear in a few seconds. -// Give examples of usage +![Rover Todo App Screenshot](Ui.png) -Example: `keyword (optional arguments)` +## Features -// A description of the expected outcome goes here +### Adding Deadlines +You can add a deadline task by using the following command format: + +**Example:** +```plaintext +deadline Finish report /by 21-02-25 23:59 +``` + +**Expected Outcome:** +- A new deadline task is added with the specified due date. +- The task will be displayed in the task list. + +### Adding To-Do Tasks + +To add a simple to-do task without deadlines, use: + +**Example:** +```plaintext +todo Buy groceries +``` + +**Expected Outcome:** +- The task is added without any time constraints. + +### Adding Events + +You can add an event by specifying a start and end time: + +**Example:** +```plaintext +event Team meeting /from 20-02-25 10:00 /to 20-02-25 12:00 +``` + +**Expected Outcome:** +- An event task is created with a specified time range. + +### Listing Tasks + +To list all existing tasks, use: + +```plaintext +list +``` + +**Expected Outcome:** +- All saved tasks will be displayed with their statuses. + +## Managing Tasks + +### Marking a Task as Done + +**Example:** +```plaintext +mark 1 ``` -expected output + +**Expected Outcome:** +- Marks the task at index 1 in the list as done. + +### Marking a Task as Not Done + +**Example:** +```plaintext +unmark 1 +``` + +**Expected Outcome:** +- Marks the task at index 1 in the list as not done. + +### Deleting a Task + +**Example:** +```plaintext +delete 1 ``` -## Feature ABC +### Expected Outcome: +- Deletes the task at index 1 in the list. -// Feature details +## Filtering Tasks +### Find Tasks by Keyword -## Feature XYZ +**Example:** +```plaintext +find report +``` + +**Expected Outcome:** +- Displays all tasks containing the keyword "report". + +### Show Tasks Before a Certain Date + +**Example:** +```plaintext +show before 01-03-25 +``` + +**Expected Outcome:** +- Displays all tasks with deadlines before the specified date. + +### Show Tasks After a Certain Date + +**Example:** +```plaintext +show after 15-02-25 +``` + +**Expected Outcome:** +- Displays all tasks with deadlines after the specified date. + +## Setting User Preferences + +### Set Your Name + +**Example:** +```plaintext +set name John +``` + +**Expected Outcome:** +- Your name will be updated. + +### Set User Image + +**Example:** +```plaintext +set userImage path/to/image.png (relative to the current directory) +``` + +**Expected Outcome:** +- Your profile picture used in the chat will be updated. + +### Set Rover's Image + +**Example:** +```plaintext +set roverImage path/to/rover.png (relative to the current directory) +``` + +**Expected Outcome:** +- Rover's profile picture used in the chat will be updated. + +## Exiting the Application + +To exit the program, use: + +**Example:** +```plaintext +bye +``` -// Feature details \ No newline at end of file +**Expected Outcome:** +- The program closes safely. \ No newline at end of file diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..6e9d96b8a3 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/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/rover/command/AddCommand.java b/src/main/java/rover/command/AddCommand.java new file mode 100644 index 0000000000..8671729893 --- /dev/null +++ b/src/main/java/rover/command/AddCommand.java @@ -0,0 +1,43 @@ +package rover.command; +import java.time.format.DateTimeParseException; + +import rover.exceptions.RoverException; +import rover.parser.Parser; +import rover.task.Task; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command that adds a task to the task list. + */ +public final class AddCommand extends Command { + + /** + * Constructs an add command with the given arguments. + * + * @param args The arguments of the command. + */ + public AddCommand(String args) { + super(args); + } + + /** + * Executes the add command to add a task to the task list. + */ + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) { + try { + Task newTask = parser.parseTaskDescription(args); + taskList.addTask(newTask, ui); + } catch (RoverException e) { + ui.displayError(e.getMessage()); + } catch (DateTimeParseException e) { + if (e.getMessage().contains("date")) { + ui.displayError("The date format should be 'dd/mm/yy'."); + } + if (e.getMessage().contains("time")) { + ui.displayError("The time format should be 'hh:mm'."); + } + } + } +} diff --git a/src/main/java/rover/command/Command.java b/src/main/java/rover/command/Command.java new file mode 100644 index 0000000000..ddfcdaa19c --- /dev/null +++ b/src/main/java/rover/command/Command.java @@ -0,0 +1,62 @@ +package rover.command; + +import rover.parser.Parser; +import rover.storage.Storage; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command that can be executed by the user. + */ +public abstract class Command { + + protected final String args; + + /** + * Constructs a command with the given arguments. + * + * @param args The arguments of the command. + */ + public Command(String args) { + this.args = args; + } + + /** + * Executes the command. + * + * @param taskList The task list to execute the command on. + * @param parser The parser to parse the command. + * @param ui The user interface to interact with the user. + */ + public abstract void execute(TaskList taskList, Parser parser, Ui ui); + + /** + * Executes the command. + * + * @param taskList The task list to execute the command on. + * @param storage The storage to save the task list. + * @param ui The user interface to interact with the user. + */ + public void execute(TaskList taskList, Storage storage, Ui ui) {} + + /** + * Returns true if the command is an exit command and is false by default. + */ + public boolean isExit() { + return false; + } + + /** + * Checks equality of commands based on their arguments. + * Returns true if the argument of the command is equal to the argument of the other command. + * + * @param obj The object to compare with. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof Command other) { + return this.args.equals(other.args); + } + return false; + } +} diff --git a/src/main/java/rover/command/DeleteCommand.java b/src/main/java/rover/command/DeleteCommand.java new file mode 100644 index 0000000000..1dadc70d80 --- /dev/null +++ b/src/main/java/rover/command/DeleteCommand.java @@ -0,0 +1,35 @@ +package rover.command; + +import rover.exceptions.RoverException; +import rover.parser.Parser; +import rover.task.TaskAction; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command to delete a task from the task list. + */ +public final class DeleteCommand extends Command { + + /** + * Constructs a DeleteCommand object. + * + * @param args The arguments to the command. + */ + public DeleteCommand(String args) { + super(args.substring(6).trim()); + } + + /** + * Executes the command to delete a task from the task list. + */ + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) { + try { + int index = parser.parseTaskNumber(args, taskList.getNumberOfTasks(), TaskAction.DELETE); + taskList.deleteTask(index, ui); + } catch (RoverException e) { + ui.displayError(e.getMessage()); + } + } +} diff --git a/src/main/java/rover/command/EmptyCommand.java b/src/main/java/rover/command/EmptyCommand.java new file mode 100644 index 0000000000..9d6b9cb543 --- /dev/null +++ b/src/main/java/rover/command/EmptyCommand.java @@ -0,0 +1,28 @@ +package rover.command; + +import rover.parser.Parser; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command that does nothing. + */ +public final class EmptyCommand extends Command { + + /** + * Constructs an EmptyCommand object. + * + * @param args The arguments to be passed to the command. + */ + public EmptyCommand(String args) { + super(args); + } + + /** + * Just displays an error message to the user. + */ + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) { + ui.displayError("Please enter a command."); + } +} diff --git a/src/main/java/rover/command/ExitCommand.java b/src/main/java/rover/command/ExitCommand.java new file mode 100644 index 0000000000..287c7b3ce0 --- /dev/null +++ b/src/main/java/rover/command/ExitCommand.java @@ -0,0 +1,34 @@ +package rover.command; + +import rover.parser.Parser; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command to exit the program. + */ +public final class ExitCommand extends Command { + + /** + * Constructs an ExitCommand object. + * + * @param args The arguments passed to the command. + */ + public ExitCommand(String args) { + super(args); + } + + /** + * Executes nothing as the program is exiting. + */ + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) {} + + /** + * Returns true as the command is an exit command. + */ + @Override + public boolean isExit() { + return true; + } +} diff --git a/src/main/java/rover/command/FindCommand.java b/src/main/java/rover/command/FindCommand.java new file mode 100644 index 0000000000..05609ff8be --- /dev/null +++ b/src/main/java/rover/command/FindCommand.java @@ -0,0 +1,32 @@ +package rover.command; + +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command to find tasks by keyword. + */ +public class FindCommand extends ShowCommand { + + /** + * Constructor for a FindCommand. + * + * @param args The arguments for the command. + */ + public FindCommand(String args) { + super(args.substring(4).trim()); + } + + /** + * Shows the tasks that contain the keyword. + */ + @Override + protected void show(TaskList taskList, Ui ui) { + if (args.isEmpty()) { + ui.displayError("The keyword to find cannot be empty."); + return; + } + taskList.showTasks(ui, (task, ignore) -> task.toString().contains(args), + "with the keyword '" + args + "'"); + } +} diff --git a/src/main/java/rover/command/InvalidCommand.java b/src/main/java/rover/command/InvalidCommand.java new file mode 100644 index 0000000000..59939bb11a --- /dev/null +++ b/src/main/java/rover/command/InvalidCommand.java @@ -0,0 +1,28 @@ +package rover.command; + +import rover.parser.Parser; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents an invalid command. + */ +public final class InvalidCommand extends Command { + + /** + * Constructs an InvalidCommand object. + * + * @param args The arguments of the command. + */ + public InvalidCommand(String args) { + super(args); + } + + /** + * Displays the help message to the user. + */ + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) { + ui.showHelpMessage(); + } +} diff --git a/src/main/java/rover/command/ListCommand.java b/src/main/java/rover/command/ListCommand.java new file mode 100644 index 0000000000..17d91c45b6 --- /dev/null +++ b/src/main/java/rover/command/ListCommand.java @@ -0,0 +1,28 @@ +package rover.command; + +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command to list all tasks in the task list. + */ +public final class ListCommand extends ShowCommand { + + /** + * Constructs a ListCommand object. + * + * @param args The arguments to be passed to the command. + */ + public ListCommand(String args) { + super(args); + } + + /** + * Executes the command to list all tasks in the task list. + */ + @Override + protected void show(TaskList taskList, Ui ui) { + taskList.showTasks(ui, (task, ignore) -> true, "in your list"); + } + +} diff --git a/src/main/java/rover/command/MarkCommand.java b/src/main/java/rover/command/MarkCommand.java new file mode 100644 index 0000000000..f176781e25 --- /dev/null +++ b/src/main/java/rover/command/MarkCommand.java @@ -0,0 +1,35 @@ +package rover.command; + +import rover.exceptions.RoverException; +import rover.parser.Parser; +import rover.task.TaskAction; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command to mark a task as done. + */ +public final class MarkCommand extends Command { + + /** + * Constructs a MarkCommand object. + * + * @param args The arguments to the command. + */ + public MarkCommand(String args) { + super(args.substring(4).trim()); + } + + /** + * Marks a task as done in the task list. + */ + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) { + try { + int index = parser.parseTaskNumber(args, taskList.getNumberOfTasks(), TaskAction.MARK_DONE); + taskList.markTask(index, ui); + } catch (RoverException e) { + ui.displayError(e.getMessage()); + } + } +} diff --git a/src/main/java/rover/command/RetrySaveCommand.java b/src/main/java/rover/command/RetrySaveCommand.java new file mode 100644 index 0000000000..c77b3551a8 --- /dev/null +++ b/src/main/java/rover/command/RetrySaveCommand.java @@ -0,0 +1,48 @@ +package rover.command; + +import rover.parser.Parser; +import rover.storage.Storage; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command to retry saving the task list. + */ +public final class RetrySaveCommand extends Command { + + /** + * Constructs a RetrySaveCommand. + * + * @param args The user input arguments. + */ + public RetrySaveCommand(String args) { + super(args); + } + + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) {} + + /** + * Retries saving the task list. + * + * @param taskList The task list. + * @param storage The storage. + * @param ui The user interface. + */ + @Override + public void execute(TaskList taskList, Storage storage, Ui ui) { + if (args.equals("y") || args.equals("yes")) { + storage.saveAll(taskList, ui.getUserPreferences(), ui); + } else { + ui.showMessage("Exiting without saving..."); + } + } + + /** + * Returns true if the user wants to exit the application. + */ + @Override + public boolean isExit() { + return args.equals("n") || args.equals("no"); + } +} diff --git a/src/main/java/rover/command/SetCommand.java b/src/main/java/rover/command/SetCommand.java new file mode 100644 index 0000000000..4996739974 --- /dev/null +++ b/src/main/java/rover/command/SetCommand.java @@ -0,0 +1,73 @@ +package rover.command; + +import java.nio.file.Paths; + +import rover.exceptions.RoverException; +import rover.parser.Parser; +import rover.preferences.PreferenceOption; +import rover.task.TaskList; +import rover.ui.Gui; +import rover.ui.TextUi; +import rover.ui.Ui; + +/** + * Represents a command to set the user preferences. + */ +public class SetCommand extends Command { + + /** + * Creates a new SetCommand with the given arguments. + * + * @param args The arguments for the command. + */ + public SetCommand(String args) { + super(args.substring(3).trim()); + } + + /** + * Executes the set command to set the given user preferences. + * + * @param taskList The task list to be operated on. + * @param parser The parser to parse the user input. + * @param ui The user interface to interact with the user. + */ + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) { + String[] preferences = args.split(" "); + if (preferences.length == 2) { + preferences[0] = preferences[0].trim(); + preferences[1] = preferences[1].trim(); + + PreferenceOption preferenceOption; + try { + preferenceOption = parser.parsePreferenceOption(preferences[0]); + } catch (RoverException e) { + ui.displayError("Invalid preference! Preference options can only be: name, userImage or roverImage " + + "and the value should be a valid file path if setting image."); + return; + } + + String optionName = String.join(" ", preferenceOption.name().split("_")).toLowerCase(); + optionName = optionName.substring(0, 1).toUpperCase() + optionName.substring(1); + if (ui instanceof TextUi && optionName.contains("image")) { + ui.displayError(String.format("%s cannot be set in text mode!", optionName)); + return; + } + + boolean isSuccessful = setOption(ui, preferenceOption, preferences[1]); + if (isSuccessful) { + ui.showMessage(String.format("%s successfully set to %s", optionName, preferences[1])); + } + } else { + ui.displayError("Invalid input! Please enter the command in the format: set [preference] [value]"); + } + } + + private boolean setOption(Ui ui, PreferenceOption preferenceOption, String value) { + return switch (preferenceOption) { + case NAME -> ui.setUsername(value); + case USER_IMAGE -> ((Gui) ui).setUserImage(Paths.get(value)); + case ROVER_IMAGE -> ((Gui) ui).setRoverImage(Paths.get(value)); + }; + } +} diff --git a/src/main/java/rover/command/ShowAfterCommand.java b/src/main/java/rover/command/ShowAfterCommand.java new file mode 100644 index 0000000000..36c6aa9fb9 --- /dev/null +++ b/src/main/java/rover/command/ShowAfterCommand.java @@ -0,0 +1,39 @@ +package rover.command; +import java.time.format.DateTimeParseException; + +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command to show tasks after a specified date. + */ +public final class ShowAfterCommand extends ShowCommand { + + /** + * Constructs a ShowAfterCommand. + * + * @param args The date to show tasks after. + */ + public ShowAfterCommand(String args) { + super(args.substring(10).trim()); + } + + /** + * Shows the tasks after the specified date and/or time. + */ + @Override + protected void show(TaskList taskList, Ui ui) { + taskList.showTasks(ui, (task, wasExceptionThrown) -> { + try { + if (wasExceptionThrown.get()) { + return false; + } + return task.isAfter(args); + } catch (DateTimeParseException e) { + ui.displayError("The date format should be 'dd/mm/yy' and the time format should be 'hh:mm'."); + wasExceptionThrown.set(true); + return false; + } + }, "after " + args); + } +} diff --git a/src/main/java/rover/command/ShowBeforeCommand.java b/src/main/java/rover/command/ShowBeforeCommand.java new file mode 100644 index 0000000000..0d3d1f9e0a --- /dev/null +++ b/src/main/java/rover/command/ShowBeforeCommand.java @@ -0,0 +1,39 @@ +package rover.command; +import java.time.format.DateTimeParseException; + +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command to show tasks before a specified date. + */ +public final class ShowBeforeCommand extends ShowCommand { + + /** + * Constructs a ShowBeforeCommand. + * + * @param args The arguments to the command. + */ + public ShowBeforeCommand(String args) { + super(args.substring(11).trim()); + } + + /** + * Shows the tasks before the specified date and/or time. + */ + @Override + protected void show(TaskList taskList, Ui ui) { + taskList.showTasks(ui, (task, wasExceptionThrown) -> { + try { + if (wasExceptionThrown.get()) { + return false; + } + return task.isBefore(args); + } catch (DateTimeParseException e) { + ui.displayError("The date format should be 'dd/mm/yy' and the time format should be 'hh:mm'."); + wasExceptionThrown.set(true); + return false; + } + }, "before " + args); + } +} diff --git a/src/main/java/rover/command/ShowCommand.java b/src/main/java/rover/command/ShowCommand.java new file mode 100644 index 0000000000..c49862eaba --- /dev/null +++ b/src/main/java/rover/command/ShowCommand.java @@ -0,0 +1,31 @@ +package rover.command; + +import rover.parser.Parser; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Abstract class for show commands. + */ +public abstract class ShowCommand extends Command { + + public ShowCommand(String args) { + super(args); + } + + /** + * Executes the show command. + */ + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) { + show(taskList, ui); + } + + /** + * Shows the task list. + * + * @param taskList The task list to show. + * @param ui The user interface to display the task list. + */ + protected abstract void show(TaskList taskList, Ui ui); +} diff --git a/src/main/java/rover/command/UnmarkCommand.java b/src/main/java/rover/command/UnmarkCommand.java new file mode 100644 index 0000000000..7ba8d9abb4 --- /dev/null +++ b/src/main/java/rover/command/UnmarkCommand.java @@ -0,0 +1,35 @@ +package rover.command; + +import rover.exceptions.RoverException; +import rover.parser.Parser; +import rover.task.TaskAction; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Represents a command to unmark a task as done. + */ +public final class UnmarkCommand extends Command { + + /** + * Constructs a new UnmarkCommand object. + * + * @param args The arguments to the command. + */ + public UnmarkCommand(String args) { + super(args.substring(6).trim()); + } + + /** + * Marks the task as not done in the task list. + */ + @Override + public void execute(TaskList taskList, Parser parser, Ui ui) { + try { + int index = parser.parseTaskNumber(args, taskList.getNumberOfTasks(), TaskAction.MARK_UNDONE); + taskList.unmarkTask(index, ui); + } catch (RoverException e) { + ui.displayError(e.getMessage()); + } + } +} diff --git a/src/main/java/rover/exceptions/RoverException.java b/src/main/java/rover/exceptions/RoverException.java new file mode 100644 index 0000000000..f52028ce2a --- /dev/null +++ b/src/main/java/rover/exceptions/RoverException.java @@ -0,0 +1,21 @@ +package rover.exceptions; + +/** + * RoverException is a custom exception class that extends the Exception class. + * It is used to throw exceptions when the rover chatBot is not able to function properly. + */ +public final class RoverException extends Exception { + public RoverException(String message) { + super(message); + } + + /** + * Overridden toString method to include the class name in the exception message. + * + * @return String + */ + @Override + public String toString() { + return "RoverException: " + this.getMessage(); + } +} diff --git a/src/main/java/rover/main/Launcher.java b/src/main/java/rover/main/Launcher.java new file mode 100644 index 0000000000..75ed9fa5e4 --- /dev/null +++ b/src/main/java/rover/main/Launcher.java @@ -0,0 +1,12 @@ +package rover.main; +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + assert args != null : "Arguments array should not be null"; + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/rover/main/Main.java b/src/main/java/rover/main/Main.java new file mode 100644 index 0000000000..4d64003168 --- /dev/null +++ b/src/main/java/rover/main/Main.java @@ -0,0 +1,35 @@ +package rover.main; +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; +import rover.ui.Gui; + +/** + * A GUI for Rover using FXML. + */ +public class Main extends Application { + + private final Rover rover = new Rover(); + + @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("Rover - Your Personal Task Manager"); + stage.setMinHeight(220); + stage.setMinWidth(417); + fxmlLoader.getController().setRover(rover); + stage.show(); + rover.startSession(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/rover/main/Rover.java b/src/main/java/rover/main/Rover.java new file mode 100644 index 0000000000..bc6133af2d --- /dev/null +++ b/src/main/java/rover/main/Rover.java @@ -0,0 +1,115 @@ +package rover.main; +import java.time.format.DateTimeParseException; + +import rover.command.Command; +import rover.exceptions.RoverException; +import rover.parser.Parser; +import rover.preferences.UserPreferences; +import rover.storage.Storage; +import rover.task.TaskList; +import rover.ui.TextUi; +import rover.ui.Ui; + +/** + * Rover is a personal task manager that helps users keep track of their tasks. + **/ +public final class Rover { + + private static final String DEFAULT_TASKS_FILE_PATH = "data/Tasks.txt"; + private static final String DEFAULT_PREFERENCES_FILE_PATH = "data/Preferences.json"; + + private UserPreferences userPreferences; + private final Storage storage; + private TaskList taskList; + private final Parser parser; + private Ui ui; + + /** + * Creates a new Rover instance with the default file path. + */ + public Rover() { + this(DEFAULT_TASKS_FILE_PATH, DEFAULT_PREFERENCES_FILE_PATH); + } + + /** + * Creates a new Rover instance by loading tasks from the specified file path. + * + * @param tasksFilePath The file path to save and load tasks from. + * @param preferencesFilePath The file path to save and load preferences from. + */ + private Rover(String tasksFilePath, String preferencesFilePath) { + parser = new Parser(); + ui = new TextUi(); + storage = new Storage(tasksFilePath, preferencesFilePath); + } + + /** + * Sets the Ui instance for Rover. + */ + public void setUi(Ui ui) { + this.ui = ui; + this.userPreferences = new UserPreferences(storage.loadPreferences(ui)); + this.ui.setUserPreferences(userPreferences); + } + + /** + * Starts the Rover session by displaying the welcome message. + */ + public void startSession() { + ui.showWelcome(); + try { + taskList = new TaskList(ui, storage.loadTasks(ui)); + } catch (RoverException | DateTimeParseException e) { + ui.displayError("Could not load saved tasks properly. Saved tasks could be corrupted."); + taskList = new TaskList(); + } + } + + /** + * Handles the response from Rover based on the user input. + */ + public boolean handleResponse(String input) { + Command command = parser.parseCommand(input); + command.execute(taskList, parser, ui); + return command.isExit(); + } + + /** + * Ends the Rover session by saving the tasks and displaying the goodbye message. + */ + public void endSession() { + storage.saveAll(taskList, userPreferences, ui); + while (!storage.isSavedSuccessfully()) { + ui.displayError("Could not save tasks. Try again? (Y/N)"); + String input = ui.readCommand(); + Command command = parser.parseCommand(input); + command.execute(taskList, storage, ui); + if (command.isExit()) { + break; + } + } + ui.sayBye(); + } + + /** + * Runs the Rover program. + */ + private void run() { + setUi(ui); + startSession(); + boolean isExit = false; + while (!isExit) { + isExit = handleResponse(ui.readCommand()); + } + endSession(); + } + + /** + * Creates a new Rover instance and runs the program. + * + * @param args Command line arguments. + */ + public static void main(String[] args) { + new Rover(DEFAULT_TASKS_FILE_PATH, DEFAULT_PREFERENCES_FILE_PATH).run(); + } +} diff --git a/src/main/java/rover/parser/DateTimeParser.java b/src/main/java/rover/parser/DateTimeParser.java new file mode 100644 index 0000000000..cc92f181ae --- /dev/null +++ b/src/main/java/rover/parser/DateTimeParser.java @@ -0,0 +1,91 @@ +package rover.parser; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * A utility class for parsing date and time strings. + */ +public final class DateTimeParser { + + public static final String[] DATE_FORMATS = { + "yyyy/MM/dd", + "yyyy-MM-dd", + "dd-MM-yyyy", + "dd/MM/yyyy", + "dd/MM/yy", + "dd-MM-yy" + }; + + public static final String[] TIME_FORMATS = { + "HH:mm", + "HHmm", + "h:mma", + "h.mma", + "hha", + "ha" + }; + + /** + * Parses a date and time string using the supported formats. + * + * @param dateTimeString the date and time string to parse + * @return LocalDateTime object representing the parsed date and time + * @throws DateTimeParseException if the string cannot be parsed + */ + public static LocalDateTime parseDateTime(String dateTimeString) throws DateTimeParseException { + assert dateTimeString != null : "Date and time string should not be null"; + for (String dateFormat : DATE_FORMATS) { + for (String timeFormat : TIME_FORMATS) { + String format = dateFormat + " " + timeFormat; + try { + return LocalDateTime.parse(dateTimeString, DateTimeFormatter.ofPattern(format)); + } catch (DateTimeParseException e) { + // Continue to the next formatter + } + } + } + throw new DateTimeParseException("Unable to parse date and time: " + dateTimeString, dateTimeString, 0); + } + + /** + * Parses a date string using the supported formats. + * + * @param dateString the date string to parse + * @return LocalDate object representing the parsed date + * @throws DateTimeParseException if the string cannot be parsed + */ + public static LocalDate parseDate(String dateString) throws DateTimeParseException { + assert dateString != null : "Date string should not be null"; + for (String format : DATE_FORMATS) { + try { + return LocalDate.parse(dateString, DateTimeFormatter.ofPattern(format)); + } catch (DateTimeParseException e) { + // Continue to the next formatter + } + } + throw new DateTimeParseException("Unable to parse date: " + dateString, dateString, 0); + } + + /** + * Parses a time string using the supported formats. + * + * @param timeString the time string to parse + * @return LocalTime object representing the parsed time + * @throws DateTimeParseException if the string cannot be parsed + */ + public static LocalTime parseTime(String timeString) throws DateTimeParseException { + assert timeString != null : "Time string should not be null"; + for (String format : TIME_FORMATS) { + try { + return LocalTime.parse(timeString, DateTimeFormatter.ofPattern(format)); + } catch (DateTimeParseException e) { + // Continue to the next formatter + } + } + throw new DateTimeParseException("Unable to parse time: " + timeString, timeString, 0); + } +} diff --git a/src/main/java/rover/parser/Parser.java b/src/main/java/rover/parser/Parser.java new file mode 100644 index 0000000000..cb72b7cd02 --- /dev/null +++ b/src/main/java/rover/parser/Parser.java @@ -0,0 +1,156 @@ +package rover.parser; + +import java.time.format.DateTimeParseException; + +import rover.command.AddCommand; +import rover.command.Command; +import rover.command.DeleteCommand; +import rover.command.EmptyCommand; +import rover.command.ExitCommand; +import rover.command.FindCommand; +import rover.command.InvalidCommand; +import rover.command.ListCommand; +import rover.command.MarkCommand; +import rover.command.RetrySaveCommand; +import rover.command.SetCommand; +import rover.command.ShowAfterCommand; +import rover.command.ShowBeforeCommand; +import rover.command.UnmarkCommand; +import rover.exceptions.RoverException; +import rover.preferences.PreferenceOption; +import rover.task.Deadline; +import rover.task.Event; +import rover.task.Task; +import rover.task.TaskAction; +import rover.task.Todo; + +/** + * Parser class to parse user input + */ +public final class Parser { + + private boolean isPreviousCommandBye = false; + + /** + * Parses the user input and returns the corresponding command + * + * @param args User input + * @return Command object + */ + public Command parseCommand(String args) { + String stdInput = args.trim().toLowerCase(); + if (stdInput.isEmpty()) { + return new EmptyCommand(args); + } else if (stdInput.equals("bye")) { + isPreviousCommandBye = true; + return new ExitCommand(args); + } else if (isPreviousCommandBye && (stdInput.equals("y") || stdInput.equals("n") + || stdInput.equals("yes") || stdInput.equals("no"))) { + return new RetrySaveCommand(args); + } else if (stdInput.equals("list")) { + return new ListCommand(args); + } else if (stdInput.startsWith("find")) { + return new FindCommand(args); + } else if (stdInput.startsWith("mark")) { + return new MarkCommand(args); + } else if (stdInput.startsWith("unmark")) { + return new UnmarkCommand(args); + } else if (stdInput.startsWith("delete")) { + return new DeleteCommand(args); + } else if (stdInput.startsWith("set")) { + return new SetCommand(args); + } else if (stdInput.startsWith("show before")) { + return new ShowBeforeCommand(args); + } else if (stdInput.startsWith("show after")) { + return new ShowAfterCommand(args); + } else if (stdInput.startsWith("todo") || stdInput.startsWith("deadline") || stdInput.startsWith("event")) { + return new AddCommand(args); + } else { + return new InvalidCommand(args); + } + } + + /** + * Parses the preference option and returns the corresponding preference option + * + * @param option Preference option specified by the user + * @return Preference option + * @throws RoverException If the preference option is invalid + */ + public PreferenceOption parsePreferenceOption(String option) throws RoverException { + return switch (option) { + case "name" -> PreferenceOption.NAME; + case "userImage" -> PreferenceOption.USER_IMAGE; + case "roverImage" -> PreferenceOption.ROVER_IMAGE; + default -> throw new RoverException("Invalid preference option"); + }; + } + + /** + * Parses the task description and returns the corresponding task object + * + * @param description Task description + * @return Task object + * @throws RoverException If the task description is invalid + * @throws DateTimeParseException If the date and time format is invalid + */ + public Task parseTaskDescription(String description) throws RoverException, DateTimeParseException { + Task newTask; + description = description.trim(); + if (description.toLowerCase().startsWith("deadline")) { + newTask = new Deadline(description.substring(8).trim()); + } else if (description.toLowerCase().startsWith("event")) { + newTask = new Event(description.substring(5).trim()); + } else if (description.toLowerCase().startsWith("todo")) { + newTask = new Todo(description.substring(4).trim()); + } else { + throw new RoverException("Not a valid task type."); + } + return newTask; + } + + /** + * Parses the task number and returns the corresponding task index + * + * @param taskNumber Task number + * @param numberOfTasks Number of tasks + * @param taskAction Task action used to determine error message + * @return Task index + * @throws RoverException If the task number is invalid + */ + public int parseTaskNumber(String taskNumber, int numberOfTasks, TaskAction taskAction) throws RoverException { + String action = getAction(taskAction); + int index = getIndex(taskNumber, action); + checkValidIndex(numberOfTasks, index, action); + return index; + } + + private String getAction(TaskAction taskAction) { + return switch (taskAction) { + case MARK_DONE -> "marked as done"; + case MARK_UNDONE -> "marked as not done"; + case DELETE -> "deleted"; + default -> throw new AssertionError("Invalid task action"); + }; + } + + private int getIndex(String taskNumber, String action) throws RoverException { + if (taskNumber.isEmpty()) { + throw new RoverException("Please specify the task number to be " + action + "."); + } + int index; + try { + index = Integer.parseInt(taskNumber) - 1; + } catch (NumberFormatException e) { + throw new RoverException("Please specify a valid task number to be " + action + "."); + } + return index; + } + + private void checkValidIndex(int numberOfTasks, int index, String action) throws RoverException { + if (index < 0 || index >= numberOfTasks) { + throw new RoverException("Please specify a valid task number to be " + action + ".\n" + + "You only have " + numberOfTasks + " tasks in total."); + } + } +} diff --git a/src/main/java/rover/preferences/PreferenceOption.java b/src/main/java/rover/preferences/PreferenceOption.java new file mode 100644 index 0000000000..2e10833403 --- /dev/null +++ b/src/main/java/rover/preferences/PreferenceOption.java @@ -0,0 +1,10 @@ +package rover.preferences; + +/** + * Represents the options for user preferences. + */ +public enum PreferenceOption { + NAME, + USER_IMAGE, + ROVER_IMAGE +} diff --git a/src/main/java/rover/preferences/UserPreferences.java b/src/main/java/rover/preferences/UserPreferences.java new file mode 100644 index 0000000000..db9886691b --- /dev/null +++ b/src/main/java/rover/preferences/UserPreferences.java @@ -0,0 +1,78 @@ +package rover.preferences; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Represents the user preferences. + */ +public class UserPreferences { + + private ObjectNode jsonNode; + + /** + * Creates a new UserPreferences object with default values. + */ + public UserPreferences() { + ObjectMapper objectMapper = new ObjectMapper(); + this.jsonNode = objectMapper.createObjectNode(); + this.jsonNode.put("name", ""); + this.jsonNode.put("userImage", "default"); + this.jsonNode.put("roverImage", "default"); + } + + /** + * Creates a new UserPreferences object. + * + * @param jsonNode The JSON node representing the user preferences. + */ + public UserPreferences(ObjectNode jsonNode) { + this(); + if (jsonNode != null) { + this.jsonNode = jsonNode; + } + } + + /** + * Sets the name of the user. + * + * @param name The name of the user. + * @return True if the name is successfully set, false otherwise. + */ + public boolean setName(String name) { + jsonNode.put("name", name); + return jsonNode.get("name").asText().equals(name); + } + + /** + * Sets the image of the user. + * + * @param userImagePath The path to the image of the user. + * @return True if the image is successfully set, false otherwise. + */ + public boolean setUserImage(String userImagePath) { + jsonNode.put("userImage", userImagePath); + return jsonNode.get("userImage").asText().equals(userImagePath); + } + + /** + * Sets the image of the rover. + * + * @param roverImagePath The path to the image of the rover. + * @return True if the image is successfully set, false otherwise. + */ + public boolean setRoverImage(String roverImagePath) { + jsonNode.put("roverImage", roverImagePath); + return jsonNode.get("roverImage").asText().equals(roverImagePath); + } + + /** + * Returns the JSON node representing the user preferences. + * + * @return The JSON node representing the user preferences. + */ + public JsonNode getJsonNode() { + return jsonNode; + } +} diff --git a/src/main/java/rover/storage/JsonFileManager.java b/src/main/java/rover/storage/JsonFileManager.java new file mode 100644 index 0000000000..b64c03effc --- /dev/null +++ b/src/main/java/rover/storage/JsonFileManager.java @@ -0,0 +1,45 @@ +package rover.storage; + +import java.io.File; +import java.io.IOException; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Represents a writer that writes tasks to a JSON file. + */ +public class JsonFileManager { + + private final ObjectMapper objectMapper; + + /** + * Creates a new JsonFileManager object. + */ + public JsonFileManager() { + this.objectMapper = new ObjectMapper(); + } + + /** + * Loads the JSON node from the specified file path. + * + * @param preferencesFilePath The file path to load the JSON node from. + * @return The JSON node loaded from the file path. + * @throws IOException If an I/O error occurs. + */ + public JsonNode load(String preferencesFilePath) throws IOException { + return objectMapper.readTree(new File(preferencesFilePath)); + } + + /** + * Writes the specified JSON node to the specified file path. + * + * @param jsonNode The JSON node to write. + * @param preferencesFilePath The file path to write the JSON node to. + * @throws IOException If an I/O error occurs. + */ + public void write(JsonNode jsonNode, String preferencesFilePath) throws IOException { + objectMapper.writeValue(new File(preferencesFilePath), jsonNode); + } + +} diff --git a/src/main/java/rover/storage/Storage.java b/src/main/java/rover/storage/Storage.java new file mode 100644 index 0000000000..4519cf35dc --- /dev/null +++ b/src/main/java/rover/storage/Storage.java @@ -0,0 +1,153 @@ +package rover.storage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import rover.preferences.UserPreferences; +import rover.task.Task; +import rover.task.TaskList; +import rover.ui.Ui; + +/** + * Handles the storage of the tasks in the file system. + */ +public final class Storage { + + private static final String NEW_LINE = System.lineSeparator(); + private final Path tasksFilePath; + private final Path preferencesFilePath; + private boolean isTasksSaved = false; + private boolean isPreferencesSaved = false; + private final JsonFileManager jsonFileManager = new JsonFileManager(); + + /** + * Returns a new Storage object with the specified file path. + * + * @param tasksFilePath The file path to save and load tasks from. + * @param preferencesFilePath The file path to save and load preferences from. + */ + public Storage(String tasksFilePath, String preferencesFilePath) { + String cwd = System.getProperty("user.dir"); + this.tasksFilePath = Paths.get(cwd, tasksFilePath.split("/")); + this.preferencesFilePath = Paths.get(cwd, preferencesFilePath.split("/")); + } + + /** + * Loads the tasks from the file system. + * + * @param ui The Ui object to display messages. + * @return An array of strings representing the tasks. + */ + public String[] loadTasks(Ui ui) { + try { + boolean fileExists = Files.exists(tasksFilePath); + if (!fileExists) { + return new String[0]; + } else { + Stream lines = Files.lines(tasksFilePath).filter(line -> !line.isBlank()); + String[] tasks = lines.toArray(String[]::new); + lines.close(); + return tasks; + } + } catch (IOException e) { + ui.displayError("Failed to load tasks."); + return new String[0]; + } + } + + /** + * Loads the preferences from the file system. + * + * @param ui The Ui object to display messages. + * @return The JSON node representing the preferences. + */ + public ObjectNode loadPreferences(Ui ui) { + try { + boolean fileExists = Files.exists(preferencesFilePath); + if (!fileExists) { + return (ObjectNode) (new UserPreferences()).getJsonNode(); + } + return (ObjectNode) jsonFileManager.load(preferencesFilePath.toString()); + } catch (IOException e) { + ui.displayError("Failed to load preferences."); + return (ObjectNode) (new UserPreferences()).getJsonNode(); + } + } + + /** + * Saves tasks and preferences to the file system. + * + * @param taskList The TaskList object containing the tasks to be saved. + * @param userPreferences The UserPreferences object containing the preferences to be saved. + * @param ui The Ui object to display messages. + */ + public void saveAll(TaskList taskList, UserPreferences userPreferences, Ui ui) { + assert taskList != null : "TaskList should not be null."; + assert userPreferences != null : "UserPreferences should not be null."; + assert ui != null : "Ui should not be null."; + String response = "Saving your tasks..."; + saveTasks(taskList); + response += NEW_LINE + (isTasksSaved ? "Tasks saved successfully!" : "Failed to save tasks."); + + response += NEW_LINE + "Saving your preferences..."; + savePreferences(userPreferences); + response += NEW_LINE + (isPreferencesSaved ? "Preferences saved successfully!" : "Failed to save preferences."); + + if (isTasksSaved && isPreferencesSaved) { + ui.showMessageWithoutLineSeparator(response); + } else { + ui.displayError(response); + } + } + + /** + * Saves the tasks to the file system and updates the isSaved field. + * + * @param taskList The TaskList object containing the tasks to be saved. + */ + private void saveTasks(TaskList taskList) { + assert taskList != null : "TaskList should not be null."; + try { + Files.createDirectories(tasksFilePath.getParent()); + Files.deleteIfExists(tasksFilePath); + Files.createFile(tasksFilePath); + String tasksString = taskList.getTasks().stream() + .map(Task::getTaskString) + .collect(Collectors.joining("\n")); + Files.writeString(tasksFilePath, tasksString, StandardOpenOption.WRITE); + isTasksSaved = true; + } catch (IOException e) { + isTasksSaved = false; + } + } + + /** + * Saves the preferences to the file system and updates the isSaved field. + * + * @param userPreferences The UserPreferences object containing the preferences to be saved. + */ + private void savePreferences(UserPreferences userPreferences) { + assert userPreferences != null : "UserPreferences should not be null."; + try { + jsonFileManager.write(userPreferences.getJsonNode(), preferencesFilePath.toString()); + isPreferencesSaved = true; + } catch (IOException e) { + isPreferencesSaved = false; + } + } + + /** + * Returns whether the tasks were saved successfully. + * + * @return True if the tasks were saved successfully, false otherwise. + */ + public boolean isSavedSuccessfully() { + return isTasksSaved && isPreferencesSaved; + } +} diff --git a/src/main/java/rover/task/Deadline.java b/src/main/java/rover/task/Deadline.java new file mode 100644 index 0000000000..6c4813f5ac --- /dev/null +++ b/src/main/java/rover/task/Deadline.java @@ -0,0 +1,168 @@ +package rover.task; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import rover.exceptions.RoverException; +import rover.parser.DateTimeParser; +import rover.ui.Ui; + +/** + * Represents a deadline task that can be added to the task list. + * A deadline task has a description, a status that indicates whether it is done, and a deadline. + */ +public final class Deadline extends Task { + + private LocalDate byDate; + private LocalTime byTime; + private String by; + private String byFullFormat; + + /** + * Constructs a deadline task with the given description. + * + * @param description The description of the deadline task. + * @throws RoverException If the description is not in the correct format. + */ + public Deadline(String description) throws RoverException, DateTimeParseException { + this(description, null); + } + + /** + * Constructs a deadline task with the given description. + * * The description must be in the format "task /by (deadline)". + * The deadline can be a date, time, or date and time. + * If the deadline is a date, the time will be set to 00:00. + * If the deadline is a time, the date will be set to the current date. + * If the deadline is a date and time, the date and time will be set accordingly. + * + * @param description The description of the deadline task. + * @param ui The ui object to display messages. + * @throws RoverException If the description is not in the correct format. + */ + public Deadline(String description, Ui ui) throws RoverException, DateTimeParseException { + super(description); + setByAndDescription(description); + setByDateAndTime(ui); + setByFullFormat(); + } + + private void setByAndDescription(String description) throws RoverException { + String[] parts = description.split(" /by "); + this.description = parts[0]; + if (parts.length != 2) { + throw new RoverException("A deadline task must be a task followed with '/by (deadline)'."); + } + this.by = parts[1]; + } + + private void setByDateAndTime(Ui ui) throws DateTimeParseException, RoverException { + String[] dateAndTime = by.split(" "); + if (dateAndTime.length == 1) { + // Deadline is a date only + // The case where only time is given is omitted as it doesn't make sense + // to create a deadline task on the same day with only a time + this.byDate = DateTimeParser.parseDate(dateAndTime[0]); + this.byTime = LocalTime.MAX; // Set to the end of the day + if (byDate.isBefore(LocalDate.now())) { + handleOverDue(ui, String.format("The following deadline: %s is overdue.", this.description), + "The deadline cannot be in the past."); + } + } else { + // Deadline is a date and time + this.byDate = DateTimeParser.parseDate(dateAndTime[0]); + this.byTime = DateTimeParser.parseTime(dateAndTime[1]); + if (byDate.isBefore(LocalDate.now()) || (byDate.isEqual(LocalDate.now()) + && byTime.isBefore(LocalTime.now()))) { + handleOverDue(ui, String.format("The following deadline: %s is overdue.", this.description), + "The deadline cannot be in the past."); + } + } + } + + private void handleOverDue(Ui ui, String warning, String error) throws RoverException { + if (ui != null) { + ui.showMessage(warning); + return; + } + throw new RoverException(error); + } + + private void setByFullFormat() { + this.byFullFormat = byDate.format(DateTimeFormatter.ofPattern("EEEE, dd MMMM, yyyy")); + this.byFullFormat += " " + byTime.format(DateTimeFormatter.ofPattern("h:mm a")).toLowerCase(); + } + + /** + * Checks if the task is due before the given date and time. + */ + @Override + public boolean isBefore(String dateTime) throws DateTimeParseException { + String[] parts = dateTime.split(" "); + if (parts.length == 1) { + try { // Interpret as a date only + LocalDate otherDate = DateTimeParser.parseDate(dateTime); + return byDate.isBefore(otherDate); + } catch (DateTimeParseException e) { + // Interpret as a time only + LocalDateTime otherTime = DateTimeParser.parseDateTime(LocalDate.now() + " " + dateTime); + return byDate.atTime(byTime).isBefore(otherTime); + } + } else { // Interpret as a date and time + LocalDateTime otherDateTime = DateTimeParser.parseDateTime(dateTime); + return byDate.atTime(byTime).isBefore(otherDateTime); + } + } + + /** + * Checks if the task is due after the given date and time. + */ + @Override + public boolean isAfter(String dateTime) { + String[] parts = dateTime.split(" "); + if (parts.length == 1) { + try { // Interpret as a date only + LocalDate otherDate = DateTimeParser.parseDate(dateTime); + return byDate.isAfter(otherDate); + } catch (DateTimeParseException e) { + // Interpret as a time only + LocalDateTime otherTime = DateTimeParser.parseDateTime(LocalDate.now() + " " + dateTime); + return byDate.atTime(byTime).isAfter(otherTime); + } + } else { // Interpret as a date and time + LocalDateTime otherDateTime = DateTimeParser.parseDateTime(dateTime); + return byDate.atTime(byTime).isAfter(otherDateTime); + } + } + + /** + * Compares this deadline task with the specified object for equality. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof Deadline other) { + return this.description.equals(other.description) && this.byDate.equals(other.byDate) + && this.byTime.equals(other.byTime); + } + return false; + } + + /** + * Returns the description of the deadline task for saving to the file. + */ + @Override + public String getTaskString() { + return "D | " + (isDone ? "1" : "0") + " | " + description + " /by " + by; + } + + /** + * Returns the string representation of the deadline task. + */ + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + byFullFormat + ")"; + } + +} diff --git a/src/main/java/rover/task/Event.java b/src/main/java/rover/task/Event.java new file mode 100644 index 0000000000..fa76fa4cf0 --- /dev/null +++ b/src/main/java/rover/task/Event.java @@ -0,0 +1,227 @@ +package rover.task; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import rover.exceptions.RoverException; +import rover.parser.DateTimeParser; +import rover.ui.Ui; + +/** + * Represents an event task. + * An event task is a task that has a start date and/or time and an end date and/or time. + */ +public final class Event extends Task { + + private LocalDate startDate; + private LocalTime startTime; + private LocalDate endDate; + private LocalTime endTime; + private String start; + private String end; + private String fromToFullFormat; + + /** + * Constructs an event task with the given description. + * + * @param description The description of the event task. + * @throws RoverException If the description is not in the correct format or the start date and time is after + * the end date and time. + */ + public Event(String description) throws RoverException, DateTimeParseException { + this(description, null); + } + + /** + * Constructs an event task with the given description. + * The description must be in the format "task /from (start) /to (end)". + * The start and end can be a date, time, or date and time. + * If the start or end is a date, the time will be set to 00:00. + * If the start or end is a time, the date will be set to the current date. + * If the start or end is a date and time, the date and time will be set accordingly. + * The start date and time must be before the end date and time. + * + * @param description The description of the event task. + * @param ui The ui object to display messages. + * @throws RoverException If the description is not in the correct format or the start date and time is after + * the end date and time. + */ + public Event(String description, Ui ui) throws RoverException, DateTimeParseException { + super(description); + setStartAndEnd(); + setStartDateAndTime(ui); + setEndDateAndTime(ui); + checkIfEndIsAfterStart(); + setFromToFullFormat(); + } + + private void setFromToFullFormat() { + fromToFullFormat = "from " + + startDate.format(DateTimeFormatter.ofPattern("EEEE, dd MMMM, yyyy")) + " " + + startTime.format(DateTimeFormatter.ofPattern("h:mm a")).toLowerCase() + " to " + + endDate.format(DateTimeFormatter.ofPattern("EEEE, dd MMMM, yyyy")) + " " + + endTime.format(DateTimeFormatter.ofPattern("h:mm a")).toLowerCase(); + } + + private void checkIfEndIsAfterStart() throws RoverException { + if (startDate.isAfter(endDate) || (startDate.isEqual(endDate) && startTime.isAfter(endTime))) { + throw new RoverException("The start date and time must be before the end date and time."); + } + } + + private void setStartAndEnd() throws RoverException { + String[] parts = description.split(" /from "); + if (parts.length != 2) { + throw new RoverException("An event task must be a task followed with '/from (start) /to (end)'."); + } + this.description = parts[0]; + String[] parts2 = parts[1].split(" /to "); + if (parts2.length != 2) { + throw new RoverException("An event task must be a task followed with '/from (start) /to (end)'."); + } + this.start = parts2[0]; + this.end = parts2[1]; + } + + private void setStartDateAndTime(Ui ui) throws DateTimeParseException, RoverException { + try { // Try to parse the start as a date and time + startDate = DateTimeParser.parseDateTime(start).toLocalDate(); + startTime = DateTimeParser.parseDateTime(start).toLocalTime(); + if (startDate.isBefore(LocalDate.now()) || (startDate.isEqual(LocalDate.now()) + && startTime.isBefore(LocalTime.now()))) { + handleOverDue(ui, String.format("The following event: %s has already transpired.", this.description), + "The start date and time cannot be in the past."); + } + } catch (DateTimeParseException e) { + try { // Try to parse the start as a date only + this.startDate = DateTimeParser.parseDate(start); + this.startTime = LocalTime.of(0, 0); + if (startDate.isBefore(LocalDate.now())) { + handleOverDue(ui, String.format("The following event: %s has already transpired.", + this.description), "The start date cannot be in the past."); + } + } catch (DateTimeParseException e2) { + // Try to parse the start as a time only + this.startDate = LocalDate.now(); + this.startTime = DateTimeParser.parseTime(start); + if (startTime.isBefore(LocalTime.now())) { + handleOverDue(ui, String.format("The following event: %s has already transpired.", + this.description), "The start time cannot be in the past."); + } + } + } + } + + private void setEndDateAndTime(Ui ui) throws DateTimeParseException, RoverException { + try { // Try to parse the end as a date and time + endDate = DateTimeParser.parseDateTime(end).toLocalDate(); + endTime = DateTimeParser.parseDateTime(end).toLocalTime(); + if (endDate.isBefore(LocalDate.now()) || (endDate.isEqual(LocalDate.now()) + && endTime.isBefore(LocalTime.now()))) { + handleOverDue(ui, String.format("The following event: %s has already transpired.", this.description), + "The end date and time cannot be in the past."); + } + } catch (DateTimeParseException e) { + try { // Try to parse the end as a time only + endDate = startDate; + endTime = DateTimeParser.parseTime(end); + if (startDate.equals(LocalDate.now()) && endTime.isBefore(LocalTime.now())) { + handleOverDue(ui, String.format("The following event: %s has already transpired.", + this.description), "The end time cannot be in the past."); + } + } catch (DateTimeParseException e2) { + // Try to parse the end as a date only + endDate = DateTimeParser.parseDate(end); + endTime = LocalTime.of(23, 59); + if (endDate.isBefore(LocalDate.now())) { + handleOverDue(ui, String.format("The following event: %s has already transpired.", + this.description), "The end date cannot be in the past."); + } + } + } + } + + private void handleOverDue(Ui ui, String warning, String error) throws RoverException { + if (ui != null) { + ui.showMessage(warning); + return; + } + throw new RoverException(error); + } + + /** + * Checks if the task is due before the given date and time. + * Event tasks are due before the start date and time. + */ + @Override + public boolean isBefore(String dateTime) { + String[] parts = dateTime.split(" "); + if (parts.length == 1) { + try { // Interpret as a date only + LocalDate otherDate = DateTimeParser.parseDate(dateTime); + return startDate.isBefore(otherDate); + } catch (DateTimeParseException e) { + // Interpret as a time only + LocalDateTime otherDateTime = DateTimeParser.parseDateTime(LocalDate.now() + " " + dateTime); + return startDate.atTime(startTime).isBefore(otherDateTime); + } + } else { // Interpret as a date and time + LocalDateTime otherDateTime = DateTimeParser.parseDateTime(dateTime); + return startDate.atTime(startTime).isBefore(otherDateTime); + } + } + + /** + * Checks if the task is due after the given date and time. + * Event tasks are due before the end date and time. + */ + @Override + public boolean isAfter(String dateTime) { + String[] parts = dateTime.split(" "); + if (parts.length == 1) { + try { // Interpret as a date only + LocalDate otherDate = DateTimeParser.parseDate(dateTime); + return startDate.isAfter(otherDate); + } catch (DateTimeParseException e) { + // Interpret as a time only + LocalDateTime otherDateTime = DateTimeParser.parseDateTime(LocalDate.now() + " " + dateTime); + return startDate.atTime(startTime).isAfter(otherDateTime); + } + } else { // Interpret as a date and time + LocalDateTime otherDateTime = DateTimeParser.parseDateTime(dateTime); + return startDate.atTime(startTime).isAfter(otherDateTime); + } + } + + /** + * Compares this event task with the specified object for equality. + * + * @param obj The object to compare with. + */ + public boolean equals(Object obj) { + if (obj instanceof Event other) { + return this.description.equals(other.description) && this.startDate.equals(other.startDate) + && this.startTime.equals(other.startTime) && this.endDate.equals(other.endDate) + && this.endTime.equals(other.endTime); + } + return false; + } + + /** + * Returns the description of the task for saving to the file. + */ + @Override + public String getTaskString() { + return "E | " + (isDone ? "1" : "0") + " | " + description + " /from " + start + " /to " + end; + } + + /** + * Returns the string representation of the task. + */ + @Override + public String toString() { + return "[E]" + super.toString() + " (" + fromToFullFormat + ")"; + } +} diff --git a/src/main/java/rover/task/Task.java b/src/main/java/rover/task/Task.java new file mode 100644 index 0000000000..2a8a3d484e --- /dev/null +++ b/src/main/java/rover/task/Task.java @@ -0,0 +1,81 @@ +package rover.task; + +import java.time.format.DateTimeParseException; + +import rover.exceptions.RoverException; + +/** + * Represents a task that can be added to the task list. + * A task has a description and a status that indicates whether it is done. + */ +public abstract sealed class Task permits Todo, Deadline, Event { + protected String description; + protected boolean isDone; + + /** + * Constructor for a task. + * + * @param description The description of the task. + * @throws RoverException If the description is empty. + */ + public Task(String description) throws RoverException { + this.description = description; + if (this.description.isEmpty()) { + throw new RoverException("The description of a task cannot be empty."); + } + this.isDone = false; + } + + /** + * Marks the task as done. + */ + public void setDone() { + this.isDone = true; + } + + /** + * Marks the task as undone. + */ + public void setUndone() { + this.isDone = false; + } + + private String getStatusIcon() { + return (isDone ? "X" : " "); + } + + /** + * Returns the description of the task for saving to the file. + * + * @return The description of the task. + */ + public abstract String getTaskString(); + + /** + * Checks if the task is due before the given date and time. + * + * @param dateTime The date and time to compare with. + * @return True if the task is due before the given date and time, false otherwise. + * @throws DateTimeParseException If the date and time is in the wrong format. + */ + public abstract boolean isBefore(String dateTime) throws DateTimeParseException; + + /** + * Checks if the task is due after the given date and time. + * + * @param dateTime The date and time to compare with. + * @return True if the task is due after the given date and time, false otherwise. + * @throws DateTimeParseException If the date and time is in the wrong format. + */ + public abstract boolean isAfter(String dateTime) throws DateTimeParseException; + + /** + * Returns the string representation of the task. + * + * @return The string representation of the task. + */ + @Override + public String toString() { + return "[" + this.getStatusIcon() + "] " + this.description; + } +} diff --git a/src/main/java/rover/task/TaskAction.java b/src/main/java/rover/task/TaskAction.java new file mode 100644 index 0000000000..f5ab15b339 --- /dev/null +++ b/src/main/java/rover/task/TaskAction.java @@ -0,0 +1,10 @@ +package rover.task; + +/** + * Represents the actions that can be performed on a task given by its index. + */ +public enum TaskAction { + MARK_DONE, + MARK_UNDONE, + DELETE +} diff --git a/src/main/java/rover/task/TaskList.java b/src/main/java/rover/task/TaskList.java new file mode 100644 index 0000000000..cf60ac800c --- /dev/null +++ b/src/main/java/rover/task/TaskList.java @@ -0,0 +1,190 @@ +package rover.task; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; + +import rover.exceptions.RoverException; +import rover.ui.Ui; + +/** + * Represents a list of tasks that can be added to, marked, unmarked, deleted, and displayed. + */ +public final class TaskList { + + private static final String NEW_LINE = System.lineSeparator(); + private static final String DELIMITER = " \\| "; + private final ArrayList tasks; + private int taskCount = 0; + + /** + * Returns an empty task list. + */ + public TaskList() { + this.tasks = new ArrayList<>(); + } + + /** + * Returns a task list with the tasks from the given array of task strings. + * + * @param ui The user interface to display messages. + * @param taskStrings The array of task strings to be converted to tasks. + * @throws RoverException If there is a possible corruption in the saved tasks. + * @throws DateTimeParseException If the date and time format is incorrect. + */ + public TaskList(Ui ui, String ...taskStrings) throws RoverException, DateTimeParseException { + assert taskStrings != null : "Task strings should not be null."; + this.tasks = new ArrayList<>(); + for (String taskString : taskStrings) { + String[] parts = taskString.split(DELIMITER); + if (parts.length != 3) { + throw new RoverException("Possible corruption in saved tasks."); + } + Task newTask = getTask(ui, parts); + tasks.add(newTask); + taskCount++; + } + } + + /** + * Returns a task based on the given parts of the task string. + * + * @param parts The parts of the task string. + * @return The task based on the parts. + * @throws RoverException If there is a possible corruption in the saved tasks. + */ + private Task getTask(Ui ui, String ...parts) throws RoverException { + assert parts != null : "Parts should not be null."; + Task newTask; + switch (parts[0]) { + case "T" -> newTask = new Todo(parts[2]); + case "D" -> newTask = new Deadline(parts[2], ui); + case "E" -> newTask = new Event(parts[2], ui); + default -> throw new RoverException("Possible corruption in saved tasks."); + } + if (parts[1].equals("1")) { + newTask.setDone(); + } else if (!parts[1].equals("0")) { + throw new RoverException("Possible corruption in saved tasks."); + } + return newTask; + } + + public ArrayList getTasks() { + return this.tasks; + } + + public int getNumberOfTasks() { + return this.taskCount; + } + + /** + * Displays all the tasks that match the given predicate. + * + * @param ui The user interface to display the tasks. + * @param predicate The predicate to filter the tasks. + * @param filterDescription The description of the filter. + */ + public void showTasks(Ui ui, BiFunction predicate, String filterDescription) { + assert predicate != null : "Predicate should not be null."; + assert filterDescription != null : "Filter description should not be null."; + assert !filterDescription.isEmpty() : "Filter description should not be empty."; + AtomicBoolean wasExceptionThrown = new AtomicBoolean(false); + List filteredTasks = tasks.stream().filter(task -> predicate.apply(task, wasExceptionThrown)).toList(); + if (wasExceptionThrown.get()) { + return; + } + + String response = getStringOfFilteredTasks(filteredTasks, filterDescription); + ui.showMessage(response); + } + + private String getStringOfFilteredTasks(List filteredTasks, String filterDescription) { + if (filteredTasks.isEmpty()) { + return "There are no tasks " + filterDescription + "."; + } + StringBuilder response = new StringBuilder("Here are the tasks " + filterDescription + ":" + NEW_LINE); + for (int i = 0; i < filteredTasks.size(); i++) { + response.append((i + 1)).append(". ").append(filteredTasks.get(i)); + if (i != filteredTasks.size() - 1) { + response.append(NEW_LINE); + } + } + return response.toString(); + } + + /** + * Adds a new task to the task list. + * + * @param newTask The new task to be added. + * @param ui The user interface to display the added task. + * @throws RoverException If the task already exists in the list. + */ + public void addTask(Task newTask, Ui ui) throws RoverException { + assert newTask != null : "Task should not be null."; + assert ui != null : "Ui should not be null."; + if (tasks.contains(newTask)) { + throw new RoverException("This task already exists in the list."); + } + tasks.add(newTask); + taskCount++; + String response = String.format("Got it%s. I've added this task:", ui.getUsername()) + NEW_LINE + + " " + newTask + NEW_LINE + + "Now you have " + taskCount + " task" + + (taskCount > 1 ? "s" : "") + " in the list."; + ui.showMessage(response); + } + + /** + * Marks a task in the task list as done. + * + * @param index The index of the task to be marked as done. + * @param ui The user interface to display the tasks found. + */ + public void markTask(int index, Ui ui) { + assert index >= 0 : "Index should be non-negative."; + assert index < taskCount : "Index should be less than the number of tasks."; + assert ui != null : "Ui should not be null."; + Task task = tasks.get(index); + task.setDone(); + String response = String.format("Nice%s! I've marked this task as done:", ui.getUsername()) + NEW_LINE + task; + ui.showMessage(response); + } + + /** + * Marks a task in the task list as undone. + * + * @param index The index of the task to be marked as undone. + * @param ui The user interface to display the tasks found. + */ + public void unmarkTask(int index, Ui ui) { + assert index >= 0 : "Index should be non-negative."; + assert index < taskCount : "Index should be less than the number of tasks."; + assert ui != null : "Ui should not be null."; + Task task = tasks.get(index); + task.setUndone(); + String response = String.format("Alright%s, I've marked this task as not done yet:", ui.getUsername()) + + NEW_LINE + task; + ui.showMessage(response); + } + + /** + * Deletes a task from the task list. + * + * @param index The index of the task to be deleted. + * @param ui The user interface to display the deleted task. + */ + public void deleteTask(int index, Ui ui) { + assert index >= 0 : "Index should be non-negative."; + assert index < taskCount : "Index should be less than the number of tasks."; + assert ui != null : "Ui should not be null."; + Task task = tasks.get(index); + tasks.remove(index); + taskCount--; + String response = String.format("Noted%s. I've removed this task:", ui.getUsername()) + NEW_LINE + task + + NEW_LINE + "Now you have " + taskCount + " task" + + (taskCount > 1 ? "s" : "") + " in the list."; + ui.showMessage(response); + } +} diff --git a/src/main/java/rover/task/Todo.java b/src/main/java/rover/task/Todo.java new file mode 100644 index 0000000000..7d46d74707 --- /dev/null +++ b/src/main/java/rover/task/Todo.java @@ -0,0 +1,72 @@ +package rover.task; + +import rover.exceptions.RoverException; + +/** + * Represents a todo task that can be added to the task list. + * A todo task has a description and a status that indicates whether it is done. + */ +public final class Todo extends Task { + + /** + * Constructor for a todo task. + * + * @param description The description of the todo task. + * @throws RoverException If the description is empty. + */ + public Todo(String description) throws RoverException { + super(description); + } + + /** + * Checks if the task is due before the given date and time. + * Todo tasks do not have a date and time and will always return false. + * + * @param dateTime The date and time to compare with. + */ + @Override + public boolean isBefore(String dateTime) { + return false; + } + + /** + * Checks if the task is due after the given date and time. + * Todo tasks do not have a date and time and will always return false. + * + * @param dateTime The date and time to compare with. + */ + @Override + public boolean isAfter(String dateTime) { + return false; + } + + /** + * Compares this todo task with the specified object for equality. + * + * @param obj The object to compare with. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof Todo other) { + return this.description.equals(other.description); + } + return false; + } + + /** + * Returns the description of the task for saving to the file. + */ + @Override + public String getTaskString() { + return "T | " + (isDone ? "1" : "0") + " | " + description; + } + + /** + * Returns the string representation of the task. + */ + @Override + public String toString() { + return "[T]" + super.toString(); + } + +} diff --git a/src/main/java/rover/ui/DialogBox.java b/src/main/java/rover/ui/DialogBox.java new file mode 100644 index 0000000000..e5954e9407 --- /dev/null +++ b/src/main/java/rover/ui/DialogBox.java @@ -0,0 +1,75 @@ +package rover.ui; +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +/** + * 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) { + assert text != null : "Text should not be null."; + assert img != null : "Image should not be null."; + try { + FXMLLoader fxmlLoader = new FXMLLoader(Gui.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. + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); + dialog.getStyleClass().add("reply-label"); + } + + /** + * Converts the dialog box to an error dialog box. + */ + private void convertToErrorDialog() { + dialog.getStyleClass().add("error-label"); + } + + public static DialogBox getUserDialog(String text, Image img) { + return new DialogBox(text, img); + } + + public static DialogBox getRoverDialog(String text, Image img) { + DialogBox db = new DialogBox(text, img); + db.flip(); + return db; + } + + public static DialogBox getErrorDialog(String text, Image img) { + DialogBox db = getRoverDialog(text, img); + db.convertToErrorDialog(); + return db; + } +} diff --git a/src/main/java/rover/ui/Gui.java b/src/main/java/rover/ui/Gui.java new file mode 100644 index 0000000000..89407ead11 --- /dev/null +++ b/src/main/java/rover/ui/Gui.java @@ -0,0 +1,282 @@ +package rover.ui; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Timer; +import java.util.TimerTask; + +import com.fasterxml.jackson.databind.JsonNode; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import rover.main.Rover; +import rover.preferences.UserPreferences; + +/** + * Controller for the main GUI. + */ +public final class Gui extends AnchorPane implements Ui { + + private static final Image DEFAULT_USER_IMAGE = new Image(Gui.class.getResourceAsStream("/images/User.png")); + private static final Image DEFAULT_ROVER_IMAGE = new Image(Gui.class.getResourceAsStream("/images/Rover.png")); + + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + + private Rover rover; + private Image userImage; + private Image roverImage; + private String username; + private UserPreferences userPreferences; + + /** + * Initializes the GUI. + */ + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + } + + /** + * Gets the UserPreferences instance. + * + * @return The UserPreferences instance. + */ + public UserPreferences getUserPreferences() { + return userPreferences; + } + + /** + * Injects the UserPreferences instance. + * + * @param userPreferences The UserPreferences instance. + */ + public void setUserPreferences(UserPreferences userPreferences) { + assert userPreferences != null : "User preferences should not be null."; + this.userPreferences = userPreferences; + + JsonNode name = userPreferences.getJsonNode().get("name"); + this.username = name.asText().isEmpty() ? "" : " " + name.asText(); + setUserImage(Path.of(userPreferences.getJsonNode().get("userImage").asText())); + setRoverImage(Path.of(userPreferences.getJsonNode().get("roverImage").asText())); + } + + /** + * Injects the Rover instance + */ + public void setRover(Rover r) { + rover = r; + rover.setUi(this); + } + + /** + * Gets the username of the user. + * + * @return The username of the user. + */ + @Override + public String getUsername() { + return username; + } + + /** + * Sets the username of the user. + * + * @param username The username of the user. + */ + @Override + public boolean setUsername(String username) { + assert username != null : "Username should not be null."; + assert !username.isBlank() : "Username should not be blank."; + this.username = " " + username; + return userPreferences.setName(username); + } + + /** + * Sets the image of the user. + * + * @param userImagePath The path to the image of the user. + */ + public boolean setUserImage(Path userImagePath) { + if (userImagePath.toString().equals("default")) { + userImage = DEFAULT_USER_IMAGE; + return false; + } else if (!Files.exists(userImagePath)) { + displayError(String.format("File %s not found at the specified path. " + + "Using default user image.", userImagePath)); + userImage = DEFAULT_USER_IMAGE; + return false; + } + userImage = new Image(userImagePath.toUri().toString()); + return userPreferences.setUserImage(userImagePath.toString()); + } + + /** + * Sets the image of the rover. + * + * @param roverImagePath The path to the image of the rover. + */ + public boolean setRoverImage(Path roverImagePath) { + if (roverImagePath.toString().equals("default")) { + roverImage = DEFAULT_ROVER_IMAGE; + return false; + } else if (!Files.exists(roverImagePath)) { + displayError(String.format("File %s not found at the specified path. " + + "Using default rover image.", roverImagePath)); + roverImage = DEFAULT_ROVER_IMAGE; + return false; + } + roverImage = new Image(roverImagePath.toUri().toString()); + return userPreferences.setRoverImage(roverImagePath.toString()); + } + + /** + * Shows the response from the user in the dialog container. + */ + private void showUserResponse() { + assert dialogContainer != null : "Dialog container should not be null."; + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(userInput.getText(), userImage) + ); + userInput.clear(); + } + + /** + * Shows the response from Rover in the dialog container. + * @param response The response from Rover. + */ + private void showRoverResponse(String response) { + assert dialogContainer != null : "Dialog container should not be null."; + dialogContainer.getChildren().addAll( + DialogBox.getRoverDialog(response, roverImage) + ); + } + + /** + * Reads the user's input. + * + * @return The user's input. + */ + @Override + public String readCommand() { + assert userInput != null : "User input should not be null."; + return userInput.getText(); + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing Rover's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + @FXML + private void handleUserInput() { + String input = readCommand(); + showUserResponse(); + assert rover != null : "Rover should not be null."; + boolean isExit = rover.handleResponse(input); + if (isExit) { + rover.endSession(); + } + } + + /** + * Displays the welcome message when the program starts. + */ + @Override + public void showWelcome() { + String response = String.format(""" + Hello%s! I'm Rover + I am your personal task manager. + What can I do for you? + """, username); + showRoverResponse(response); + } + + /** + * Displays the goodbye message when the program ends. + * Part of the code was taken from a StackOverflow answer given by Jason C. + * source: ... + * Thanks to Jason C. for the original code. + * Thanks to @zuoshihua for pointing out the issue with the original code. + * Thanks to @ChinZJ for the fix to use new Timer(true) instead. + */ + @Override + public void sayBye() { + String response = String.format("Bye%s. Hope to see you again soon!", username); + showRoverResponse(response); + new Timer(true).schedule(new TimerTask() { + public void run() { + Platform.exit(); + } + }, 2000); + } + + /** + * Displays the help message when the user types an invalid command. + */ + @Override + public void showHelpMessage() { + String briefHelp = String.format(""" + I'm sorry%s, but I don't know what that means. + The following commands are supported: + You can add a task by typing: + - todo (description) + - deadline (description) /by (deadline) + - event (description) /from (start) /to (end) + You can set your preferences by typing 'set (preference) (value)'. + To set your name, type 'set name (your name)'. + To set your user image, type 'set userImage (path to image)'. + To set Rover's image, type 'set roverImage (path to image)'. + List the existing tasks by typing 'list'. + Mark a task as done by typing 'mark (task number)'. + Mark a task as not done by typing 'unmark (task number)'. + Delete a task by typing 'delete (task number)'. + Find tasks with a certain keyword by typing 'find (keyword)'. + Show tasks before a certain date and/or time by typing 'show before (date) (time)'. + Show tasks after a certain date and/or time by typing 'show after (date) (time)'. + Exit the program by typing 'bye'. + """, username); + showRoverResponse(briefHelp); + } + + /** + * Displays the message to the user. + * + * @param message The message to be displayed. + */ + @Override + public void showMessage(String message) { + showRoverResponse(message); + } + + /** + * Displays the message to the user without a line separator. + * + * @param message The message to be displayed. + */ + @Override + public void showMessageWithoutLineSeparator(String message) { + showRoverResponse(message); + } + + /** + * Displays the error message to the user. + * + * @param message The error message to be displayed. + */ + @Override + public void displayError(String message) { + String response = "Oops! Error: " + message; + dialogContainer.getChildren().addAll( + DialogBox.getErrorDialog(response, roverImage) + ); + } +} diff --git a/src/main/java/rover/ui/TextUi.java b/src/main/java/rover/ui/TextUi.java new file mode 100644 index 0000000000..5640a81c99 --- /dev/null +++ b/src/main/java/rover/ui/TextUi.java @@ -0,0 +1,188 @@ +package rover.ui; + +import java.util.Scanner; + +import com.fasterxml.jackson.databind.JsonNode; + +import rover.preferences.UserPreferences; + +/** + * Ui class deals with interactions with the user. + * It displays messages to the user and reads input from the user. + */ +public final class TextUi implements Ui { + + private static final String divider = "--------------------------------------------"; + private String username = ""; + private final Scanner sc; + private UserPreferences userPreferences; + + /** + * Constructor for Ui class. + * It initializes the scanner to read input from the user. + */ + public TextUi() { + this.sc = new Scanner(System.in); + } + + /** + * Reads the next line of input from the user. + * + * @return The next line of input from the user. + */ + @Override + public String readCommand() { + String input = sc.nextLine(); + assert input != null && !input.isEmpty() : "Input should not be null or empty."; + return input; + } + + /** + * Gets the user preferences for the Ui. + * + * @return The user preferences object. + */ + public UserPreferences getUserPreferences() { + return userPreferences; + } + + /** + * Sets the user preferences for the Ui. + * + * @param userPreferences The user preferences object to set. + */ + @Override + public void setUserPreferences(UserPreferences userPreferences) { + assert userPreferences != null : "User preferences should not be null."; + this.userPreferences = userPreferences; + JsonNode name = userPreferences.getJsonNode().get("name"); + this.username = name.asText().isEmpty() ? "" : " " + name.asText(); + } + + /** + * Gets the username of the user. + * + * @return The username of the user. + */ + @Override + public String getUsername() { + return username; + } + + /** + * Sets the username of the user. + * + * @param username The username of the user. + */ + @Override + public boolean setUsername(String username) { + assert username != null : "Username should not be null."; + assert !username.isBlank() : "Username should not be blank."; + this.username = " " + username; + return userPreferences.setName(username); + } + + /** + * Displays a line to separate different messages. + */ + private void showLine() { + System.out.println(divider); + } + + /** + * Displays the welcome message when the program starts. + */ + @Override + public void showWelcome() { + String logo = """ + ___ + | _`\\ + | (_) ) _ _ _ __ _ __ + | , / /'_`\\ ( ) ( ) /'__`\\( '__) + | |\\ \\ ( (_) )| \\_/ |( ___/| | + (_) (_)`\\___/'`\\___/'`\\____)(_) + """; + + showLine(); + System.out.println("Hello" + username + "! I'm Rover"); + System.out.println(logo); + System.out.println("I am your personal task manager."); + System.out.println("What can I do for you?"); + showLine(); + } + + /** + * Displays the goodbye message when the program ends. + */ + @Override + public void sayBye() { + System.out.println("Bye" + username + ". Hope to see you again soon!"); + showLine(); + sc.close(); + } + + /** + * Displays the help message when the user types an invalid command. + */ + @Override + public void showHelpMessage() { + showLine(); + System.out.println("I'm sorry" + username + ", but I don't know what that means."); + String briefHelp = """ + The following commands are supported: + You can add a task by typing: + - todo (description) + - deadline (description) /by (deadline) + - event (description) /from (start) /to (end) + You can set your preferences by typing 'set (preference) (value)'. + To set your name, type 'set name (your name)'. + To set your user image, type 'set userImage (path to image)'. + To set Rover's image, type 'set roverImage (path to image)'. + List the existing tasks by typing 'list'. + Mark a task as done by typing 'mark (task number)'. + Mark a task as not done by typing 'unmark (task number)'. + Delete a task by typing 'delete (task number)'. + Find tasks with a certain keyword by typing 'find (keyword)'. + Show tasks before a certain date and/or time by typing 'show before (date) (time)'. + Show tasks after a certain date and/or time by typing 'show after (date) (time)'. + Exit the program by typing 'bye'. + """; + System.out.print(briefHelp); + showLine(); + } + + /** + * Displays the message to the user. + * + * @param message The message to be displayed. + */ + @Override + public void showMessage(String message) { + showLine(); + System.out.println(message); + showLine(); + } + + /** + * Displays the message to the user without the 2nd line separator. + * + * @param message The message to be displayed. + */ + @Override + public void showMessageWithoutLineSeparator(String message) { + showLine(); + System.out.println(message); + } + + /** + * Displays the error message to the user. + * + * @param message The error message to be displayed. + */ + @Override + public void displayError(String message) { + showLine(); + System.out.println("Oops! Error: " + message); + showLine(); + } +} diff --git a/src/main/java/rover/ui/Ui.java b/src/main/java/rover/ui/Ui.java new file mode 100644 index 0000000000..ba8da8ae4e --- /dev/null +++ b/src/main/java/rover/ui/Ui.java @@ -0,0 +1,31 @@ +package rover.ui; + +import rover.preferences.UserPreferences; + +/** + * Ui interface deals with interactions with the user. + */ +public interface Ui { + + UserPreferences getUserPreferences(); + + void setUserPreferences(UserPreferences userPreferences); + + String getUsername(); + + boolean setUsername(String username); + + String readCommand(); + + void showWelcome(); + + void sayBye(); + + void showHelpMessage(); + + void showMessage(String message); + + void showMessageWithoutLineSeparator(String message); + + void displayError(String message); +} diff --git a/src/main/resources/css/dialog-box.css b/src/main/resources/css/dialog-box.css new file mode 100644 index 0000000000..0a3c63d2f7 --- /dev/null +++ b/src/main/resources/css/dialog-box.css @@ -0,0 +1,31 @@ +.label { + -fx-background-color: linear-gradient(to bottom right, #00ffbf, #00ddff); + -fx-border-color: #00ddff #00ddff #00ddff #00ddff; + -fx-border-width: 2px; + -fx-background-radius: 1em 1em 0 1em; + -fx-border-radius: 1em 1em 0 1em; +} + +.reply-label { + -fx-background-color: linear-gradient(to bottom right, #ad8ef6, #b470f6); + -fx-border-color: #b470f6 #b470f6 #b470f6 #b470f6; + -fx-background-radius: 1em 1em 1em 0; + -fx-border-radius: 1em 1em 1em 0; +} + +.error-label { + -fx-background-color: linear-gradient(to bottom right, #dc5959, #e18888); + -fx-border-color: #e18888 #e18888 #e18888 #e18888; +} + +#displayPicture { + /* Shadow effect on image. */ + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.2), 10, 0.5, 5, 5); + + /* Change size of image. */ + -fx-scale-x: 1; + -fx-scale-y: 1; + + /* Rotate image clockwise by degrees. */ + -fx-rotate: 0; +} diff --git a/src/main/resources/css/main.css b/src/main/resources/css/main.css new file mode 100644 index 0000000000..a06d9ee3bb --- /dev/null +++ b/src/main/resources/css/main.css @@ -0,0 +1,46 @@ +.root { + main-color: rgb(237, 255, 242); /* Create a looked-up color called "main-color" within root. */ + -fx-background-color: main-color; +} + +.text-field { + -fx-background-color: #d9ffe2; + -fx-font: 20px "Arial"; +} + +.button { + -fx-background-color: mediumspringgreen; + -fx-font: italic bold 16px "Arial"; +} + +.button:hover { + -fx-background-color:cyan; + -fx-font-size: 18px; +} + +.button:pressed { + -fx-background-color:orange; + -fx-font-size: 20px; +} + +.scroll-pane, +.scroll-pane .viewport { + -fx-background-color: transparent; +} + +.scroll-bar { + -fx-font-size: 10px; /* Change width of scroll bar. */ + -fx-background-color: main-color; +} + +.scroll-bar .thumb { + -fx-background-color: #ff9cb4; + -fx-background-radius: 1em; +} + +/* Hides the increment and decrement buttons. */ +.scroll-bar .increment-button, +.scroll-bar .decrement-button { + -fx-pref-height: 0; + -fx-opacity: 0; +} diff --git a/src/main/resources/images/Rover.png b/src/main/resources/images/Rover.png new file mode 100644 index 0000000000..c815ea58f0 Binary files /dev/null and b/src/main/resources/images/Rover.png differ diff --git a/src/main/resources/images/User.png b/src/main/resources/images/User.png new file mode 100644 index 0000000000..b7349be178 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..a8fda3c0e4 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..7a0d3912e2 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,16 @@ + + + + + + + + +