+ * NOTE: This method is called on the JavaFX Application Thread. + *
+ * + * @param stage the primary stage for this application, onto which + * the application scene can be set. + * Applications may create other stages, if needed, but they will not be + * primary stages. + */ + @Override + public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + VBox rootLayout = fxmlLoader.load(); + Scene scene = new Scene(rootLayout); + stage.setScene(scene); + MainWindow mainWindowController = fxmlLoader.getController(); + changooseBot = new Duke(); + mainWindowController.setDuke(changooseBot); + String startUpMessage = changooseBot.initStorage(); + mainWindowController.addMessageFromDuke(startUpMessage); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/duke/MainWindow.java b/src/main/java/duke/MainWindow.java new file mode 100644 index 0000000000..117e2db8bb --- /dev/null +++ b/src/main/java/duke/MainWindow.java @@ -0,0 +1,63 @@ +package duke; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +/** + * Controller for MainWindow. Provides the layout for the other controls. + */ +public class MainWindow extends AnchorPane { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private Duke duke; + + private Image userImage = new Image(this.getClass().getResourceAsStream("/images/DaUser.png")); + private Image dukeImage = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png")); + + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + } + + public void setDuke(Duke d) { + duke = d; + } + + /** + * Creates one dialog box that echoes a message from Duke, and then appends it to the dialog container. + * + * @param message The message to be displayed by Duke. + */ + public void addMessageFromDuke(String message) { + dialogContainer.getChildren().add( + DialogBox.getDukeDialog(message, dukeImage) + ); + } + + /** + * Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + String response = duke.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, userImage), + DialogBox.getDukeDialog(response, dukeImage) + ); + userInput.clear(); + } +} diff --git a/src/main/java/duke/commands/AddTaskCommand.java b/src/main/java/duke/commands/AddTaskCommand.java new file mode 100644 index 0000000000..7b62cd833b --- /dev/null +++ b/src/main/java/duke/commands/AddTaskCommand.java @@ -0,0 +1,56 @@ +package duke.commands; + +import java.util.List; + +import duke.Duke; +import duke.exception.DukeStorageException; +import duke.exception.TaskParseException; +import duke.exception.TimeUtilException; +import duke.service.TaskFactory; +import duke.service.UiService; +import duke.tasks.Task; + +/** + * Represents a command to add a task in the Duke application. + */ +public class AddTaskCommand extends Command { + private final String taskType; + private final List+ * All commands that are intended to be executed in Duke should extend this class. + *
+ */ +public abstract class Command { + protected Duke dukeBot; + protected UiService uiService; + + /** + * Constructs a Command with the given Duke instance and UI service. + * + * @param dukeBot The main Duke instance. + * @param uiService The UI service for interactions. + */ + protected Command(Duke dukeBot, UiService uiService) { + this.dukeBot = dukeBot; + this.uiService = uiService; + } + + /** + * Executes the command. + * + * @return a String describing the output of the Command. + */ + public abstract String execute(); + + /** + * Indicates if the command should cause the program to exit. + * + * @return False by default; should be overridden by commands that cause program exit. + */ + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/duke/commands/DeleteTaskCommand.java b/src/main/java/duke/commands/DeleteTaskCommand.java new file mode 100644 index 0000000000..b6d3fe237a --- /dev/null +++ b/src/main/java/duke/commands/DeleteTaskCommand.java @@ -0,0 +1,44 @@ +package duke.commands; + +import java.util.Optional; + +import duke.Duke; +import duke.exception.DukeStorageException; +import duke.service.UiService; +import duke.tasks.Task; + +/** + * Represents a command to delete a task from the Duke application. + */ +public class DeleteTaskCommand extends Command { + private final int taskId; + + /** + * Constructs a DeleteTaskCommand. + * + * @param dukeBot The main Duke instance. + * @param uiService The UI service for interactions. + * @param taskId The ID of the task to be deleted. + */ + public DeleteTaskCommand(Duke dukeBot, UiService uiService, int taskId) { + super(dukeBot, uiService); + this.taskId = taskId; + } + + /** + * Executes the command to delete a task. + * + * @return A string representing the status of the task deletion. + * It either confirms the task deletion or details any errors encountered. + */ + @Override + public String execute() { + try { + Optional+ * This service is responsible for continuously reading commands from the CLI, parsing them into + * their respective components, and then executing the associated commands using a {@link CommandFactory}. + *
+ */ +public class CliParserService { + private final UiService uiService; + private final CommandFactory commandFactory; + + /** + * Constructs a new instance of the CLI parser service. + * + * @param uiService The UI service to use for interactions with the user. + * @param commandFactory The command factory to use for creating command objects. + */ + public CliParserService(UiService uiService, CommandFactory commandFactory) { + this.uiService = uiService; + this.commandFactory = commandFactory; + } + + /** + * Reads and executes commands from the command line interface continuously. + *+ * This method will parse the given input into command components, create the associated {@link Command} objects + * using the {@link CommandFactory}, and then execute the commands. + *
+ * + * @param input The input to be parsed. + * @return The String returned from executing the commands. + */ + public String parse(String input) { + ParseResult parseResult = parseCommandAndArguments(input); + String commandType = parseResult.getCommandType(); + List+ * This class interprets the user's command and its associated arguments + * to generate the correct {@link Command} object that corresponds to the user's request. + *
+ */ +public class CommandFactory { + private TaskFactory taskFactory; + private Duke dukeBot; + private UiService uiService; + + /** + * Initializes a new instance of CommandFactory. + * + * @param taskFactory The factory responsible for creating tasks. + * @param dukeBot The main Duke bot instance. + * @param uiService The UI service instance for user interactions. + */ + public CommandFactory(TaskFactory taskFactory, Duke dukeBot, UiService uiService) { + this.taskFactory = taskFactory; + this.dukeBot = dukeBot; + this.uiService = uiService; + } + + /** + * Creates a {@link Command} object based on the provided command and its arguments. + * + * @param command The main user command as a string. + * @param args A list of arguments associated with the command. + * @return The corresponding {@link Command} object. + * @throws UnknownCommandException If the provided command string does not match any known command. + * @throws InvalidCommandInputException If the command input is invalid or incomplete. + */ + public Command createCommand(String command, List+ * The OutputService ensures consistent display formats, particularly with the use + * of indentation and dividers for enhanced readability. + *
+ */ +public class OutputService { + private static final int indentLength = 4; + private final String divider = appendNewLine( + indentLeft(String.format("%40s", "").replace(" ", "-"))); + + /** + * Echos the provided input string to the user without a prefix. + * + * @param input The string to be displayed. + */ + public String echo(String input) { + return echo(input, ""); + } + + /** + * Echos the provided input string to the user with the specified prefix. + * + * @param input The string to be displayed. + * @param prefix A prefix to be added before the input string. + */ + public String echo(String input, String prefix) { + return echo(List.of(prefix + input)); + } + + /** + * Echos a list of strings to the user with standardized formatting. + * + * @param inputs The list of strings to be displayed. + */ + public String echo(List+ * This service class provides an abstraction for the storage operations. It manages the tasks + * in the local storage and offers functionalities such as loading, saving, and deleting tasks. + *
+ */ +public class StorageService { + public static final String PATH_NAME = "./data"; + public static final String FILE_NAME = "./data/duke.txt"; + private final File directory; + private final File tasksStorageFile; + private boolean wasFileCorrupted; + + /** + * Initializes a new StorageService, ensuring the existence of the storage directory and file. + * + * @throws DukeStorageException If there's any issue creating or accessing the storage. + */ + public StorageService() throws DukeStorageException { + this.directory = new File(PATH_NAME); + this.tasksStorageFile = new File(FILE_NAME); + this.wasFileCorrupted = false; + initDirectory(); + initFile(); + } + + /** + * Checks if the storage file was corrupted during the last operation. + * + * @return {@code true} if the file was corrupted, otherwise {@code false}. + */ + public boolean wasFileCorrupted() { + return wasFileCorrupted; + } + + /** + * Loads tasks from the storage file. + * + * @return A list of {@link Task} objects loaded from the storage file. + */ + public List+ * This class provides an abstraction over the task creation process. It receives a task type and + * a list of arguments, then produces the corresponding task instance. + *
+ */ +public class TaskFactory { + + /** + * Creates a task instance based on the provided task type and arguments. + * + * @param taskType The type of task to be created. It should match one of the enum values in {@code TaskType}. + * @param argsList The list of arguments required for the task. The first item is always the task name. + * Further items depend on the task type (e.g., due date for deadlines, start and + * end times for events). + * @return The created Task object. + * @throws TaskParseException If there's an error in the task's format or arguments. + * @throws TimeUtilException If there's an error in parsing the date and time. + */ + public Task createTask(String taskType, List+ * This service acts as an intermediate between the logic and the actual output service. It simplifies + * the process of generating structured messages for different operations within the application. + *
+ */ +public class UiService { + private final OutputService outputService; + + /** + * Creates a new UiService instance with a specified OutputService. + * + * @param outputService The service to handle actual output operations. + */ + public UiService(OutputService outputService) { + this.outputService = outputService; + } + + /** + * Displays a greeting message with the given bot's name. + * + * @param botName Name of the bot. + * @return A String containing the greeting message. + */ + public String greetMessage(String botName) { + return String.format("Hello! I'm %s%nWhat can I do for you?", botName); + } + + /** + * Displays a farewell message. + * + * @return A string containing the farewell message. + */ + public String formatBye() { + return outputService.echo("Bye! Hope to see you again soon!"); + } + + /** + * Displays a generic message to the user. + * + * @param message The message to be displayed. + * @return The message to be displayed, formatted by OutputService. + */ + public String formatGenericMessage(String message) { + return outputService.echo(message); + } + + /** + * Displays a generic message to the user. + * + * @param messages The List of messages to be displayed. + * @return The messages to be displayed, formatted by OutputService. + */ + public String formatGenericMessage(List+ * This utility class provides multiple date and time formats for parsing, + * which can be helpful in accommodating various user inputs. + *
+ */ +public class TimeUtil { + private static final DateTimeFormatter[] DATE_TIME_FORMATTERS = { + DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm"), + DateTimeFormatter.ofPattern("yyyyMMdd HHmm") + }; + + private static final DateTimeFormatter[] DATE_ONLY_FORMATTERS = { + DateTimeFormatter.ISO_LOCAL_DATE, + DateTimeFormatter.BASIC_ISO_DATE, + DateTimeFormatter.ofPattern("d MMM yyyy", Locale.ENGLISH), + DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.ENGLISH) + }; + + private static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"); + + // Private constructor to prevent instantiation + private TimeUtil() {} + + /** + * Parses the provided date-time string to a LocalDateTime object. + *+ * The function tries various date-time patterns to find a match. + * If none of the patterns match, it throws a TimeUtilException. + *
+ * + * @param input The date-time string to parse. + * @return A LocalDateTime object. + * @throws TimeUtilException If the input string cannot be parsed. + */ + public static LocalDateTime parseDateTimeString(String input) throws TimeUtilException { + LocalDateTime dateTime = handleSpecialStrings(input); + if (dateTime != null) { + return dateTime; + } + for (DateTimeFormatter formatter : DATE_TIME_FORMATTERS) { + try { + return LocalDateTime.parse(input, formatter); + } catch (DateTimeParseException ignored) { + // try next formatter + } + } + for (DateTimeFormatter formatter : DATE_ONLY_FORMATTERS) { + try { + LocalDate parsedDate = LocalDate.parse(input, formatter); + return LocalDateTime.of(parsedDate, LocalDateTime.now().toLocalTime()); + } catch (DateTimeParseException ignored) { + // try next formatter + } + } + throw new TimeUtilException(getHelpMessage()); + } + + /** + * Formats a LocalDateTime object to a human-readable string. + * + * @param localDate The LocalDateTime object to format. + * @return A string representation of the LocalDateTime object. + */ + public static String formatLocalDateTime(LocalDateTime localDate) { + return localDate.format(DISPLAY_FORMATTER); + } + + /** + * Returns a help message explaining valid date formats. + * + * @return A string containing a list of valid date formats. + */ + public static String getHelpMessage() { + return "Invalid date format! Please use one of the following formats:" + + "\n- yyyy-MM-dd HHmm (e.g. 2023-05-28 1800)" + + "\n- yyyyMMdd HHmm (e.g. 2023-05-28 1800)" + + "\n- yyyy-MM-dd (e.g. 2023-05-28)" + + "\n- yyyymmdd (e.g. 20230528)" + + "\n- d MMM yyyy (e.g. 1 Jan 2023)" + + "\n- d MMMM yyyy (e.g. 1 January 2023)" + + "\nOr use special terms like:" + + "\n- today" + + "\n- tomorrow" + + "\n- monday (for the next Monday)" + + "\n- tuesday (for the next Tuesday)" + + "\n- ... (similarly for other days of the week)"; + } + + /** + * Handles special string inputs, converting them to LocalDateTime. + *+ * The function currently supports 'today' and 'tomorrow' as special strings. + *
+ * + * @param input The special string input. + * @return A LocalDateTime representation of the input, or null if the input is not special. + */ + private static LocalDateTime handleSpecialStrings(String input) { + switch (input.toLowerCase()) { + case "today": + return LocalDateTime.now().withHour(23).withMinute(59); + case "tomorrow": + return LocalDateTime.now().plusDays(1).withHour(23).withMinute(59); + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + return getNextDayOfWeek(DayOfWeek.valueOf(input.toUpperCase())); + default: + return null; + } + } + + /** + * Returns the LocalDateTime of the next occurrence of the specified day of the week. + *+ * This method calculates the number of days between the current day and the desired day of the week. + * If the desired day is the same as the current day, it returns the date of the next week's occurrence. + *
+ * + * @param desiredDay The desired day of the week as a DayOfWeek enum value. + * @return A LocalDateTime representing the next occurrence of the desired day, with the time set to 23:59. + */ + private static LocalDateTime getNextDayOfWeek(DayOfWeek desiredDay) { + LocalDateTime now = LocalDateTime.now(); + int daysUntilDesired = desiredDay.getValue() - now.getDayOfWeek().getValue(); + if (daysUntilDesired <= 0) { // if today is the desired day or a later day in the week + daysUntilDesired += 7; // get the next occurrence of the desired day + } + return now.plusDays(daysUntilDesired).withHour(23).withMinute(59); + } + +} diff --git a/src/main/resources/images/DaDuke.png b/src/main/resources/images/DaDuke.png new file mode 100644 index 0000000000..c72c1398cb 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..b7b421504d 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..2da5ef8ddd --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,17 @@ + + + + + + + +