diff --git a/.gitignore b/.gitignore index 2873e189e1..4da6229490 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +Johan.class +test_johan.txt diff --git a/README.md b/README.md index af0309a9ef..be006b04f1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Duke project template +# johan.Duke project template This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. @@ -13,7 +13,7 @@ Prerequisites: JDK 17, update Intellij to the most recent version. 1. If there are any further prompts, accept the defaults. 1. Configure the project to use **JDK 17** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
In the same dialog, set the **Project language level** field to the `SDK default` option. -1. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: +1. After that, locate the `src/main/java/johan.Duke.java` file, right-click it, and choose `Run johan.Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output: ``` Hello from ____ _ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..17ddf9f00b --- /dev/null +++ b/build.gradle @@ -0,0 +1,77 @@ +plugins { + id 'java' + id 'application' + id 'checkstyle' + id 'com.github.johnrengelman.shadow' version '7.1.2' +} + +repositories { + mavenCentral() + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } +} + +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' + implementation group: 'com.joestelmach', name: 'natty', version: '0.13' + checkstyle('com.puppycrawl.tools:checkstyle:10.12.7') { + exclude group: 'com.google.collections', module: 'google-collections' + } + 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("johan.launcher.Launcher") +} + +shadowJar { + archiveBaseName = "johan" + archiveClassifier = null + archiveFileName = 'johan.jar' + mergeServiceFiles() + archiveVersion.set(new Date().format('yyyyMMdd-HHmmss')) + archiveFileName = "johan-${archiveVersion.get()}.jar" +} + +run { + standardInput = System.in + enableAssertions = true +} + +test { + useJUnitPlatform() +} + +checkstyle { + toolVersion = '10.2' + configFile = file('config/checkstyle/checkstyle.xml') + ignoreFailures = true + showViolations = true +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..a1f4a94ff0 --- /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/data/johan.txt b/data/johan.txt new file mode 100644 index 0000000000..e3e2482db7 --- /dev/null +++ b/data/johan.txt @@ -0,0 +1,4 @@ +T | 0 | read book +E | 0 | project meeting | 12/2/2019 | 12/3/2020 +D | 0 | submit | 1/3/2025 +D | 0 | review | 1/2/2025 diff --git a/docs/README.md b/docs/README.md index 47b9f984f7..7fd227fb8b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,30 +1,84 @@ -# Duke User Guide + +# Johan Chatbot User Guide -// Update the title above to match the actual product name +![Johan Chatbot](Ui.png) -// Product screenshot goes here +Johan Chatbot is a JavaFX-based task manager that lets you organize your todos, deadlines, and events through a simple chat interface. Type commands in the text field and hit "Send" to manage your tasks efficiently. -// Product intro goes here +## Quick Start -## Adding deadlines +1. Download `johan.jar` from [releases](https://github.com/jhwan0707/ip/releases) or build it with `./gradlew shadowJar`. +2. **Run**: `java -jar build/libs/johan.jar`. -// Describe the action and its outcome. +## Adding Deadlines -// Give examples of usage +Add a task with a due date to keep track of time-sensitive duties. -Example: `keyword (optional arguments)` +- **Command**: `deadline /by ` +- **Examples**: + - `deadline submit report /by 2025-03-01` + - `deadline pay bills /by 01/03/2025` +- **Outcome**: Adds a deadline to your list, displayed as `[D][ ] (by: Mar 1 2025)`. -// A description of the expected outcome goes here +## Managing Tasks -``` -expected output -``` +### Listing Tasks +View all your tasks in the current order. -## Feature ABC +- **Command**: `list` +- **Example**: `list` +- **Outcome**: Shows all tasks with numbers (e.g., `1. [D][ ] submit report (by: Mar 1 2025)`). -// Feature details +### Marking Tasks +Mark tasks as done or undone. +- **Command**: `mark ` or `unmark ` +- **Examples**: `mark 1`, `unmark 1` +- **Outcome**: Updates task status (e.g., `[D][X] submit report` or `[D][ ] submit report`). -## Feature XYZ +### Deleting Tasks +Remove a task from your list. -// Feature details \ No newline at end of file +- **Command**: `delete ` +- **Example**: `delete 1` +- **Outcome**: Deletes the task at position 1. + +## Sorting Tasks + +Sort deadlines chronologically and other tasks alphabetically. + +- **Command**: `sort` +- **Example**: `sort` + - Before: `1. [D][ ] submit /by 2025-03-01`, `2. [D][ ] review /by 2025-02-01` + - After: `1. [D][ ] review /by 2025-02-01`, `2. [D][ ] submit /by 2025-03-01` +- **Outcome**: Reorders tasks (e.g., deadlines by date, todos alphabetically). + +## Searching Tasks + +### Finding Tasks +Search tasks by keyword. + +- **Command**: `find ` +- **Example**: `find report` +- **Outcome**: Lists matching tasks (e.g., `1. [D][ ] submit report (by: Mar 1 2025)`). + +### Tasks on a Date +View tasks due or occurring on a specific date. + +- **Command**: `on ` +- **Example**: `on 2025-03-01` +- **Outcome**: Shows relevant tasks (e.g., deadlines due on March 1, 2025). + +## Exiting + +Close the chatbot and save your tasks. + +- **Command**: `bye` +- **Example**: `bye` +- **Outcome**: Exits the app, saving tasks to `data/johan.txt`. + +## Notes + +- Task numbers are 1-based. +- Dates can be `YYYY-MM-DD` or `DD/MM/YYYY`. +- Commands are case-insensitive. \ No newline at end of file diff --git a/docs/Ui.png b/docs/Ui.png new file mode 100644 index 0000000000..62d05832b8 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/data/johan.txt b/src/main/java/data/johan.txt new file mode 100644 index 0000000000..6ebc238016 --- /dev/null +++ b/src/main/java/data/johan.txt @@ -0,0 +1,3 @@ +D | 1 | readbook | 2/12/2019 +E | 0 | try clothes | 10/2/2019 | 1/3/2020 +T | 0 | read book diff --git a/src/main/java/johan/DialogBox.java b/src/main/java/johan/DialogBox.java new file mode 100644 index 0000000000..4c31bd21d1 --- /dev/null +++ b/src/main/java/johan/DialogBox.java @@ -0,0 +1,78 @@ +package johan; + + +import java.io.IOException; +import java.util.Collections; + +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +/** + * Represents a dialog box with text and an image in the Johan GUI. + */ +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + /** + * Constructs a DialogBox with the specified text and image using FXML. + * + * @param text The text to display + * @param image The image to display + */ + private DialogBox(String text, Image image) { + 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(image); + } + + /** + * Flips the dialog box so the image is on the left and text on the right. + */ + private void flip() { + this.setAlignment(Pos.TOP_LEFT); + var children = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(children); // Use Collections.reverse() + this.getChildren().setAll(children); + } + + /** + * Creates a dialog box for a user message (image on right). + * + * @param text The user's message text + * @param image The user's avatar image + * @return A new DialogBox instance + */ + public static DialogBox getUserDialog(String text, Image image) { + return new DialogBox(text, image); + } + + /** + * Creates a dialog box for a Johan message (image on left). + * + * @param text Johan's message text + * @param image Johan's avatar image + * @return A new DialogBox instance + */ + public static DialogBox getJohanDialog(String text, Image image) { + DialogBox db = new DialogBox(text, image); + db.flip(); + return db; + } +} diff --git a/src/main/java/Duke.java b/src/main/java/johan/Duke.java similarity index 86% rename from src/main/java/Duke.java rename to src/main/java/johan/Duke.java index 5d313334cc..8177679984 100644 --- a/src/main/java/Duke.java +++ b/src/main/java/johan/Duke.java @@ -1,3 +1,8 @@ +package johan; + +/** + * Main class for Duke Program + */ public class Duke { public static void main(String[] args) { String logo = " ____ _ \n" diff --git a/src/main/java/johan/Johan.java b/src/main/java/johan/Johan.java new file mode 100644 index 0000000000..79feffebf3 --- /dev/null +++ b/src/main/java/johan/Johan.java @@ -0,0 +1,126 @@ +package johan; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.function.Consumer; + +import johan.command.Command; +import johan.parser.Parser; +import johan.storage.Storage; +import johan.task.Task; +import johan.task.TaskList; +import johan.ui.Ui; + + +// to run +// from repos dir >> javac -d bin src/main/java/johan/*.java src/main/java/johan/*/*.java +// java -cp bin johan.Johan + +/** + * Main class for the Johan task management application. + */ +public class Johan { + private final Storage storage; + private final TaskList tasks; + private final Ui ui; + private final Parser parser; + + /** + * Constructs a Johan instance with the specified storage file path. + * + * @param filePath The path to the storage file + */ + public Johan(String filePath) { + this.ui = new Ui(); + this.storage = new Storage(filePath); + this.parser = new Parser(); + this.tasks = loadTasks(); + } + /** + * Constructs a Johan instance with explicit dependencies (GUI mode). + * @param storage The storage instance + * @param tasks The task list instance + * @param parser The parser instance + */ + public Johan(Storage storage, TaskList tasks, Parser parser) { + assert storage != null : "Storage should not be null"; + assert tasks != null : "Task list should not be null"; + assert parser != null : "Parser should not be null"; + this.storage = storage; + this.tasks = tasks; + this.parser = parser; + this.ui = null; + } + private TaskList loadTasks() { + ArrayList loadedTasks; + try { + loadedTasks = storage.loadTasks(); + assert loadedTasks != null : "Task list should not be null"; + } catch (Exception e) { + ui.showError("Failed to load tasks: " + e.getMessage()); + loadedTasks = new ArrayList<>(); + } + return new TaskList(loadedTasks); + } + /** + * Runs the main application loop, processing user commands until exit. + */ + public void run() { + ui.showWelcome(); + boolean isExit = false; + while (!isExit) { + try { + String fullCommand = ui.readCommand(); + ui.showLine(); + Command command = parser.parse(fullCommand); + command.execute(tasks, ui, storage); + isExit = command.isExit(); + } catch (Exception e) { + ui.showError(e.getMessage()); + } finally { + ui.showLine(); + } + } + ui.showGoodbye(); + } + + /** + * Executes a user command and sends output to the provided consumer (GUI mode). + * @param input The user command string + * @param outputConsumer A consumer to handle output messages + * @throws Exception If the command execution fails + */ + public void executeCommand(String input, Consumer outputConsumer) throws Exception { + assert input != null && !input.isEmpty() : "Input should not be null"; + Ui guiUi = new Ui(outputConsumer); + Command command = parser.parse(input); + command.execute(tasks, guiUi, storage); + // TODO: Use streams to process tasks (e.g., filter) in future increments + } + /** + * Main entry point for the application. + * + * @param args Command-line arguments (unused) + */ + public static void main(String[] args) { + new Johan("./data/johan.txt").run(); + } + /** + * Parses a date string into a LocalDate object. + * + * @param dateStr The date string to parse + * @return The parsed LocalDate + */ + public static LocalDate parseDate(String dateStr) { + try { + DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("d/M/yyyy") + .withLocale(java.util.Locale.ENGLISH); + return LocalDate.parse(dateStr.trim(), formatter1); + } catch (Exception e) { + DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd") + .withLocale(java.util.Locale.ENGLISH); + return LocalDate.parse(dateStr.trim(), formatter2); + } + } +} diff --git a/src/main/java/johan/Main.java b/src/main/java/johan/Main.java new file mode 100644 index 0000000000..5b522d955c --- /dev/null +++ b/src/main/java/johan/Main.java @@ -0,0 +1,50 @@ +package johan; + +import java.io.IOException; +import java.util.ArrayList; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.AnchorPane; +import javafx.stage.Stage; +import johan.parser.Parser; +import johan.storage.Storage; +import johan.task.TaskList; + +/** + * The main JavaFX application class for Johan, providing a GUI interface using FXML. + */ +public class Main extends Application { + private Johan johan; + + @Override + public void start(Stage stage) { + try { + Storage storage = new Storage("./data/johan.txt"); + Parser parser = new Parser(); + ArrayList loadedTasks; + try { + loadedTasks = storage.loadTasks(); + } catch (Exception e) { + loadedTasks = new ArrayList<>(); + } + TaskList tasks = new TaskList(loadedTasks); + this.johan = new Johan(storage, tasks, parser); + + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = fxmlLoader.load(); + Scene scene = new Scene(ap); + stage.setTitle("Johan Chatbot"); + stage.setResizable(false); + stage.setScene(scene); + + MainWindow controller = fxmlLoader.getController(); + controller.setJohan(johan); + + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/johan/MainWindow.java b/src/main/java/johan/MainWindow.java new file mode 100644 index 0000000000..d103c6934e --- /dev/null +++ b/src/main/java/johan/MainWindow.java @@ -0,0 +1,63 @@ +package johan; + +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.VBox; + +/** + * Controller for the main GUI window in Johan. + */ +public class MainWindow { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private Johan johan; + private Image userImage = new Image(getClass().getResourceAsStream("/images/User.png")); + private Image johanImage = new Image(getClass().getResourceAsStream("/images/Johan.png")); + + /** + * Initializes the controller after FXML is loaded. + */ + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + dialogContainer.getChildren() + .add(DialogBox.getJohanDialog("Hello! I'm Johan. What can I do for you?", johanImage)); + } + + /** + * Sets the Johan instance for this controller. + * + * @param j The Johan instance to use + */ + public void setJohan(Johan j) { + this.johan = j; + } + + @FXML + private void handleUserInput() { + String input = userInput.getText().trim(); + if (!input.isEmpty()) { + dialogContainer.getChildren().add(DialogBox.getUserDialog(input, userImage)); + try { + johan.executeCommand(input, message -> dialogContainer.getChildren().add( + DialogBox.getJohanDialog(message, johanImage))); + if (input.equals("bye")) { + javafx.application.Platform.exit(); + } + } catch (Exception e) { + dialogContainer.getChildren().add(DialogBox.getJohanDialog("Oops! " + e.getMessage(), johanImage)); + } + userInput.clear(); + } + } +} diff --git a/src/main/java/johan/command/AddCommand.java b/src/main/java/johan/command/AddCommand.java new file mode 100644 index 0000000000..98b4a1ad44 --- /dev/null +++ b/src/main/java/johan/command/AddCommand.java @@ -0,0 +1,36 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Abstract base class for commands that add tasks to the task list. + */ +public abstract class AddCommand extends Command { + protected final String description; + + /** + * Constructs an AddCommand with the specified task description. + * + * @param description the description of the task to be added + */ + public AddCommand(String description) { + this.description = description; + } + + /** + * Executes the command to add a task to the task list. + * + * @param tasks The task list to modify + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + * @throws Exception If an error occurs during exception + */ + @Override + public abstract void execute(TaskList tasks, Ui ui, Storage storage) throws Exception; +} + + + + diff --git a/src/main/java/johan/command/Command.java b/src/main/java/johan/command/Command.java new file mode 100644 index 0000000000..2d404ccafb --- /dev/null +++ b/src/main/java/johan/command/Command.java @@ -0,0 +1,27 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Abstract base class for all commands in the application + */ +public abstract class Command { + /** + * Executes the command with the given task list, UI, and storage. + * @param tasks The task list to operate on + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + * @throws Exception If an error occurs during execution + */ + public abstract void execute(TaskList tasks, Ui ui, Storage storage) throws Exception; + + /** + * Indicates whether this command should terminate the application. + * @return true if this is an exit command, false otherwise + */ + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/johan/command/DeadlineCommand.java b/src/main/java/johan/command/DeadlineCommand.java new file mode 100644 index 0000000000..485743905c --- /dev/null +++ b/src/main/java/johan/command/DeadlineCommand.java @@ -0,0 +1,38 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.Deadline; +import johan.task.Task; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Command to add a deadline task to the task list. + */ +public class DeadlineCommand extends AddCommand { + private final String by; + + /** + * Constructs a DeadlineCommand with the specified description and deadline. + * @param description The description of the deadline task + * @param by The deadline date/time string + */ + public DeadlineCommand(String description, String by) { + super(description); + this.by = by; + } + + /** + * Adds a deadline task to the task list and updates storage. + * @param tasks The task list to modify + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + */ + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + Task task = new Deadline(description, by); + tasks.addTask(task); + ui.showTaskAdded(task, tasks.size()); + storage.saveTasks(tasks.getTasks()); + } +} diff --git a/src/main/java/johan/command/DeleteCommand.java b/src/main/java/johan/command/DeleteCommand.java new file mode 100644 index 0000000000..b18ab67a45 --- /dev/null +++ b/src/main/java/johan/command/DeleteCommand.java @@ -0,0 +1,34 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.Task; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Command to delete a task from the task list. + */ +public class DeleteCommand extends Command { + private final int taskIndex; + + /** + * Constructs a DeleteCommand for the specified task index + * @param taskIndex The zero-based index of the task to delete + */ + public DeleteCommand(int taskIndex) { + this.taskIndex = taskIndex; + } + + /** + * Deletes the task at the specified index and updates storage. + * @param tasks The task list to operate on + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + */ + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + Task task = tasks.deleteTask(taskIndex); + ui.showTaskDeleted(task, tasks.size()); + storage.saveTasks(tasks.getTasks()); + } +} diff --git a/src/main/java/johan/command/EventCommand.java b/src/main/java/johan/command/EventCommand.java new file mode 100644 index 0000000000..9af3cb5ab4 --- /dev/null +++ b/src/main/java/johan/command/EventCommand.java @@ -0,0 +1,41 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.Event; +import johan.task.Task; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Command to add an event task to the task list. + */ +public class EventCommand extends AddCommand { + private final String from; + private final String to; + + /** + * Constructs an EventCommand with the specified description and time range. + * @param description The description of the event + * @param from The start date/time string + * @param to The end date/time string + */ + public EventCommand(String description, String from, String to) { + super(description); + this.from = from; + this.to = to; + } + + /** + * Adds an event task to the task list and updates storage. + * @param tasks The task list to modify + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + */ + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + Task task = new Event(description, from, to); + tasks.addTask(task); + ui.showTaskAdded(task, tasks.size()); + storage.saveTasks(tasks.getTasks()); + } +} diff --git a/src/main/java/johan/command/ExitCommand.java b/src/main/java/johan/command/ExitCommand.java new file mode 100644 index 0000000000..feac4bcc5e --- /dev/null +++ b/src/main/java/johan/command/ExitCommand.java @@ -0,0 +1,30 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Command to exit the application. + */ +public class ExitCommand extends Command { + /** + * Performs no action as termination is handled by isExit(). + * @param tasks The task list to operate on + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + */ + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + // No action needed; isExit() handles termination + } + + /** + * Indicates that this command terminates the application. + * @return true always + */ + @Override + public boolean isExit() { + return true; + } +} diff --git a/src/main/java/johan/command/FindCommand.java b/src/main/java/johan/command/FindCommand.java new file mode 100644 index 0000000000..a942e19d61 --- /dev/null +++ b/src/main/java/johan/command/FindCommand.java @@ -0,0 +1,43 @@ +package johan.command; + +import java.util.ArrayList; + +import johan.storage.Storage; +import johan.task.Task; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Command to find tasks containing a specific keyword in their description. + */ +public class FindCommand extends Command { + private final String keyword; + + /** + * Constructs a FindCommand with the specified search keyword. + * + * @param keyword The keyword to search for in task descriptions + */ + public FindCommand(String keyword) { + this.keyword = keyword; + } + + /** + * Executes the command to find and display tasks matching the keyword. + * + * @param tasks The task list to search + * @param ui The user interface for displaying results + * @param storage The storage system (unused in this command) + */ + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + ArrayList matchingTasks = new ArrayList<>(); + for (int i = 0; i < tasks.size(); i++) { + Task task = tasks.getTask(i); + if (task.getDescription().toLowerCase().contains(this.keyword.toLowerCase())) { + matchingTasks.add(task); + } + } + ui.showFoundTasks(matchingTasks); + } +} diff --git a/src/main/java/johan/command/ListCommand.java b/src/main/java/johan/command/ListCommand.java new file mode 100644 index 0000000000..c7dd5ca591 --- /dev/null +++ b/src/main/java/johan/command/ListCommand.java @@ -0,0 +1,21 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Command to list all tasks in the task list. + */ +public class ListCommand extends Command { + /** + * Displays all tasks in the task list. + * @param tasks The task list to display + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + */ + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + ui.showTaskList(tasks); + } +} diff --git a/src/main/java/johan/command/MarkCommand.java b/src/main/java/johan/command/MarkCommand.java new file mode 100644 index 0000000000..6c31110aec --- /dev/null +++ b/src/main/java/johan/command/MarkCommand.java @@ -0,0 +1,43 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.Task; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Command to mark a task as done or not done. + */ +public class MarkCommand extends Command { + private final int taskIndex; + private final boolean markAsDone; + + /** + * Constructs a MarkCommand for the specified task and mark status. + * @param taskIndex The zero-based index of the task to mark + * @param markAsDone True to mark as done, false to mark as not done + */ + public MarkCommand(int taskIndex, boolean markAsDone) { + this.taskIndex = taskIndex; + this.markAsDone = markAsDone; + } + + /** + * Marks the specified task and updates storage. + * @param tasks The task list to operate on + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + */ + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + Task task = tasks.getTask(taskIndex); + if (markAsDone) { + task.markAsDone(); + ui.showTaskMarked(task, true); + } else { + task.markAsNotDone(); + ui.showTaskMarked(task, false); + } + storage.saveTasks(tasks.getTasks()); + } +} diff --git a/src/main/java/johan/command/OnDateCommand.java b/src/main/java/johan/command/OnDateCommand.java new file mode 100644 index 0000000000..a63a1ee10f --- /dev/null +++ b/src/main/java/johan/command/OnDateCommand.java @@ -0,0 +1,34 @@ +package johan.command; + +import java.time.LocalDate; + +import johan.Johan; +import johan.storage.Storage; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Command to show tasks occurring on a specific date. + */ +public class OnDateCommand extends Command { + private final LocalDate targetDate; + + /** + * Constructs an OnDateCommand for the specified date string. + * @param dateStr The date string to parse + */ + public OnDateCommand(String dateStr) { + this.targetDate = Johan.parseDate(dateStr); + } + + /** + * Displays all tasks occurring on the target date. + * @param tasks The task list to operate on + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + */ + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + ui.showTasksOnDate(tasks, targetDate); + } +} diff --git a/src/main/java/johan/command/SortCommand.java b/src/main/java/johan/command/SortCommand.java new file mode 100644 index 0000000000..55a1c37ff1 --- /dev/null +++ b/src/main/java/johan/command/SortCommand.java @@ -0,0 +1,17 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.TaskList; +import johan.ui.Ui; + +/** + * Command to sort tasks in the task list. + */ +public class SortCommand extends Command { + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + tasks.sort(); + ui.showMessage("Tasks sorted: deadlines by date, others by name."); + ui.showTaskList(tasks); + } +} diff --git a/src/main/java/johan/command/TodoCommand.java b/src/main/java/johan/command/TodoCommand.java new file mode 100644 index 0000000000..ccfa543932 --- /dev/null +++ b/src/main/java/johan/command/TodoCommand.java @@ -0,0 +1,38 @@ +package johan.command; + +import johan.storage.Storage; +import johan.task.Task; +import johan.task.TaskList; +import johan.task.Todo; +import johan.ui.Ui; + +/** + * Command to add a todo task to the task list + */ +public class TodoCommand extends AddCommand { + /** + * Constructs a TodoCommand with the specified description. + * @param description The description of the todo task + */ + public TodoCommand(String description) { + super(description); + } + + /** + * Adds a todo task to the task list and updates storage. + * @param tasks The task list to modify + * @param ui The user interface for displaying output + * @param storage The storage system for persisting tasks + * @throws IllegalArgumentException If the description is empty + */ + @Override + public void execute(TaskList tasks, Ui ui, Storage storage) { + if (description.isEmpty()) { + throw new IllegalArgumentException("The description of a todo cannot be empty."); + } + Task task = new Todo(description); + tasks.addTask(task); + ui.showTaskAdded(task, tasks.size()); + storage.saveTasks(tasks.getTasks()); + } +} diff --git a/src/main/java/johan/launcher/Launcher.java b/src/main/java/johan/launcher/Launcher.java new file mode 100644 index 0000000000..68267c77f5 --- /dev/null +++ b/src/main/java/johan/launcher/Launcher.java @@ -0,0 +1,13 @@ +package johan.launcher; + +import javafx.application.Application; +import johan.Main; + +/** + * A launcher class to workaround classpath issues with JavaFX. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Main.class, args); + } +} diff --git a/src/main/java/johan/parser/Parser.java b/src/main/java/johan/parser/Parser.java new file mode 100644 index 0000000000..6258653498 --- /dev/null +++ b/src/main/java/johan/parser/Parser.java @@ -0,0 +1,77 @@ +package johan.parser; + +import johan.command.Command; +import johan.command.DeadlineCommand; +import johan.command.DeleteCommand; +import johan.command.EventCommand; +import johan.command.ExitCommand; +import johan.command.FindCommand; +import johan.command.ListCommand; +import johan.command.MarkCommand; +import johan.command.OnDateCommand; +import johan.command.SortCommand; +import johan.command.TodoCommand; + +/** + * Parses user input into executable commands. + */ +public class Parser { + /** + * Parses the input string into a corresponding Command object. + * + * @param input The user input string to parse + * @return The corresponding Command object + * @throws Exception If the input cannot be parsed + */ + public Command parse(String input) throws Exception { + if (input.equals("bye")) { + return new ExitCommand(); + } else if (input.equals("list")) { + return new ListCommand(); + } else if (input.startsWith("mark ")) { + int id = Integer.parseInt(input.substring(5)) - 1; + return new MarkCommand(id, true); + } else if (input.startsWith("unmark ")) { + int id = Integer.parseInt(input.substring(7)) - 1; + return new MarkCommand(id, false); + } else if (input.startsWith("todo ")) { + String desc = input.substring(5).trim(); + return new TodoCommand(desc); + } else if (input.startsWith("deadline ")) { + int byIndex = input.indexOf("/by"); + if (byIndex == -1) { + throw new IllegalArgumentException("Please specify a deadline with /by."); + } + String desc = input.substring(9, byIndex).trim(); + String by = input.substring(byIndex + 4).trim(); + return new DeadlineCommand(desc, by); + } else if (input.startsWith("event ")) { + int fromIndex = input.indexOf("/from"); + int toIndex = input.indexOf("/to"); + if (fromIndex == -1 || toIndex == -1) { + throw new IllegalArgumentException("Please specify /from and /to."); + } + String desc = input.substring(6, fromIndex).trim(); + String from = input.substring(fromIndex + 6, toIndex).trim(); + String to = input.substring(toIndex + 4).trim(); + return new EventCommand(desc, from, to); + } else if (input.startsWith("delete ")) { + int id = Integer.parseInt(input.substring(7)) - 1; + return new DeleteCommand(id); + } else if (input.startsWith("on ")) { + String dateStr = input.substring(3).trim(); + return new OnDateCommand(dateStr); + } else if (input.startsWith("find ")) { + String keyword = input.substring(5).trim(); + if (keyword.isEmpty()) { + throw new IllegalArgumentException("Please specify a keyword."); + } + return new FindCommand(keyword); + } else if (input.startsWith("sort")) { + return new SortCommand(); + } else { + throw new IllegalArgumentException("I'm sorry, but I don't know what that means :-("); + } + } +} + diff --git a/src/main/java/johan/storage/Storage.java b/src/main/java/johan/storage/Storage.java new file mode 100644 index 0000000000..885c67d6f6 --- /dev/null +++ b/src/main/java/johan/storage/Storage.java @@ -0,0 +1,152 @@ +package johan.storage; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +import johan.task.Deadline; +import johan.task.Event; +import johan.task.Task; +import johan.task.Todo; + + +/** + * Handles storage and retrieval of tasks to/from a file + */ +public class Storage { + private final String filePath; + + /** + * Constructs a Storage instance with the specified file path. + * @param filePath The path to the storage file + */ + public Storage(String filePath) { + this.filePath = filePath; + } + + /** + * Saves the task list to the storage file. + * @param tasks The list of tasks to save + */ + public void saveTasks(ArrayList tasks) { + File file = new File(filePath); + File directory = new File(file.getParent()); + + if (!directory.exists()) { + directory.mkdirs(); // Ensure directory exists + } + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + for (Task task : tasks) { + writer.write(formatTaskForSaving(task)); + writer.newLine(); + } + } catch (IOException e) { + System.out.println("Error saving tasks: " + e.getMessage()); + } + } + + /** + * Loads tasks from the storage files. + * @return The list of loaded tasks + */ + public ArrayList loadTasks() { + ArrayList tasks = new ArrayList<>(); + File file = new File(filePath); + + if (!file.exists()) { + System.out.println("No saved tasks found. Starting fresh."); + return tasks; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + // DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern("d/M/yyyy HHmm"); + while ((line = reader.readLine()) != null) { + String[] parts = line.split(" \\| "); + if (parts.length < 3) { + continue; + } + + String type = parts[0]; + boolean isDone = parts[1].equals("1"); + String description = parts[2]; + + Task task; + if (type.equals("T")) { + task = new Todo(description); + } else if (type.equals("D") && parts.length == 4) { + try { + // LocalDate deadline = LocalDate.parse(parts[3], inputFormatter); + task = new Deadline(description, parts[3]); + } catch (Exception e) { + System.out.println("Error parsing deadline for task: " + description + ". Skipping..."); + continue; + } + } else if (type.equals("E") && parts.length == 5) { + try { + // LocalDate from = LocalDate.parse(parts[3], inputFormatter); + // LocalDate to = LocalDate.parse(parts[4], inputFormatter); + task = new Event(description, parts[3], parts[4]); + } catch (Exception e) { + System.out.println("Error parsing deadline for task: " + description + ". Skipping..."); + continue; + } + } else { + continue; // Ignore invalid entries + } + + if (isDone) { + task.markAsDone(); + } + + tasks.add(task); + } + } catch (IOException e) { + System.out.println("Error loading tasks: " + e.getMessage()); + } + + return tasks; + } + + /** + * Formats a task for saving to the file. + * @param task The task to format + * @return The formatted string representation + */ + private String formatTaskForSaving(Task task) { + String type; + if (task instanceof Todo) { + type = "T"; + } else if (task instanceof Deadline) { + type = "D"; + } else if (task instanceof Event) { + type = "E"; + } else { + return ""; + } + + String status = task.isDone() ? "1" : "0"; + String description = task.getDescription(); + + if (task instanceof Deadline) { + LocalDate deadline = ((Deadline) task).getBy(); + return type + " | " + status + " | " + description + " | " + + deadline.format(DateTimeFormatter.ofPattern("d/M/yyyy")); + } else if (task instanceof Event) { + LocalDate startDate = ((Event) task).getStartDate(); + LocalDate endDate = ((Event) task).getEndDate(); + return type + " | " + status + " | " + description + " | " + + startDate.format(DateTimeFormatter.ofPattern("d/M/yyyy")) + " | " + + endDate.format(DateTimeFormatter.ofPattern("d/M/yyyy")); + } else { + return type + " | " + status + " | " + description; + } + } +} diff --git a/src/main/java/johan/task/Deadline.java b/src/main/java/johan/task/Deadline.java new file mode 100644 index 0000000000..3c910086bb --- /dev/null +++ b/src/main/java/johan/task/Deadline.java @@ -0,0 +1,62 @@ +package johan.task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Represents a task with a deadline. + */ +public class Deadline extends Task { + + protected LocalDate by; + + /** + * Constructs a Deadline task with the specified description and deadline. + * @param description The task description + * @param by The deadline date string + */ + public Deadline(String description, String by) { + super(description); + // this.by = by; + // DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern("d/M/yyyy"); + this.by = parseDate(by); + } + + /** + * Parses a date string into a LocalDate object. + * @param dateString The date string to parse + * @return The parsed LocalDate + */ + private static LocalDate parseDate(String dateString) { + try { + DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("d/M/yyyy").withLocale(java.util.Locale.ENGLISH); + return LocalDate.parse(dateString.trim(), formatter1); + } catch (Exception e) { + DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd") + .withLocale(java.util.Locale.ENGLISH); + return LocalDate.parse(dateString.trim(), formatter2); + } + } + + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + by.format(DateTimeFormatter.ofPattern("MMM dd yyyy")) + ")"; + } + + public LocalDate getBy() { + return by; + } + /** + * Compares deadlines chronologically by due date. + * + * @param other The other task to compare to + * @return Negative if this deadline is earlier, positive if later, zero if same + */ + @Override + public int compareTo(Task other) { + if (other instanceof Deadline) { + return this.by.compareTo(((Deadline) other).by); + } + return super.compareTo(other); + } +} diff --git a/src/main/java/johan/task/Event.java b/src/main/java/johan/task/Event.java new file mode 100644 index 0000000000..acd2bbe199 --- /dev/null +++ b/src/main/java/johan/task/Event.java @@ -0,0 +1,72 @@ +package johan.task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Represents a task with a start and end date/time. + */ +public class Event extends Task { + + protected LocalDate startDate; + protected LocalDate endDate; + + /** + * Constructs an Event task with the specified description and time range. + * @param description The task description + * @param startDate The start date string + * @param endDate The end date string + */ + public Event(String description, String startDate, String endDate) { + super(description); + // DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern("d/M/yyyy"); + // this.startDate = LocalDate.parse(startDate, inputFormatter); + // this.endDate = LocalDate.parse(endDate, inputFormatter); + this.startDate = parseDate(startDate); + this.endDate = parseDate(endDate); + } + + /** + * Parses a date string into a LocalDate object. + * @param dateString The date string to parse + * @return The parsed LocalDate + */ + private static LocalDate parseDate(String dateString) { + try { + DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("d/M/yyyy") + .withLocale(java.util.Locale.ENGLISH); + return LocalDate.parse(dateString.trim(), formatter1); + } catch (Exception e) { + DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd") + .withLocale(java.util.Locale.ENGLISH); + return LocalDate.parse(dateString.trim(), formatter2); + } + } + + /** + * Returns a string representation of the event task. + * @return The formatted string + */ + @Override + public String toString() { + return "[E]" + super.toString() + + " (from: " + startDate.format(DateTimeFormatter.ofPattern("MMM dd yyyy")) + + " to: " + endDate.format(DateTimeFormatter.ofPattern("MMM dd yyyy")) + ")"; + } + + /** + * Gets the start date of the event. + * @return The start date as a LocalDate + */ + public LocalDate getStartDate() { + return startDate; + } + + /** + * Gets the end date of the event. + * @return The end date as a LocalDate + */ + public LocalDate getEndDate() { + return endDate; + } +} diff --git a/src/main/java/johan/task/Task.java b/src/main/java/johan/task/Task.java new file mode 100644 index 0000000000..6aeb305a23 --- /dev/null +++ b/src/main/java/johan/task/Task.java @@ -0,0 +1,109 @@ +package johan.task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Abstract base class representing a task in the application. + */ +public abstract class Task implements Comparable { + private static int nextTaskID = 1; + protected String description; + protected boolean isDone; + protected LocalDate deadline; + private final int id; + /** + * Constructs a Task with the specified description. + * @param description The task description + */ + public Task(String description) { + this.description = description; + this.isDone = false; + this.id = nextTaskID++; + this.deadline = null; + } + + /** + * Gets the unique ID of the task as a string. + * + * @return The task ID + */ + public String getID() { + return Integer.toString(this.id); + } + /** + * Gets the status icon representing whether the task is done. + * + * @return "X" if done, " " if not done + */ + public String getStatusIcon() { + return (isDone ? "X" : " "); // mark done task with X + } + /** + * Marks the task as completed. + */ + public void markAsDone() { + isDone = true; + } + /** + * Marks the task as not completed. + */ + public void markAsNotDone() { + isDone = false; + } + /** + * Gets the deadline of the task, if any. + * + * @return The deadline date, or null if not set + */ + public LocalDate getDeadline() { + return deadline; + } + /** + * Sets the deadline for the task. + * + * @param deadline The deadline date to set + */ + public void setDeadline(LocalDate deadline) { + this.deadline = deadline; + } + /** + * Checks if the task is marked as done. + * + * @return true if the task is done, false otherwise + */ + public boolean isDone() { + return isDone; + } + /** + * Gets the description of the task. + * + * @return The task description + */ + public String getDescription() { + return description; + } + /** + * Returns a string representation of the task. + * + * @return The formatted string including status and deadline if present + */ + @Override + public String toString() { + String baseString = "[" + this.getStatusIcon() + "] " + description; + if (deadline != null) { + baseString += "(by: " + deadline.format(DateTimeFormatter.ofPattern("MMM dd yyyy")) + ")"; + } + return baseString; + } + /** + * Compares tasks alphabetically by description as a default. + * + * @param other The other task to compare to + * @return Negative if this task comes before, positive if after, zero if equal + */ + @Override + public int compareTo(Task other) { + return this.description.compareTo(other.description); + } +} diff --git a/src/main/java/johan/task/TaskList.java b/src/main/java/johan/task/TaskList.java new file mode 100644 index 0000000000..a820e38645 --- /dev/null +++ b/src/main/java/johan/task/TaskList.java @@ -0,0 +1,75 @@ +package johan.task; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Manages a list of tasks. + */ +public class TaskList { + private final ArrayList tasks; + /** + * Constructs a TaskList with the specified initial tasks. + * + * @param tasks The initial list of tasks + */ + public TaskList(ArrayList tasks) { + this.tasks = tasks; + } + /** + * Adds a task to the list. + * + * @param task The task to add + */ + public void addTask(Task task) { + tasks.add(task); + } + /** + * Deletes a task at the specified index. + * + * @param index The zero-based index of the task to delete + * @return The deleted task + * @throws IllegalArgumentException If the index is invalid + */ + public Task deleteTask(int index) { + if (index >= 0 && index < tasks.size()) { + return tasks.remove(index); + } + throw new IllegalArgumentException("Invalid task index."); + } + /** + * Gets the task at the specified index. + * + * @param index The zero-based index of the task to retrieve + * @return The task at the specified index + * @throws IllegalArgumentException If the index is invalid + */ + public Task getTask(int index) { + if (index >= 0 && index < tasks.size()) { + return tasks.get(index); + } + throw new IllegalArgumentException("Invalid task index."); + } + /** + * Gets the complete list of tasks. + * + * @return The ArrayList containing all tasks + */ + public ArrayList getTasks() { + return tasks; + } + /** + * Gets the number of tasks in the list. + * + * @return The size of the task list + */ + public int size() { + return tasks.size(); + } + /** + * Sorts the task list chronologically for deadlines, alphabetically otherwise. + */ + public void sort() { + Collections.sort(tasks); + } +} diff --git a/src/main/java/johan/task/Todo.java b/src/main/java/johan/task/Todo.java new file mode 100644 index 0000000000..5240865f85 --- /dev/null +++ b/src/main/java/johan/task/Todo.java @@ -0,0 +1,23 @@ +package johan.task; +/** + * Represents a simple todo task without a deadline. + */ +public class Todo extends Task { + /** + * Constructs a Todo task with the specified description. + * + * @param description The task description + */ + public Todo(String description) { + super(description); + } + /** + * Returns a string representation of the todo task. + * + * @return The formatted string with "[T]" prefix + */ + @Override + public String toString() { + return "[T]" + super.toString(); + } +} diff --git a/src/main/java/johan/ui/Ui.java b/src/main/java/johan/ui/Ui.java new file mode 100644 index 0000000000..ad376da988 --- /dev/null +++ b/src/main/java/johan/ui/Ui.java @@ -0,0 +1,172 @@ +package johan.ui; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Scanner; +import java.util.function.Consumer; + +import johan.task.Deadline; +import johan.task.Event; +import johan.task.Task; +import johan.task.TaskList; + +/** + * Handles user interface interactions for the Johan task management application. + */ +public class Ui { + private final Scanner scanner = new Scanner(System.in); + private final Consumer outputConsumer; + + public Ui() { + this.outputConsumer = System.out::println; + } + public Ui(Consumer outputConsumer) { + this.outputConsumer = outputConsumer; + } + /** + * Displays a welcome message to the user. + */ + public void showWelcome() { + outputConsumer.accept("Hello! I'm johan.Johan"); + outputConsumer.accept("What can I do for you?"); + outputConsumer.accept("Type 'list' or 'sort' to get started!"); + } + /** + * Displays a goodbye message to the user. + */ + public void showGoodbye() { + outputConsumer.accept("Bye. Hope to see you again soon!"); + } + /** + * Displays the list of tasks in the provided TaskList. + * + * @param tasks The TaskList containing tasks to display + */ + public void showTaskList(TaskList tasks) { + outputConsumer.accept("Here are the tasks in your list:"); + for (int i = 0; i < tasks.size(); i++) { + outputConsumer.accept((i + 1) + "." + tasks.getTask(i).toString()); + } + } + /** + * Displays a confirmation message for a newly added task. + * + * @param task The task that was added + * @param taskCount The total number of tasks in the list after adding + */ + public void showTaskAdded(Task task, int taskCount) { + outputConsumer.accept("Got it. I've added this task:"); + outputConsumer.accept(task.toString()); + outputConsumer.accept("Now you have " + taskCount + " tasks in the list."); + } + /** + * Displays a confirmation message when a task's completion status is changed. + * + * @param task The task whose status was updated + * @param isDone True if the task was marked as done, false if marked as not done + */ + public void showTaskMarked(Task task, boolean isDone) { + if (isDone) { + outputConsumer.accept("Nice! I've marked this task as done:"); + } else { + outputConsumer.accept("OK, I've marked this task as not done yet:"); + } + outputConsumer.accept(task.toString()); + } + /** + * Displays a confirmation message when a task is deleted. + * + * @param task The task that was removed + * @param taskCount The total number of tasks remaining in the list + */ + public void showTaskDeleted(Task task, int taskCount) { + outputConsumer.accept("____________________________________________________________"); + outputConsumer.accept("Noted. I've removed this task:"); + outputConsumer.accept(task.toString()); + outputConsumer.accept("Now you have " + taskCount + " tasks in the list."); + outputConsumer.accept("____________________________________________________________"); + } + /** + * Displays an error message to the user. + * + * @param message The error message to display + */ + public void showError(String message) { + outputConsumer.accept("____________________________________________________________"); + outputConsumer.accept(" OOPS!!! " + message); + outputConsumer.accept("____________________________________________________________"); + } + /** + * Reads a command from the user input. + * + * @return The user's command as a lowercase, trimmed string + */ + public String readCommand() { + return scanner.nextLine().toLowerCase().trim(); + } + /** + * Displays a horizontal line as a visual separator. + */ + public void showLine() { + outputConsumer.accept("____________________________________________________________"); + } + /** + * Displays tasks occurring on the specified date from the TaskList. + * + * @param tasks The TaskList to search for tasks + * @param targetDate The date to filter tasks by + */ + public void showTasksOnDate(TaskList tasks, LocalDate targetDate) { + outputConsumer.accept("Tasks on " + targetDate.format(DateTimeFormatter.ofPattern("d/MM/yyyy")) + ":"); + boolean found = false; + for (int i = 0; i < tasks.size(); i++) { + Task task = tasks.getTask(i); + if (task instanceof Deadline) { + LocalDate deadline = ((Deadline) task).getBy(); + if (deadline != null && deadline.equals(targetDate)) { + outputConsumer.accept((i + 1) + "." + task.toString()); + found = true; + } + } else if (task instanceof Event) { + LocalDate startDate = ((Event) task).getStartDate(); + LocalDate endDate = ((Event) task).getEndDate(); + if (startDate != null && endDate != null && !startDate.isAfter(targetDate) + && !endDate.isBefore(targetDate)) { + outputConsumer.accept((i + 1) + "." + task.toString()); + found = true; + } + } + } + if (!found) { + outputConsumer.accept("No tasks found on this date."); + } + } + + /** + * Displays tasks whose descriptions contain the search keyword. + * + * @param matchingTasks The list of tasks that match the keyword + */ + public void showFoundTasks(ArrayList matchingTasks) { + System.out.println("____________________________________________________________"); + System.out.println(" Here are the matching tasks in your list:"); + if (matchingTasks.isEmpty()) { + System.out.println(" No matching tasks found."); + } else { + for (int i = 0; i < matchingTasks.size(); i++) { + System.out.println(" " + (i + 1) + "." + matchingTasks.get(i).toString()); + } + } + System.out.println("____________________________________________________________"); + } + /** + * Displays a generic message to the user. + * + * @param message The message to display + */ + public void showMessage(String message) { + outputConsumer.accept(message); + } +} + diff --git a/src/main/resources/images/Johan.png b/src/main/resources/images/Johan.png new file mode 100644 index 0000000000..a1954acf6d Binary files /dev/null and b/src/main/resources/images/Johan.png differ diff --git a/src/main/resources/images/User.png b/src/main/resources/images/User.png new file mode 100644 index 0000000000..76c6643ee2 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..a855ddb7f8 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml new file mode 100644 index 0000000000..1eb272351e --- /dev/null +++ b/src/main/resources/view/MainWindow.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + +