diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..001c9e0351 --- /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' +} + +checkstyle { + toolVersion = '10.2' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + String javaFxVersion = '17.0.7' + + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + + showExceptions true + exceptionFormat "full" + showCauses true + showStackTraces true + showStandardStreams = false + } +} + +application { + mainClass.set("mysutong.Launcher") +} + +shadowJar { + archiveFileName = "mysutong.jar" + archiveBaseName = "mysutong" + archiveClassifier = null +} + +run{ + standardInput = System.in + enableAssertions = true +} \ No newline at end of file diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..eb761a9b9a --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..39efb6e4ac --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/data/duke.txt b/data/duke.txt new file mode 100644 index 0000000000..894b296745 --- /dev/null +++ b/data/duke.txt @@ -0,0 +1,2 @@ +T | 0 | borrow book +D | 0 | return book | 2019-12-02T18:00 diff --git a/data/tasks.txt b/data/tasks.txt new file mode 100644 index 0000000000..2c67cd0bcc --- /dev/null +++ b/data/tasks.txt @@ -0,0 +1,3 @@ +T | 0 | CS2103T IP | 2 +D | 0 | CS2100 Assignment 1 | 24/9/2024 2359 | 3 +T | 0 | 功课 | 3 diff --git a/docs/README.md b/docs/README.md index 47b9f984f7..8b408ffee5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,178 @@ -# Duke User Guide +# MySutong User Guide -// Update the title above to match the actual product name +![img.png](Ui.png) -// Product screenshot goes here +Welcome to **MySutong**! MySutong is a personal chatbot that helps you keep track of your daily tasks. You can add +different types of tasks, mark them as done, or tag them for easy reference. Sammy can also help you manage deadlines +and events. +--- +## Quick start -// Product intro goes here +1. Ensure you have Java 17 or above installed in your computer. You can install it from [here](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) +2. Download the latest `.jar` file from [here](https://github.com/yu-sutong/ip/releases) +3. Copy the file to the folder you want to use as the *home folder* of your task list. +4. Open a command terminal, `cd` into the folder you put the jar file in, and type `java -jar mysutong.jar` then press `enter` to run the application. +5. Type your command in the text field at the bottom and press `Enter` or "Send" to execute it. +## Features -## Adding deadlines +## 1. Add a Task: `todo` +Adds a simple task to your task list. -// Describe the action and its outcome. +Format: todo `TASK_DESCRIPTION` -// Give examples of usage +Example: +``` +todo CS2103T IP +``` + +Parameter: +- TASKDESCRIPTION is compulsory and must be a non-empty string +--- +## 2. Adding a Task with a Deadline: `deadline` + +Adds a task with a deadline to your task list. + +Format: `deadline TASK_DESCRIPTION /by DATE` + +Example: +``` +deadline submit CS2103T IP /by 23/9/2024 2359 +``` + +Parameter: +- TASK_DESCRIPTION and DATE are compulsory and must be a non-empty string. +- DATE must be in the form of dd/mm/yyyy where dd and mm can be a single or double digits. +--- +## 3. Adding an Event: `event` + +Adds an event with a start and end date/time to your task list. + +Format: `event TASK_DESCRIPTION /from DATE /to DATE` + +Example: +``` +event project meeting /from 23/9/2024 14:00 /to 23/9/2024 16:00 +``` + +Parameter: +- TASK_DESCRIPTION and DATE are compulsory and must be a non-empty string. +- DATE must be in the form of dd/mm/yyyy where dd and mm can be a single or double digits. +--- +## 4. Listing All Tasks: `list` + +Displays all the tasks in your task list. + +Format: `list` + +Example: +``` +list +``` +--- +## 5. Deleting a Task: `delete` + +Deletes a task from your task list. + +Format: `delete INDEX` +- `INDEX` refers to the position of the task in the list, starting from 1. + +Example: +``` +delete 2 +``` +This deletes the 2nd task in your list. + +Parameter: +- INDEX must be a positive integer and less than or equal to the total number of tasks in your list. +- INDEX starts from 1 (the first task on the list). +--- +## 6. Marking a Task as Done: `mark` + +Marks a task as done. + +Format: `mark INDEX` + +Example: +``` +mark 2 +``` +This marks the 2nd task in your list as done. + +Parameter: +- INDEX must be a positive integer and less than or equal to the total number of tasks in your list. +- INDEX starts from 1 (the first task on the list). +--- +## 7. Unmarking a Task: `unmark` -Example: `keyword (optional arguments)` +Unmarks a task, making it incomplete again. -// A description of the expected outcome goes here +Format: `unmark INDEX` +Example: ``` -expected output +unmark 2 ``` +This unmarks the 2nd task, making it incomplete. -## Feature ABC +Parameter: +- INDEX must be a positive integer and less than or equal to the total number of tasks in your list. +- INDEX starts from 1 (the first task on the list). +--- +## 8. Find matching tasks: `find` -// Feature details +Find matching tasks in your list based on the keyword. +Format: `find KEYWORD` + +Example: +``` +find assignment +``` -## Feature XYZ +Parameter: +- KEYWORD must be a non-empty string. +--- +## 9. Giving your task a priority: `priority` + +Give your task (based on the INDEX) a priority tag (based on PRIORITY_NO. +When PRIORITY_NO = 1, the tag is "High". +When PRIORITY_NO = 2, the tag is "Medium". +When PRIORITY_NO = 3, the tag is "Low". +The default tag for each task if "Low". + +Format: `priority INDEX PRIORITY_NO` + +Example: +``` +priority 2 1 +``` + +Parameter: +- INDEX must be a positive integer and less than or equal to the total number of tasks in your list. +- INDEX of the task starts from 1. +- PRIORITY_NO must be a integer from 1 to 3. +## 10. Exiting the Program: `bye` + +Exits the chatbot. + +Format: `bye` + +Example: +``` +bye +``` +--- -// Feature details \ No newline at end of file +## Command Summary +| Action | Format, Examples | +|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------| +| **Add a Task** | `todo TASK_DESCRIPTION`
Example: `todo read a book` | +| **Add a Task with a Deadline** | `deadline TASK_DESCRIPTION /by DATE`
Example: `deadline submit assignment /by 2024-09-30` | +| **Add an Event** | `event TASK_DESCRIPTION /from DATE /to DATE`
Example: `event project meeting /from 2024-09-21 14:00 /to 2024-09-21 16:00` | +| **List Tasks** | `list`
Example: `list` | +| **Delete a Task** | `delete INDEX`
Example: `delete 3` | +| **Mark a Task** | `mark INDEX`
Example: `mark 2` | +| **Unmark a Task** | `unmark INDEX`
Example: `unmark 2` | +| **Finding matching tasks** | `find KEYWORD`
Example: `find assignment` | +| **Giving task a priority** | `priority INDEX PRIORITY_NO`
Example: `priority 2 1` | +| **Exit** | `bye`
Example: `bye` | diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..0c407523e9 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/mysutong/Deadline.java b/src/main/java/mysutong/Deadline.java new file mode 100644 index 0000000000..e61ca9534f --- /dev/null +++ b/src/main/java/mysutong/Deadline.java @@ -0,0 +1,65 @@ +package mysutong; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Represents a deadline task with a specific date and time by which the task must be completed. + * Extends {@link Task}. + */ +public class Deadline extends Task { + /** + * The date and time by which the task must be completed. + */ + private LocalDateTime by; + + /** + * Constructs a new Deadline with the default priority (low). + * + * @param description The description of the deadline task. + * @param by The date and time by which the deadline task must be completed. + */ + public Deadline(String description, LocalDateTime by) { + super(description); + this.by = by; + } + + /** + * Constructs a new Deadline with a specific priority. + * + * @param description The description of the deadline task. + * @param by The date and time by which the deadline task must be completed. + * @param priority The priority level (1 for high, 2 for medium, 3 for low). + */ + public Deadline(String description, LocalDateTime by, int priority) { + super(description); + this.by = by; + setPriority(priority); // Set the priority using the method from the superclass. + } + + /** + * Returns a string representation of the deadline, including its description, + * its due date formatted as "MMM d yyyy h:mma", and the priority. + * + * @return A string representation of the deadline. + */ + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + by.format(DateTimeFormatter.ofPattern("MMM d yyyy h:mma")) + ")"; + } + + /** + * Returns a string suitable for file storage, representing the deadline task with its completion status, + * description, due date formatted as "d/M/yyyy HHmm", and priority. + * + * @return A formatted string suitable for file storage. + */ + @Override + public String toFileString() { + return String.format("D | %d | %s | %s | %d", + (isDone ? 1 : 0), + description, + by.format(DateTimeFormatter.ofPattern("d/M/yyyy HHmm")), + getPriority()); + } +} diff --git a/src/main/java/mysutong/DialogBox.java b/src/main/java/mysutong/DialogBox.java new file mode 100644 index 0000000000..7844c0db16 --- /dev/null +++ b/src/main/java/mysutong/DialogBox.java @@ -0,0 +1,85 @@ +package mysutong; + +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. + * This class manages the layout of dialog boxes for both the user and Duke in the GUI. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; // The label containing the dialog text. + @FXML + private ImageView displayPicture; // The ImageView displaying the speaker's image. + + /** + * Constructs a DialogBox with the specified text and image. + * This loads the FXML file and sets the text and image for the dialog box. + * + * @param text The text to display in the dialog box. + * @param img The image to display next to the dialog. + */ + private DialogBox(String text, Image img) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(img); + } + + /** + * Flips the dialog box such that the ImageView is on the left and text is on the right. + * This is used for differentiating the dialog box of Duke (flipped) from the user's dialog box (normal). + */ + private void flip() { + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + getChildren().setAll(tmp); + setAlignment(Pos.TOP_LEFT); // Align the flipped dialog box to the top left. + } + + /** + * Creates a dialog box for the user's input. + * The user's dialog box is not flipped (ImageView on the right, text on the left). + * + * @param text The text entered by the user. + * @param img The image representing the user. + * @return A DialogBox representing the user's input. + */ + public static DialogBox getUserDialog(String text, Image img) { + return new DialogBox(text, img); + } + + /** + * Creates a dialog box for Duke's response. + * Duke's dialog box is flipped (ImageView on the left, text on the right). + * + * @param text The text of Duke's response. + * @param img The image representing Duke. + * @return A DialogBox representing Duke's response, with the ImageView flipped to the left. + */ + public static DialogBox getDukeDialog(String text, Image img) { + var db = new DialogBox(text, img); + db.flip(); + return db; + } +} diff --git a/src/main/java/mysutong/Event.java b/src/main/java/mysutong/Event.java new file mode 100644 index 0000000000..2b955c2245 --- /dev/null +++ b/src/main/java/mysutong/Event.java @@ -0,0 +1,76 @@ +package mysutong; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Represents an event task with specific start and end dates and times. + * Extends {@link Task}. + */ +public class Event extends Task { + /** + * The starting date and time of the event. + */ + private LocalDateTime from; + + /** + * The ending date and time of the event. + */ + private LocalDateTime to; + + /** + * Constructs a new Event with the default priority (low). + * + * @param description The description of the event task. + * @param from The starting date and time of the event. + * @param to The ending date and time of the event. + */ + public Event(String description, LocalDateTime from, LocalDateTime to) { + super(description); + this.from = from; + this.to = to; + } + + /** + * Constructs a new Event with a specific priority. + * + * @param description The description of the event task. + * @param from The starting date and time of the event. + * @param to The ending date and time of the event. + * @param priority The priority level (1 for high, 2 for medium, 3 for low). + */ + public Event(String description, LocalDateTime from, LocalDateTime to, int priority) { + super(description); + this.from = from; + this.to = to; + setPriority(priority); // Set the priority using the method from the superclass. + } + + /** + * Returns a string representation of the event, including its description, priority, + * start and end dates formatted as "MMM d yyyy h:mma". + * + * @return A string representation of the event. + */ + @Override + public String toString() { + return "[E]" + super.toString() + " (from: " + from.format(DateTimeFormatter.ofPattern("MMM d yyyy h:mma")) + + " to: " + to.format(DateTimeFormatter.ofPattern("MMM d yyyy h:mma")) + ")"; + } + + /** + * Returns a string suitable for file storage, representing the event task with its completion status, + * description, start and end dates formatted as "d/M/yyyy HHmm", and priority. + * + * @return A formatted string suitable for file storage. + */ + @Override + public String toFileString() { + return String.format("E | %d | %s | %s | %s | %d", + (isDone ? 1 : 0), + description, + from.format(DateTimeFormatter.ofPattern("d/M/yyyy HHmm")), + to.format(DateTimeFormatter.ofPattern("d/M/yyyy HHmm")), + getPriority()); + } +} diff --git a/src/main/java/mysutong/InvalidTaskNumberException.java b/src/main/java/mysutong/InvalidTaskNumberException.java new file mode 100644 index 0000000000..1e4cfdbbea --- /dev/null +++ b/src/main/java/mysutong/InvalidTaskNumberException.java @@ -0,0 +1,17 @@ +package mysutong; + +/** + * Exception thrown when an invalid task number is used in operations that require a valid task index. + * Extends {@link SutongException}. + */ +class InvalidTaskNumberException extends SutongException { + + /** + * Constructs a new InvalidTaskNumberException with the specified detail message. + * + * @param message the detail message, providing more information about the exception context. + */ + public InvalidTaskNumberException(String message) { + super(message); + } +} diff --git a/src/main/java/mysutong/Launcher.java b/src/main/java/mysutong/Launcher.java new file mode 100644 index 0000000000..02dee82478 --- /dev/null +++ b/src/main/java/mysutong/Launcher.java @@ -0,0 +1,25 @@ +package mysutong; + +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + * + * This class serves as an entry point to launch the JavaFX application. + * It exists primarily to address classpath issues that might occur when launching + * the application directly from certain environments (like IDEs or build tools). + * The `main` method calls `Application.launch()`, which internally starts the + * JavaFX application lifecycle. + */ +public class Launcher { + + /** + * The main method that serves as the entry point for the Java application. + * It launches the JavaFX application by calling {@link Application#launch(Class, String...)}. + * + * @param args The command-line arguments passed to the application. + */ + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/mysutong/Main.java b/src/main/java/mysutong/Main.java new file mode 100644 index 0000000000..42e858f44f --- /dev/null +++ b/src/main/java/mysutong/Main.java @@ -0,0 +1,47 @@ +package mysutong; + +import java.io.IOException; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; + +/** + * A GUI for MySutong using FXML. + * + * This class is the entry point for the JavaFX application. + * It extends {@link javafx.application.Application} and sets up the main window + * for the GUI using FXML. + */ +public class Main extends Application { + + private MySutong mysutong = new MySutong("data/tasks.txt"); + + /** + * Starts the JavaFX application. + * This method is called when the JavaFX application is launched. + * It loads the FXML layout for the main window, sets up the scene, + * and displays the GUI. + * + * @param stage The primary stage for this application, onto which + * the application scene can be set. + */ + @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); + + // Inject the MySutong instance into the controller + fxmlLoader.getController().setMySutong(mysutong); + stage.setTitle("MySutong"); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/mysutong/MainWindow.java b/src/main/java/mysutong/MainWindow.java new file mode 100644 index 0000000000..fabde36d93 --- /dev/null +++ b/src/main/java/mysutong/MainWindow.java @@ -0,0 +1,68 @@ +package mysutong; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +/** + * Controller for the main GUI. + * This class is responsible for managing user interactions in the JavaFX GUI, + * such as handling user input and displaying responses from MySutong. + */ +public class MainWindow extends AnchorPane { + @FXML + private ScrollPane scrollPane; // ScrollPane to display the conversation between the user and MySutong. + @FXML + private VBox dialogContainer; // VBox to hold dialog boxes. + @FXML + private TextField userInput; // TextField for the user to type input. + @FXML + private Button sendButton; // Button to submit the user input. + + private MySutong mysutong; // Instance of MySutong to process user commands. + + private Image userImage = new Image(this.getClass().getResourceAsStream("/images/DaUser.png")); + private Image dukeImage = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png")); + /** + * Initializes the controller. + * This method binds the ScrollPane's scroll value to the height of the dialog container, + * ensuring that the latest dialog is always visible when the user interacts with the GUI. + */ + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + dialogContainer.getChildren().addAll(DialogBox.getDukeDialog("Hello there." , dukeImage)); + } + + /** + * Injects the MySutong instance into this controller. + * This method is used to pass the MySutong object from the main application into the controller + * so that the controller can delegate user input processing to MySutong. + * + * @param m The MySutong instance to be used by this controller. + */ + public void setMySutong(MySutong m) { + mysutong = m; + } + + /** + * Handles user input by creating dialog boxes for both the user input and MySutong's response. + * This method is triggered when the user submits a command. It processes the command, gets the response from + * MySutong, and displays both the input and response as dialog boxes in the dialog container. The user's input + * field is then cleared. + */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + String response = mysutong.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + } +} diff --git a/src/main/java/mysutong/MySutong.java b/src/main/java/mysutong/MySutong.java new file mode 100644 index 0000000000..a5ed92524f --- /dev/null +++ b/src/main/java/mysutong/MySutong.java @@ -0,0 +1,86 @@ +package mysutong; + +import java.io.IOException; + +/** + * Main class for the MySutong application, which manages tasks. + * This class is responsible for initializing the application, loading existing tasks from storage, + * and handling user interactions through a command loop or a GUI. + */ +public class MySutong { + private Storage storage; + private TaskList tasks; + private Ui ui; + + /** + * Constructs a new instance of MySutong with a specified file path for storage. + * Initializes the user interface, storage, and task list. If there is an error loading the tasks from storage, + * an empty task list will be initialized, and an error will be shown in the UI. + * + * @param filePath the path to the file where tasks are stored. + */ + public MySutong(String filePath) { + ui = new Ui(); + storage = new Storage(filePath); + try { + tasks = new TaskList(storage.load()); + } catch (IOException e) { + ui.showLoadingError(); + tasks = new TaskList(); // Initialize with an empty list if loading fails + ui.showError("Failed to load tasks from file: " + e.getMessage()); + } catch (Exception e) { + ui.showError("An unexpected error occurred: " + e.getMessage()); + tasks = new TaskList(); // Safeguard: Initialize an empty list on any other exceptions + } + } + + /** + * Runs the main loop of the application. + * Continuously reads user commands from the CLI, delegates execution of the commands to the {@link Parser}, + * and displays the output through the {@link Ui}. This loop continues until the application is terminated. + * It handles any exceptions that may occur during command execution and displays them via the UI. + */ + public void run() { + ui.showWelcome(); + Parser parser = new Parser(); + while (true) { + try { + String fullCommand = ui.readCommand(); + parser.executeCommand(fullCommand, tasks, ui, storage); + } catch (Exception e) { + ui.showError("An unexpected error occurred during command execution: " + e.getMessage()); + } + } + } + + /** + * Gets a response from MySutong for the given user input. + * This method is used in the GUI to process user input, delegate the command to the {@link Parser}, + * and return the output as a string. It captures any errors that occur and returns them as part of the response. + * + * @param input The user input/command to be processed. + * @return The response from MySutong after executing the given command. + */ + public String getResponse(String input) { + Parser parser = new Parser(); + try { + // Create a custom Ui to capture the output as a string + Ui responseUi = new Ui(); + parser.executeCommand(input, tasks, responseUi, storage); + return responseUi.getResponse(); // Get the captured output from Ui + } catch (Exception e) { + return "An error occurred: " + e.getMessage(); + } + } + + /** + * The entry point of the MySutong application. + * Initializes and runs the application with a predefined path for task storage. + * This method starts the CLI version of MySutong. + * + * @param args command-line arguments (not used). + */ + public static void main(String[] args) { + new MySutong("data/tasks.txt").run(); + } +} diff --git a/src/main/java/mysutong/NoDescriptionException.java b/src/main/java/mysutong/NoDescriptionException.java new file mode 100644 index 0000000000..4285a59bb3 --- /dev/null +++ b/src/main/java/mysutong/NoDescriptionException.java @@ -0,0 +1,17 @@ +package mysutong; + +/** + * Exception thrown when a required description is missing for a task or operation. + * Extends {@link SutongException}. + */ +class NoDescriptionException extends SutongException { + + /** + * Constructs a new NoDescriptionException with the specified detail message. + * + * @param message the detail message, providing more information about why the exception was thrown. + */ + public NoDescriptionException(String message) { + super(message); + } +} diff --git a/src/main/java/mysutong/Parser.java b/src/main/java/mysutong/Parser.java new file mode 100644 index 0000000000..4b8f6f990e --- /dev/null +++ b/src/main/java/mysutong/Parser.java @@ -0,0 +1,28 @@ +package mysutong; + +/** + * The {@code Parser} class interprets and executes user commands within the MySutong application. + * It processes the input commands and delegates execution to the {@code TaskList} for task-specific operations. + */ +public class Parser { + + /** + * Executes a user command by parsing it and delegating it to the {@code TaskList} to handle. + * + * @param fullCommand the raw input command from the user. + * @param tasks the current task list to operate on. + * @param ui the UI handler to provide feedback to the user. + * @param storage the storage handler to save/load tasks from persistent storage. + */ + public void executeCommand(String fullCommand, TaskList tasks, Ui ui, Storage storage) { + String[] inputs = fullCommand.split(" ", 2); + assert (inputs.length > 0); + String command = inputs[0]; + + try { + tasks.handleCommand(command, inputs.length > 1 ? inputs[1].trim() : "", ui, storage); + } catch (Exception e) { + ui.showError(e.getMessage()); + } + } +} diff --git a/src/main/java/mysutong/Storage.java b/src/main/java/mysutong/Storage.java new file mode 100644 index 0000000000..6a5d861e88 --- /dev/null +++ b/src/main/java/mysutong/Storage.java @@ -0,0 +1,123 @@ +package mysutong; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * Handles file-based storage operations for the MySutong application, including loading and saving tasks. + * Now supports saving and loading task priority. + */ +public class Storage { + private final String filePath; + + /** + * Constructs a new Storage instance with the specified file path. + * + * @param filePath the path to the file where tasks are stored and retrieved. + */ + public Storage(String filePath) { + this.filePath = filePath; + } + + /** + * Loads tasks from the file specified by {@code filePath}. + * Creates the file and its directories if they do not already exist. + * Parses the file to reconstruct task objects based on their saved format. + * + * @return a list of {@link Task} objects loaded from the file. + * @throws IOException if an I/O error occurs when opening or creating the file. + */ + public List load() throws IOException { + File file = new File(filePath); + if (!file.exists()) { + File parentDir = file.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + } + file.createNewFile(); + return new ArrayList<>(); // Return an empty list if file is newly created. + } + + Scanner scanner = new Scanner(file); + ArrayList tasks = new ArrayList<>(); + + while (scanner.hasNextLine()) { + String[] parts = scanner.nextLine().split(" \\| "); + String taskType = parts[0]; + boolean isDone = parts[1].equals("1"); + String description = parts[2]; + int priority = Integer.parseInt(parts[parts.length - 1]); // Priority is the last part + + Task task = parseTask(parts, taskType, description, isDone, priority); + tasks.add(task); + } + scanner.close(); + return tasks; + } + + /** + * Saves the current state of tasks to the file specified by {@code filePath}. + * Ensures that the directory structure for the file exists, creating it if necessary. + * + * @param tasks the {@link TaskList} containing tasks to be saved. + * @throws IOException if an I/O error occurs when writing to the file. + */ + public void save(TaskList tasks) throws IOException { + File file = new File(filePath); + File parentDir = file.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + } + + BufferedWriter writer = new BufferedWriter(new FileWriter(filePath)); + for (Task task : tasks.getTasks()) { + writer.write(task.toFileString()); + writer.newLine(); + } + writer.close(); + } + + /** + * Helper method to parse task data from loaded strings and create task objects. + * Now includes priority when creating task objects. + * + * @param parts the array of string parts from a split line of task data. + * @param taskType the type of the task ('T', 'D', 'E'). + * @param description the description of the task. + * @param isDone the completion status of the task. + * @param priority the priority of the task (1 for high, 2 for medium, 3 for low). + * @return the constructed Task object. + * @throws IllegalStateException if the task type is unknown. + */ + private Task parseTask(String[] parts, String taskType, String description, boolean isDone, int priority) { + Task task; + switch (taskType) { + case "T": + task = new Todo(description); + break; + case "D": + LocalDateTime deadlineDate = LocalDateTime.parse(parts[3], DateTimeFormatter.ofPattern("d/M/yyyy HHmm")); + task = new Deadline(description, deadlineDate); + break; + case "E": + LocalDateTime eventStart = LocalDateTime.parse(parts[3], DateTimeFormatter.ofPattern("d/M/yyyy HHmm")); + LocalDateTime eventEnd = LocalDateTime.parse(parts[4], DateTimeFormatter.ofPattern("d/M/yyyy HHmm")); + task = new Event(description, eventStart, eventEnd); + break; + default: + throw new IllegalStateException("Unknown task type: " + taskType); + } + if (isDone) { + task.markAsDone(); + } + task.setPriority(priority); // Set the priority from file + return task; + } +} diff --git a/src/main/java/mysutong/SutongException.java b/src/main/java/mysutong/SutongException.java new file mode 100644 index 0000000000..232cd235ee --- /dev/null +++ b/src/main/java/mysutong/SutongException.java @@ -0,0 +1,17 @@ +package mysutong; + +/** + * Custom exception class for the MySutong application, used to handle application-specific errors. + * Extends {@link Exception}. + */ +class SutongException extends Exception { + + /** + * Constructs a new SutongException with the specified detail message. + * + * @param message the detail message, providing more information about why the exception was thrown. + */ + public SutongException(String message) { + super(message); + } +} diff --git a/src/main/java/mysutong/Task.java b/src/main/java/mysutong/Task.java new file mode 100644 index 0000000000..669c21a767 --- /dev/null +++ b/src/main/java/mysutong/Task.java @@ -0,0 +1,111 @@ +package mysutong; + +/** + * Represents a general task in the MySutong application. + * This class provides the base structure and functionalities for task objects, + * including support for priorities (1 for high, 2 for medium, and 3 for low). + */ +public class Task { + protected String description; // Description of the task. + protected boolean isDone; // Status of the task, whether it is completed or not. + protected int priority; // Priority of the task (1 = high, 2 = medium, 3 = low). + + /** + * Constructs a new Task with a specific description. + * + * @param description The description of the task. + */ + public Task(String description) { + this.description = description; + this.isDone = false; // By default, the task is not completed. + this.priority = 3; // Default priority is low (3). + } + + /** + * Returns the completion status of the task. + * + * @return true if the task is completed, otherwise false. + */ + public boolean isDone() { + return isDone; + } + + /** + * Returns a status icon indicating whether the task is completed. + * 'X' represents completed, and a space represents incomplete. + * + * @return a string representing the completion status icon. + */ + public String getStatusIcon() { + return (isDone ? "X" : " "); // 'X' for done, ' ' for not done. + } + + /** + * Marks the task as completed. + */ + public void markAsDone() { + isDone = true; + } + + /** + * Marks the task as not completed. + */ + public void unmark() { + isDone = false; + } + + /** + * Sets the priority of the task. + * + * @param priority The priority level (1 for high, 2 for medium, 3 for low). + */ + public void setPriority(int priority) { + if (priority < 1 || priority > 3) { + throw new IllegalArgumentException("Priority must be 1 (high), 2 (medium), or 3 (low)."); + } + this.priority = priority; + } + + /** + * Gets the priority of the task. + * + * @return the priority level of the task (1 for high, 2 for medium, 3 for low). + */ + public int getPriority() { + return priority; + } + + /** + * Returns a string representation of the priority level. + * + * @return a string representing the priority of the task. + */ + public String getPriorityString() { + switch (priority) { + case 1: return "High"; + case 2: return "Medium"; + case 3: return "Low"; + default: return "Unknown"; + } + } + + /** + * Returns a string representation suitable for file storage. + * This method includes the priority of the task in the storage format. + * + * @return a formatted string representing the task for file storage. + */ + public String toFileString() { + return String.format("%s | %d | %s | %d", this.getClass().getSimpleName().charAt(0), (isDone ? 1 : 0), description, priority); + } + + /** + * Returns a string representation of the task, including its status, description, and priority. + * + * @return a string representation of the task. + */ + @Override + public String toString() { + return "[" + getStatusIcon() + "] " + description + " (Priority: " + getPriorityString() + ")"; + } +} diff --git a/src/main/java/mysutong/TaskList.java b/src/main/java/mysutong/TaskList.java new file mode 100644 index 0000000000..5ed36a76b8 --- /dev/null +++ b/src/main/java/mysutong/TaskList.java @@ -0,0 +1,297 @@ +package mysutong; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a list of tasks in the MySutong application. This class provides methods + * to manage tasks such as adding, removing, and retrieving tasks based on their index. + * It also supports searching for tasks based on a keyword found in the task's description. + * Additionally, it now handles commands like add, mark, unmark, delete, and set priority. + */ +public class TaskList { + private final List tasks; // The list of tasks, stored as a generic List interface. + + /** + * Constructs a new TaskList using an existing list of tasks. + * This allows for initializing the TaskList with a predefined list, + * enabling flexibility in the type of List used. + * + * @param tasks A List of {@link Task} objects to initialize the TaskList. + */ + public TaskList(List tasks) { + this.tasks = tasks; + } + + /** + * Default constructor that initializes the TaskList with an empty ArrayList. + * This is useful when no initial tasks are provided, and tasks need to be added dynamically. + */ + public TaskList() { + this.tasks = new ArrayList<>(); + } + + /** + * Returns the list of tasks. + * + * @return A list of {@link Task} objects. + */ + public List getTasks() { + return tasks; + } + + /** + * Retrieves a task from the list based on its index. + * + * @param index The index of the task to retrieve. + * @return The {@link Task} object at the specified index. + * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || index >= size()). + */ + public Task getTask(int index) { + if (index < 0 || index >= tasks.size()) { + throw new IndexOutOfBoundsException("Task index out of range."); + } + return tasks.get(index); + } + + /** + * Adds a new task to the task list. + * + * @param task The {@link Task} object to add to the list. + */ + public void addTask(Task task) { + tasks.add(task); + } + + /** + * Removes a task from the task list based on its index. + * + * @param index The index of the task to be removed. + * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || index >= size()). + */ + public void removeTask(int index) { + if (index < 0 || index >= tasks.size()) { + throw new IndexOutOfBoundsException("Task index out of range."); + } + tasks.remove(index); + } + + /** + * Searches for tasks that contain the specified keyword in their description. + * + * @param keyword The keyword to search for within the task descriptions. + * @return A list of {@link Task} objects that contain the keyword in their description. + */ + public List findTasksByKeyword(String keyword) { + List foundTasks = new ArrayList<>(); + for (Task task : tasks) { + if (task.description.toLowerCase().contains(keyword.toLowerCase())) { + foundTasks.add(task); + } + } + return foundTasks; + } + + /** + * Handles the execution of commands by delegating appropriate actions for each command. + * + * @param command The user command to be processed. + * @param arguments The arguments provided with the command (if any). + * @param ui The UI handler for displaying feedback to the user. + * @param storage The storage handler to save tasks after any changes. + */ + public void handleCommand(String command, String arguments, Ui ui, Storage storage) throws Exception { + switch (command) { + case "bye": + ui.showGoodbye(); + System.exit(0); + break; + + case "list": + ui.showTaskList(this); + break; + + case "mark": + markTaskAsDone(arguments, ui, storage); + break; + + case "unmark": + unmarkTask(arguments, ui, storage); + break; + + case "todo": + addTodoTask(arguments, ui, storage); + break; + + case "deadline": + addDeadlineTask(arguments, ui, storage); + break; + + case "event": + addEventTask(arguments, ui, storage); + break; + + case "delete": + deleteTask(arguments, ui, storage); + break; + + case "priority": + setPriority(arguments, ui, storage); + break; + + case "find": + findTasks(arguments, ui); + break; + + default: + throw new UnknownCommandException("I'm sorry, but I don't know what that means."); + } + } + + // Helper methods to handle different task operations: + + private void markTaskAsDone(String argument, Ui ui, Storage storage) throws Exception { + int index = parseTaskIndex(argument); + Task task = getTask(index); + task.markAsDone(); + ui.showLine(); + ui.showMessage("Nice! I've marked this task as done:"); + ui.showMessage(task.toString()); + ui.showLine(); + storage.save(this); + } + + private void unmarkTask(String argument, Ui ui, Storage storage) throws Exception { + int index = parseTaskIndex(argument); + Task task = getTask(index); + task.unmark(); + ui.showLine(); + ui.showMessage("OK, I've marked this task as not done yet:"); + ui.showMessage(task.toString()); + ui.showLine(); + storage.save(this); + } + + private void addTodoTask(String argument, Ui ui, Storage storage) throws Exception { + if (argument.isEmpty()) { + throw new NoDescriptionException("Description cannot be empty for todo."); + } + Task todo = new Todo(argument); + addTask(todo); + ui.showLine(); + ui.showMessage("Got it. I've added this task:"); + ui.showMessage(todo.toString()); + ui.showLine(); + storage.save(this); + } + + private void addDeadlineTask(String argument, Ui ui, Storage storage) throws Exception { + if (!argument.contains(" /by ")) { + throw new NoDescriptionException("Deadline command must include '/by' followed by a date/time."); + } + String[] details = argument.split(" /by ", 2); + Task deadline = new Deadline(details[0].trim(), + LocalDateTime.parse(details[1].trim(), + DateTimeFormatter.ofPattern("d/M/yyyy HHmm"))); + addTask(deadline); + ui.showLine(); + ui.showMessage("Got it. I've added this task:"); + ui.showMessage(deadline.toString()); + ui.showLine(); + storage.save(this); + } + + private void addEventTask(String argument, Ui ui, Storage storage) throws Exception { + // Ensure the argument contains both "/from" and "/to" + if (!argument.contains(" /from ") || !argument.contains(" /to ")) { + throw new NoDescriptionException( + "Event command must include '/from' and '/to' followed by their respective times."); + } + + try { + // Split the argument to extract the description, from date, and to date + String[] details = argument.split(" /from | /to ", 3); + + // Validate that three parts are extracted (description, from, and to) + if (details.length != 3) { + throw new NoDescriptionException( + "Please provide a valid description, from date, and to date for the event."); + } + + // Parse the from and to dates + LocalDateTime fromDate = LocalDateTime.parse(details[1].trim(), + DateTimeFormatter.ofPattern("d/M/yyyy HHmm")); + LocalDateTime toDate = LocalDateTime.parse(details[2].trim(), + DateTimeFormatter.ofPattern("d/M/yyyy HHmm")); + + // Create a new Event and add it to the task list + Task event = new Event(details[0].trim(), fromDate, toDate); + addTask(event); + + // Provide feedback to the user + ui.showLine(); + ui.showMessage("Got it. I've added this task:"); + ui.showMessage(event.toString()); + ui.showLine(); + + // Save the updated task list to storage + storage.save(this); + + } catch (ArrayIndexOutOfBoundsException e) { + throw new NoDescriptionException( + "Please provide valid dates for both /from and /to in the format 'd/M/yyyy HHmm'."); + } catch (Exception e) { + throw new NoDescriptionException( + "There was an error parsing the event dates. Ensure you use the format 'd/M/yyyy HHmm'."); + } + } + + private void deleteTask(String argument, Ui ui, Storage storage) throws Exception { + int index = parseTaskIndex(argument); + Task removedTask = getTask(index); + removeTask(index); + ui.showLine(); + ui.showMessage("Noted. I've removed this task:"); + ui.showMessage(removedTask.toString()); + ui.showLine(); + storage.save(this); + } + + private void setPriority(String argument, Ui ui, Storage storage) throws Exception { + String[] details = argument.split(" "); + if (details.length != 2) { + throw new IllegalArgumentException("Invalid priority command format. Use: priority "); + } + int index = parseTaskIndex(details[0]); + int priority = Integer.parseInt(details[1]); + Task task = getTask(index); + task.setPriority(priority); + ui.showLine(); + ui.showMessage("Task priority updated:"); + ui.showMessage(task.toString()); + ui.showLine(); + storage.save(this); + } + + private void findTasks(String keyword, Ui ui) { + List foundTasks = findTasksByKeyword(keyword); + ui.showSearchResults(foundTasks); + } + + private int parseTaskIndex(String argument) throws Exception { + if (argument.isEmpty()) { + throw new InvalidTaskNumberException("No task index provided."); + } + try { + int index = Integer.parseInt(argument) - 1; + if (index < 0 || index >= tasks.size()) { + throw new InvalidTaskNumberException("Task index out of range."); + } + return index; + } catch (NumberFormatException e) { + throw new InvalidTaskNumberException("Please provide a valid task number."); + } + } +} diff --git a/src/main/java/mysutong/Todo.java b/src/main/java/mysutong/Todo.java new file mode 100644 index 0000000000..328aa3b9c2 --- /dev/null +++ b/src/main/java/mysutong/Todo.java @@ -0,0 +1,52 @@ +package mysutong; + +/** + * Represents a todo task in the MySutong application. + * A todo task is a simple task with a description, completion status, and priority. + * Extends {@link Task}. + */ +class Todo extends Task { + + /** + * Constructs a new Todo instance with the specified description. + * The task is initialized with a default priority of "low" (priority 3). + * + * @param description the description of the todo task. + */ + public Todo(String description) { + super(description); // Call to the superclass (Task) constructor. + } + + /** + * Constructs a new Todo instance with the specified description and priority. + * + * @param description the description of the todo task. + * @param priority the priority level (1 for high, 2 for medium, 3 for low). + */ + public Todo(String description, int priority) { + super(description); // Call to the superclass (Task) constructor. + setPriority(priority); // Set the priority using the method from the superclass. + } + + /** + * Returns a string representation of the todo task, which includes the task type + * prefixed by "[T]" and the status, description, and priority from the superclass. + * + * @return a string representation of the todo task. + */ + @Override + public String toString() { + return "[T]" + super.toString(); // Include the task type in the toString output. + } + + /** + * Returns a string representation suitable for file storage. + * Includes the task type, status, description, and priority for this todo task. + * + * @return a formatted string representing the todo task for file storage. + */ + @Override + public String toFileString() { + return "T | " + (isDone ? "1" : "0") + " | " + description + " | " + getPriority(); + } +} diff --git a/src/main/java/mysutong/Ui.java b/src/main/java/mysutong/Ui.java new file mode 100644 index 0000000000..d5b7394074 --- /dev/null +++ b/src/main/java/mysutong/Ui.java @@ -0,0 +1,129 @@ +package mysutong; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * Handles the user interface for the MySutong application. + * This class manages all user interactions and display functionalities. + */ +public class Ui { + private final Scanner scanner = new Scanner(System.in); // Scanner to read user input. + private List responses; // List to store responses for later retrieval. + + /** + * Constructs a new Ui instance and initializes the response list. + */ + public Ui() { + responses = new ArrayList<>(); + } + + /** + * Displays a welcome message to the user at the start of the application. + */ + public void showWelcome() { + String horizontalLine = ""; + responses.add(horizontalLine); + responses.add("Hello! I'm mysutong.MySutong"); + responses.add("What can I do for you?"); + responses.add(horizontalLine); + } + + /** + * Adds a horizontal line for separating sections of output. + */ + public void showLine() { + responses.add(""); + } + + /** + * Adds an error message to the responses when there is a problem loading tasks from the file. + */ + public void showLoadingError() { + responses.add("Error loading tasks from file."); + } + + /** + * Reads a command from the user input (still needed for CLI functionality). + * + * @return the command entered by the user. + */ + public String readCommand() { + return scanner.nextLine(); + } + + /** + * Adds a goodbye message to the responses when the user exits the application. + */ + public void showGoodbye() { + responses.add("Bye. Hope to see you again soon!"); + } + + /** + * Adds the list of tasks currently in the TaskList to the responses. + * + * @param tasks the TaskList containing the tasks to be displayed. + */ + public void showTaskList(TaskList tasks) { + showLine(); + responses.add("Here are the tasks in your list:"); + for (int i = 0; i < tasks.getTasks().size(); i++) { + responses.add((i + 1) + ". " + tasks.getTasks().get(i)); + } + showLine(); + } + + /** + * Adds the search results to the responses. + * + * @param tasks the list of tasks that match the search criteria. + */ + public void showSearchResults(List tasks) { + showLine(); + if (tasks.isEmpty()) { + responses.add("No matching tasks found."); + } else { + responses.add("Here are the matching tasks in your list:"); + for (int i = 0; i < tasks.size(); i++) { + responses.add((i + 1) + ". " + tasks.get(i)); + } + } + showLine(); + } + + /** + * Adds a message to the responses. + * + * @param message the message to be added. + */ + public void showMessage(String message) { + responses.add(message); + } + + /** + * Adds an error message to the responses. + * + * @param message the error message to be added. + */ + public void showError(String message) { + responses.add("Oh no. " + message); + } + + /** + * Returns all the responses as a single concatenated string. + * + * @return the concatenated response string. + */ + public String getResponse() { + return String.join("\n", responses); + } + + /** + * Clears the current responses list. + * This is useful if you want to start fresh for a new response session. + */ + public void clearResponses() { + responses.clear(); + } +} diff --git a/src/main/java/mysutong/UnknownCommandException.java b/src/main/java/mysutong/UnknownCommandException.java new file mode 100644 index 0000000000..74c2417357 --- /dev/null +++ b/src/main/java/mysutong/UnknownCommandException.java @@ -0,0 +1,17 @@ +package mysutong; + +/** + * Exception thrown when the MySutong application encounters an unrecognized command. + * Extends {@link SutongException} to handle application-specific exceptions related to command processing. + */ +class UnknownCommandException extends SutongException { + + /** + * Constructs a new UnknownCommandException with the specified detail message. + * + * @param message the detail message, providing more information about why the exception was thrown. + */ + public UnknownCommandException(String message) { + super(message); // Pass the message up to the superclass constructor. + } +} diff --git a/src/main/resources/images/DaDuke.png b/src/main/resources/images/DaDuke.png new file mode 100644 index 0000000000..d893658717 Binary files /dev/null and b/src/main/resources/images/DaDuke.png differ diff --git a/src/main/resources/images/DaUser.png b/src/main/resources/images/DaUser.png new file mode 100644 index 0000000000..3c82f45461 Binary files /dev/null and b/src/main/resources/images/DaUser.png differ diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 0000000000..fe672c90ea --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..7f741bbf61 --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,44 @@ + + + + + + + + + + + +