diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 1cb8fed3b69..71b35fed74e 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -237,7 +237,7 @@ The following diagram shows the Class Diagram of the `DeleteCommand` hierarchy: The following diagram shows the Sequence Diagram of executing a `DeleteMultipleModulesCommand`: -![DeleteMultpleModulesCommandSequential](images/delete/deleteCommandSequenceDiagram.png) +![DeleteMultpleModulesCommandSequential](images/delete/DeleteModuleSequenceDiagram.png) The following is a description of the code execution flow 1. `DeleteCommandParser#parse(String)` takes the user's input as a `String` argument and determines the intention of the command (delete module, lecture or video). @@ -281,6 +281,86 @@ The following table below depicts the consideration of inputs against the user' ### Mark / UnMARK +The proposed mark command supports: +1. Marking unmarked videos as watched +2. Marking marked videos as unwatched +3. Marking multiple videos in 1. and 2. + + - E.g.: User wishes to mark a video "Vid 1" in lecture "Week 1" of module "CS2040S" as watched. + Executing `mark Vid 1 /mod CS2040S /lec Week 1` would allow the user to do so, unless either one of the following conditions are true: + 1. the module (CS2040S) does not exist in the Tracker + 2. the lecture (Week 1) does not exist in the module (CS2040S) + 3. the video (Vid 1) does not exist in the lecture of the module (CS2040S > Week 1) + 4. the video (CS2040S > Week 1 > Vid 1) has already been marked as watched + - E.g.: User wishes to mark multiple videos "Vid 3", "Vid 4" and "Lecture Summary" in lecture "Topic 4" of module "ST2334" as unwatched. + Executing `unmark Vid 3, Vid 4, Lecture Summary /mod ST2334 /lec Topic 1` would allow the user to do so, unless either on of the following conditions are true: + 1. the module (ST2334) doese not exist in the Tracker + 2. the lecture (Topic 1) does not exist in the module (ST2334) + 3. either of the videos (Vid 3, Vid 4, Lecture Summary) does not exist in the lecture of the module (ST2334 > Topic 1) + 4. either of the videos (Vid 3, Vid 4, Lecture Summary) has already been marked as unwatched + +This feature's behaviour is dependent on the arguments provided by the user, as well as the state of Le Tracker. + +#### Implementation Details + +The feature utilises the following classes: + - `MarkCommand`: Abstract class extending from `Command` for commands that mark a specified video as watched or unwatched + - `MarkAsWatchedCommandParser`: parses arguments appropriately for `MarkAsWatchedCommand` to be returned to be executed + - `MarkAsWatchedCommand`: Subclass of `MarkCommand` which handles marking a video as watched. Can handle marking multiple videos as well + + - `MarkAsUnwatchedCommandParser`: parses arguments appropriately for `MarkAsUnwatchedCommand` and `MarkMultipleAsUnwatchedCommand` to be returned to be executed + - `MarkAsUnwatchedCommand`: Subclass of `MarkCommand` which handles marking a video as unwatched + - `MarkMultipleAsUnwatchedCommand`: Subclass of `MarkCommand` which handles marking multiple videos as unwatched + +The following diagram shows the Sequence Diagram of executing a `MarkAsWatchedCommand`: + +![MarkAsWatched](diagrams/MarkAsWatchedSequenceDiagram.png) + +The following is a description of the code execution flow +1. `MarkAsWatchedCommandParser#parse(String)` / `MarkAsUnwatchedCommandParser#parse(String` takes the user's input as a `String` argument and determines the target video to be marked. The following table below depicts the command returned against the user's intent + +| Parser | Has Multiple Videos | Command | +| --- | --- | --- | +| `MarkAsWatchedCommandParser` | -- | `MarkAsWatchedCommand` | +| `MarkAsUnwatchedCommandParser` | Yes | `MarkAsUnwatchedCommand` | +| | No | `MarkMultipleAsUnwatchedCommand` | +| --- | --- | --- | + +2. The argument values are then checked on as such: + - ModuleCode: valid module code that complies with the module code format + - LectureName: valid lecture name that does not containt symbols + - VideoName: valid lecture name that does not contain symbols + + Note: LectureName and VideoName should not contain commas (","). Rather than throwing errors, Le Tracker will treat it as though the user intended to delete multiple videos + +3. The appropriate `MarkCommand` subclass object is created then returned to its caller + +4. Upon execution, the argument values in the `MarkCommand` subclass object are then checked on as such: + - ModuleCode: if module with ModuleCode exists in Le Tracker + - LectureName: if lecture with LectureName exists in module ModuleCode + - VideoName: if video(s) with VideoName exists in lecture LectureName of module ModuleCode and whether the video(s) is/are marked or unmarked (differs according to whether `mark` or `unmark` is called) + +5. If no exceptions are thrown, Le Tracker has successfully managed to mark/unmark the specified video(s) + +#### Reasons for such implementation: + + 1. Adhering to Open-Close Principle: Open for Extension, Closed for Modification + 2. Having abstract classes to group mark commands together allows for adherance of DRY (Don't Repeat Yourself) in cases such as success message formats in every class + +#### Alternatives considered: + 1. Combine all featured classes into one large single class + Pros: + - all file content in one single place + - easily adheres to DRY since there would be no need to repeat information across multiple files + Cons: + - violates Open-Close Principle + - creates a large file that needs to be edited. Hard to search through + +#### Possible further implementation + - Collate `MarkAsUnwatchedCommand` and `MarkMultipleAsUnwatchedCommand` into one class, similar to `MarkAsWatchedCommand` + + + ### Find command feature The proposed find command supports: diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 2a231392652..1c6cf6062b2 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -20,6 +20,7 @@ LE TRACKER is a gamified tracking application that allows fast typist to easily - [Navigate Relatively](#navigate-relatively) - [Navigate Directly](#navigate-directly) - [Navigate Backwards](#navigate-backwards) + - [List Modules or Lectures or Videos](#list-modules-or-lectures-or-videos) - [List Modules](#list-modules) - [List Lectures of Modules](#list-lectures-of-modules) - [List Videos of Lectures](#list-videos-of-lectures) @@ -30,9 +31,9 @@ LE TRACKER is a gamified tracking application that allows fast typist to easily - [Edit a Lecture](#edit-a-lecture) - [Edit a Video](#edit-a-video) - [Mark or Unmark a Video](#mark-or-unmark-a-video) - - [Delete a Module](#delete-a-module) - - [Delete a Lecture](#delete-a-lecture) - - [Delete a Video](#delete-a-video) + - [Delete Module](#delete-module) + - [Delete Lecture](#delete-lecture) + - [Delete Video](#delete-video) - [Tag a module](#tag-a-module) - [Tag a lecture](#tag-a-lecture) - [Tag a video](#tag-a-video) @@ -69,6 +70,7 @@ LE TRACKER is a gamified tracking application that allows fast typist to easily ### List - `list`: Lists all modules/lectures/videos based on context +- `list /r`: Lists all modules from any context - `list [/mod {module_code}]`: Lists all the lectures in a specified module - `list [/lec {lecture_name}]`: Lists all the videos in a navigated module and specified lecture (:exclamation: only works if you are in `module` context) - `list [/mod {module_code} /lec {lecture_name}]`: Lists all the videos in a specified module and lecture @@ -178,11 +180,17 @@ Format: `nav /mod {module_code / lecture_name} [/lec {lecture_name}]` Format: `navb` +### List Modules or Lectures or Videos + +> Root context: modules, Module context: lectures, Lecture context: videos + +Format: `list` + ### List Modules > Lists all modules -Format: `list` +Format: `list /r` ### List Lectures of Modules @@ -329,23 +337,32 @@ Examples: - `edit Video 1 /mod CS2040S /lec Week 1 /name Video 01 Grade Breakdown /watch /tags Intro, Short` -### Mark or Unmark a Video +### Mark or Unmark Video(s) + +> Marks/Unmarks a video as watched/unwatched in a lecture of its specified module. -> Marks/Unmarks a video as watched/unwatched in a lecture of its specified module +Format: +- `mark {video_name} /mod {module_name} /lec {lecture_index}` +- `unmark {video_name} /mod {module_name} /lec {lecture_index}` -Format: `mark {video_name_1}[, {video_name_2}[, {video_name_3}[, ...]]] /mod {module_code} /lec {lecture_name}` +Parameters: +- `mark` marks `{video_name}` as watched +- `unmark` marks `{video_name}` as unwatched +- `{video_name}` can be names of multiple videos, separated by commas (",") +- if `{video_name}` contains repeated names, the repeats will be ignored -Format: `unmark {video_name_1}[, {video_name_2}[, {video_name_3}[, ...]]] /mod {module_code} /lec {lecture_name}` +Note: Calling mark or unmark would only prompt an error for already marked or unmarked videos if calling on a single video, not when calling on multiple videos in one command - `video_name_1`, `video_name_2`, `video_name_3`, ...: Multiple videos can be specified to be deleted by specying multiple video namese, separating them by commas(",") - Video Names must be of valid format - If any video specified does not exist or has already been marked or unmarked (accordingly to the command called), nothing changes within the model -Examples: - `mark Vid 1 /mod CS2040 /lec Week 1` -- `unmark Vid 2, Vid 5 /mod ST2334 /lec Topic 4` +- `mark Vid 1, Vid 2 /mod CS2040 /lec Week 1` +- `unmark Vid 2 /mod CS2040 /lec Week 1` +- `unmark Vid 1, Vid 2 /mod CS2040 /lec Week 1` -### Delete Module(s) +### Delete Module > Deletes the specified module(s) and all its embodied content from the application @@ -360,7 +377,7 @@ Examples: - `delete CS2040` - `delete CS2040, ST2334` -### Delete Lecture(s) +### Delete Lecture > Deletes the specified lecture(s) and all its embodied content from the same specified module @@ -376,7 +393,7 @@ Examples: - `delete lecture 1 /mod CS2040` deletes `lecture 1` lecture found in module `CS2040` - `delete lecture 1, lecture 2 /mod ST2334` deletes `lecture 1` and `lecture 2` lectures found in module `ST2334` -### Delete Video(s) +### Delete Video > Deletes the specified video(s) and all its embodied content from the same specified lecture of the specified module diff --git a/docs/diagrams/DeleteModuleSequenceDiagram.puml b/docs/diagrams/DeleteModuleSequenceDiagram.puml new file mode 100644 index 00000000000..8d20f6747c1 --- /dev/null +++ b/docs/diagrams/DeleteModuleSequenceDiagram.puml @@ -0,0 +1,65 @@ +@startuml + +--> ":LogicManager" +activate ":LogicManager" + +":LogicManager" --> ":TrackerParser": TrackerParser() +activate ":TrackerParser" + +":TrackerParser" --> ":DeleteCommandParser": parseCommand(String userInput) +activate ":DeleteCommandParser" + +":DeleteCommandParser" --> ":DeleteMultipleModulesCommand": parse(String args) +activate ":DeleteMultipleModulesCommand" + +":DeleteCommandParser" <-- ":DeleteMultipleModulesCommand" +deactivate ":DeleteMultipleModulesCommand" + +":TrackerParser" <-- ":DeleteCommandParser" +deactivate ":DeleteCommandParser" + +":LogicManager" <-- ":TrackerParser" +deactivate ":TrackerParser" + +":LogicManager" --> ":DeleteMultipleModulesCommand": execute(Model m) +activate ":DeleteMultipleModulesCommand" + +loop for each module specified + ":DeleteMultipleModulesCommand" --> ":DeleteModuleCommand": DeleteModuleCommand() + activate ":DeleteModuleCommand" + + ":DeleteMultipleModulesCommand" <-- ":DeleteModuleCommand" + deactivate ":DeleteModuleCommand" + + ":DeleteMultipleModulesCommand" --> ":DeleteModuleCommand": execute(Model m) + activate ":DeleteModuleCommand" + + ":DeleteModuleCommand" --> "m:Model" : hasModule() + activate "m:Model" + + ":DeleteModuleCommand" <-- "m:Model": true + deactivate "m:Model" + + ":DeleteModuleCommand" --> "m:Model": deleteModule() + activate "m:Model" + + ":DeleteModuleCommand" <-- "m:Model" + deactivate "m:Model" + + ":DeleteModuleCommand" --> "r:CommandResult": CommandResult() + activate "r:CommandResult" + + ":DeleteModuleCommand" <-- "r:CommandResult": r + deactivate "r:CommandResult" + + ":DeleteMultipleModulesCommand" <-- ":DeleteModuleCommand": r + deactivate ":DeleteModuleCommand" +end + +":LogicManager" <-- ":DeleteMultipleModulesCommand" +deactivate ":DeleteMultipleModulesCommand" + +<-- ":LogicManager" +deactivate ":LogicManager" + +@enduml diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 9204163b61f..0b1beb944fd 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/delete/DeleteModuleSequenceDiagram.png b/docs/images/delete/DeleteModuleSequenceDiagram.png new file mode 100644 index 00000000000..6097a0f19b7 Binary files /dev/null and b/docs/images/delete/DeleteModuleSequenceDiagram.png differ diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index ee0df7bbbbe..d3170dd8834 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -2,7 +2,12 @@ import static java.util.Objects.requireNonNull; +import java.util.List; +import java.util.stream.Collectors; + +import seedu.address.logic.commands.CommandResult.ModuleEditInfo; import seedu.address.model.Model; +import seedu.address.model.module.ReadOnlyModule; /** * Clears the tracker. @@ -12,11 +17,20 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; public static final String MESSAGE_SUCCESS = "Le Tracker has been cleared!"; - @Override public CommandResult execute(Model model) { requireNonNull(model); model.clearTracker(); - return new CommandResult(MESSAGE_SUCCESS); + List modules = model.getTracker() + .getModuleList() + .stream() + .map(x -> (ReadOnlyModule) x) + .collect(Collectors.toList()); + + ModuleEditInfo[] clearedModuleInfos = new ModuleEditInfo[modules.size()]; + for (int i = 0; i < modules.size(); i++) { + clearedModuleInfos[i] = new ModuleEditInfo(modules.get(i), null); + } + return new CommandResult(MESSAGE_SUCCESS, clearedModuleInfos); } } diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 66a40b6a3d7..1f6cd47138f 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -5,6 +5,7 @@ import static seedu.address.commons.core.Messages.MESSAGE_MODULE_DOES_NOT_EXIST; import static seedu.address.logic.parser.CliSyntax.PREFIX_LECTURE; import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ROOT; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_MODULES; import seedu.address.logic.commands.exceptions.CommandException; @@ -26,7 +27,8 @@ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; public static final String MESSAGE_USAGE = "Make sure that you are calling this command from the correct context.\n" - + COMMAND_WORD + ": List modules or lectures or videos from ANY context.\n" + + COMMAND_WORD + " " + PREFIX_ROOT + ": List modules from ANY context.\n" + + COMMAND_WORD + ": List modules or lectures or videos depending on current context.\n" + COMMAND_WORD + " " + PREFIX_MODULE + " {module_code}: List lectures from MODULE context.\n" + COMMAND_WORD + " " + PREFIX_LECTURE + " {lecture_name}: List videos from LECTURE context.\n" + COMMAND_WORD + " " + PREFIX_MODULE + " {module_code} " @@ -45,6 +47,8 @@ public class ListCommand extends Command { public static final String MESSAGE_FAIL_CODE = "Module code format is invalid"; + private final boolean isRoot; + private ModuleCode moduleCode; private LectureName lectureName; @@ -52,7 +56,16 @@ public class ListCommand extends Command { /** * Creates a ListCommand to list content from current context */ - public ListCommand() {} + public ListCommand() { + this.isRoot = false; + } + + /** + * Creates a ListCommand to list module contents from any context + */ + public ListCommand(boolean isRoot) { + this.isRoot = isRoot; + } /** * Creates a ListCommand to list lecture contents from module context @@ -60,6 +73,7 @@ public ListCommand() {} */ public ListCommand(ModuleCode moduleCode) { this.moduleCode = moduleCode; + this.isRoot = false; } /** @@ -70,11 +84,16 @@ public ListCommand(ModuleCode moduleCode) { public ListCommand(ModuleCode moduleCode, LectureName lectureName) { this.moduleCode = moduleCode; this.lectureName = lectureName; + this.isRoot = false; } @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); + if (isRoot) { + return filterByModuleList(model); + } + if (moduleCode != null && lectureName != null) { if (!model.hasLecture(moduleCode, lectureName)) { throw new CommandException( diff --git a/src/main/java/seedu/address/logic/commands/MarkAsWatchedCommand.java b/src/main/java/seedu/address/logic/commands/MarkAsWatchedCommand.java deleted file mode 100644 index 051c3da816d..00000000000 --- a/src/main/java/seedu/address/logic/commands/MarkAsWatchedCommand.java +++ /dev/null @@ -1,74 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.lecture.LectureName; -import seedu.address.model.lecture.ReadOnlyLecture; -import seedu.address.model.module.ModuleCode; -import seedu.address.model.video.Video; -import seedu.address.model.video.VideoName; - -/** - * Marks a video identified using its name, within a lecture, within a module, as watched - */ -public class MarkAsWatchedCommand extends MarkCommand { - - public static final String COMMAND_WORD = "mark"; - - private final ModuleCode moduleCode; - private final LectureName lectureName; - private final VideoName targetVideoName; - - /** - * Creates a Mark As Watched Command that marks a video with {@code targetVideoName} - * from lecture with {@code lectureName} in module of {@code moduleCode} as watched. - * - * @param targetVideoName Name of the Video to mark - * @param moduleCode Module Code of module that contains lecture that video is within - * @param lectureName Name of Lecture that video is within - */ - public MarkAsWatchedCommand(VideoName targetVideoName, ModuleCode moduleCode, LectureName lectureName) { - this.targetVideoName = targetVideoName; - this.moduleCode = moduleCode; - this.lectureName = lectureName; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - // TODO: could try to encapsulate this to reuse - requireNonNull(model); - - if (!model.hasModule(moduleCode)) { - throw new CommandException(String.format(Messages.MESSAGE_MODULE_DOES_NOT_EXIST, moduleCode)); - } - - if (!model.hasLecture(moduleCode, lectureName)) { - throw new CommandException(String.format(Messages.MESSAGE_LECTURE_DOES_NOT_EXIST, lectureName, moduleCode)); - } - - ReadOnlyLecture lecture = model.getLecture(moduleCode, lectureName); - - if (!model.hasVideo(moduleCode, lectureName, targetVideoName)) { - throw new CommandException(String.format(Messages.MESSAGE_VIDEO_DOES_NOT_EXIST, - targetVideoName, - lectureName, - moduleCode)); - } - - Video targetVideo = model.getVideo(moduleCode, lectureName, targetVideoName); - - // TODO: ends here - - if (targetVideo.hasWatched()) { - throw new CommandException(String.format(MESSAGE_VIDEO_MARK_NOT_CHANGED, targetVideoName, COMMAND_WORD)); - } - - Video newVideo = new Video(targetVideoName, true, targetVideo.getTimestamp(), targetVideo.getTags()); - model.setVideo(lecture, targetVideo, newVideo); - - return new CommandResult(String.format(MESSAGE_MARK_VIDEO_SUCCESS, targetVideoName, COMMAND_WORD)); - } -} diff --git a/src/main/java/seedu/address/logic/commands/MarkCommand.java b/src/main/java/seedu/address/logic/commands/MarkCommand.java deleted file mode 100644 index af80225e7c4..00000000000 --- a/src/main/java/seedu/address/logic/commands/MarkCommand.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.logic.commands; - -import static seedu.address.logic.parser.CliSyntax.PREFIX_LECTURE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; - -/** - * Marks a video of a lecture in a module as watched or unwatched - */ -public abstract class MarkCommand extends Command { - - public static final String MESSAGE_EXAMPLE = " vIdEo 1 " - + PREFIX_MODULE + " CS2040S " - + PREFIX_LECTURE + " Week 2"; - - public static final String COMMAND_WORDS = MarkAsWatchedCommand.COMMAND_WORD - + "/" + MarkAsUnwatchedCommand.COMMAND_WORD; - - public static final String MESSAGE_USAGE = COMMAND_WORDS - + ":\n" - + "Marks the video in the lecture in the module as watched.\n" - + "Video is identified by the video name.\n" - + "Module is identified by the module code\n" - + "Lecture is identified by the lecture name.\n" - + "Parameters: video name, module code, lecture name\n" - + "Example: " + COMMAND_WORDS + MESSAGE_EXAMPLE; - - public static final String MESSAGE_VIDEO_MARK_NOT_CHANGED = "Video %1$s already %2$s" + "ed! No change..."; - - public static final String MESSAGE_MARK_VIDEO_SUCCESS = "Successfully %2$s" + "ed Video: %1$s"; - -} diff --git a/src/main/java/seedu/address/logic/commands/MarkAsUnwatchedCommand.java b/src/main/java/seedu/address/logic/commands/mark/MarkAsUnwatchedCommand.java similarity index 70% rename from src/main/java/seedu/address/logic/commands/MarkAsUnwatchedCommand.java rename to src/main/java/seedu/address/logic/commands/mark/MarkAsUnwatchedCommand.java index 38ac5e5baf4..69f2979bf0c 100644 --- a/src/main/java/seedu/address/logic/commands/MarkAsUnwatchedCommand.java +++ b/src/main/java/seedu/address/logic/commands/mark/MarkAsUnwatchedCommand.java @@ -1,8 +1,10 @@ -package seedu.address.logic.commands; +package seedu.address.logic.commands.mark; import static java.util.Objects.requireNonNull; import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandResult.VideoEditInfo; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.lecture.LectureName; @@ -24,13 +26,17 @@ public class MarkAsUnwatchedCommand extends MarkCommand { /** * Creates a Mark As Unwatched Command that marks a video with {@code targetVideoname} - * from lecture with {@code lectureName} in module of {@code moduelCode} as watched. + * from lecture with {@code lectureName} in module of {@code moduleCode} as unwatched. * * @param targetVideoName * @param moduleCode * @param lectureName */ public MarkAsUnwatchedCommand(VideoName targetVideoName, ModuleCode moduleCode, LectureName lectureName) { + requireNonNull(targetVideoName); + requireNonNull(moduleCode); + requireNonNull(lectureName); + this.targetVideoName = targetVideoName; this.moduleCode = moduleCode; this.lectureName = lectureName; @@ -63,12 +69,32 @@ public CommandResult execute(Model model) throws CommandException { // TODO: ends here if (!targetVideo.hasWatched()) { - throw new CommandException(String.format(MESSAGE_VIDEO_MARK_NOT_CHANGED, targetVideoName, COMMAND_WORD)); + throw new CommandException(String.format(MESSAGE_VIDEO_MARK_NOT_CHANGED, + targetVideoName, + COMMAND_WORD, + "", + "", + lectureName, + moduleCode)); } Video newVideo = new Video(targetVideoName, false, targetVideo.getTimestamp(), targetVideo.getTags()); model.setVideo(lecture, targetVideo, newVideo); - return new CommandResult(String.format(MESSAGE_MARK_VIDEO_SUCCESS, targetVideoName, COMMAND_WORD)); + return new CommandResult(String.format(MESSAGE_MARK_VIDEO_SUCCESS, + targetVideoName, COMMAND_WORD, "", "", lectureName, moduleCode), + new VideoEditInfo(moduleCode, lectureName, targetVideo, newVideo)); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof MarkAsUnwatchedCommand)) { + return false; + } + + MarkAsUnwatchedCommand markCommand = (MarkAsUnwatchedCommand) other; + return this.targetVideoName.equals(markCommand.targetVideoName) + && this.lectureName.equals(markCommand.lectureName) + && this.moduleCode.equals(markCommand.moduleCode); } } diff --git a/src/main/java/seedu/address/logic/commands/mark/MarkAsWatchedCommand.java b/src/main/java/seedu/address/logic/commands/mark/MarkAsWatchedCommand.java new file mode 100644 index 00000000000..43ea6206cca --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/mark/MarkAsWatchedCommand.java @@ -0,0 +1,148 @@ +package seedu.address.logic.commands.mark; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandResult.VideoEditInfo; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.MultipleEventsParser; +import seedu.address.model.Model; +import seedu.address.model.lecture.LectureName; +import seedu.address.model.lecture.ReadOnlyLecture; +import seedu.address.model.module.ModuleCode; +import seedu.address.model.video.Video; +import seedu.address.model.video.VideoName; + + +/** + * Marks a video identified using its name, within a lecture, within a module, as watched + */ +public class MarkAsWatchedCommand extends MarkCommand { + + public static final String COMMAND_WORD = "mark"; + + private final ModuleCode moduleCode; + private final LectureName lectureName; + private final ArrayList targetVideoNames; + + /** + * Creates a Mark As Watched Command that marks a video with {@code targetVideoName} + * from lecture with {@code lectureName} in module of {@code moduleCode} as watched. + * + * @param videoNames Names of the Videos to mark + * @param moduleCode Module Code of module that contains lecture that video is within + * @param lectureName Name of Lecture that video is within + */ + public MarkAsWatchedCommand(ModuleCode moduleCode, LectureName lectureName, VideoName... videoNames) { + + requireNonNull(moduleCode); + requireNonNull(lectureName); + requireNonNull(videoNames); + + ArrayList videoNamesArrayList = new ArrayList<>(); + for (VideoName each: videoNames) { + videoNamesArrayList.add(each); + } + this.targetVideoNames = videoNamesArrayList; + this.moduleCode = moduleCode; + this.lectureName = lectureName; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (!model.hasModule(moduleCode)) { + throw new CommandException(String.format(Messages.MESSAGE_MODULE_DOES_NOT_EXIST, moduleCode)); + } + + if (!model.hasLecture(moduleCode, lectureName)) { + throw new CommandException(String.format(Messages.MESSAGE_LECTURE_DOES_NOT_EXIST, lectureName, moduleCode)); + } + + ReadOnlyLecture lecture = model.getLecture(moduleCode, lectureName); + + int inputLength = targetVideoNames.size(); + ArrayList nonExistentVideosNames = new ArrayList<>(); + ArrayList alreadyMarkedVideosNames = new ArrayList<>(); + Video[] originalVideos = new Video[inputLength]; + Video[] newVideos = new Video[inputLength]; + for (int i = 0; i < inputLength; i++) { + + VideoName videoName = targetVideoNames.get(i); + if (!model.hasVideo(moduleCode, lectureName, videoName)) { + nonExistentVideosNames.add(targetVideoNames.get(i)); + continue; + } + + Video targetVideo = model.getVideo(moduleCode, lectureName, videoName); + originalVideos[i] = targetVideo; + + if (targetVideo.hasWatched()) { + alreadyMarkedVideosNames.add(videoName); + continue; + } + + Video newVideo = new Video(videoName, true, targetVideo.getTimestamp(), targetVideo.getTags()); + newVideos[i] = newVideo; + } + + + if (nonExistentVideosNames.size() != 0) { + throw new CommandException(String.format(( + nonExistentVideosNames.size() == 1 + ? Messages.MESSAGE_VIDEO_DOES_NOT_EXIST + : Messages.MESSAGE_VIDEOS_DO_NOT_EXIST + ), + MultipleEventsParser.convertArrayListToString(nonExistentVideosNames), + this.moduleCode, + this.lectureName)); + } + + if (alreadyMarkedVideosNames.size() != 0) { + throw new CommandException(String.format(MESSAGE_VIDEO_MARK_NOT_CHANGED, + MultipleEventsParser.convertArrayListToString(alreadyMarkedVideosNames), + COMMAND_WORD, + "", + alreadyMarkedVideosNames.size() == 1 ? "" : "s", + lectureName, + moduleCode)); + } + + ArrayList videoNamesArrayList = new ArrayList<>(); + VideoEditInfo[] editedVideosInfos = new VideoEditInfo[inputLength]; + for (int i = 0; i < inputLength; i++) { + videoNamesArrayList.add(targetVideoNames.get(i)); + + Video targetVideo = originalVideos[i]; + Video newVideo = newVideos[i]; + + model.setVideo(lecture, targetVideo, newVideo); + editedVideosInfos[i] = new VideoEditInfo(this.moduleCode, this.lectureName, targetVideo, newVideo); + } + + return new CommandResult(String.format(MESSAGE_MARK_VIDEO_SUCCESS, + MultipleEventsParser.convertArrayListToString(videoNamesArrayList), + COMMAND_WORD, + inputLength + " ", + inputLength == 1 ? "" : "s", + lectureName, + moduleCode), + editedVideosInfos); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof MarkAsWatchedCommand)) { + return false; + } + + MarkAsWatchedCommand command = (MarkAsWatchedCommand) other; + return this.targetVideoNames.equals(command.targetVideoNames) + && this.moduleCode.equals(command.moduleCode) + && this.lectureName.equals(command.lectureName); + } +} diff --git a/src/main/java/seedu/address/logic/commands/mark/MarkCommand.java b/src/main/java/seedu/address/logic/commands/mark/MarkCommand.java new file mode 100644 index 00000000000..2dba25c4251 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/mark/MarkCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands.mark; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_LECTURE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.Command; + +/** + * Marks a video of a lecture in a module as watched or unwatched + */ +public abstract class MarkCommand extends Command { + + public static final String MESSAGE_EXAMPLE = " vIdEo 1 " + + PREFIX_MODULE + " CS2040S " + + PREFIX_LECTURE + " Week 2"; + + public static final String COMMAND_WORDS = MarkAsWatchedCommand.COMMAND_WORD + + "/" + MarkAsUnwatchedCommand.COMMAND_WORD; + + public static final String MESSAGE_USAGE = COMMAND_WORDS + + ":\n" + + "Marks the video in the lecture in the module as watched.\n" + + "Video is identified by the video name.\n" + + "Module is identified by the module code\n" + + "Lecture is identified by the lecture name.\n" + + "Parameters: video name, module code, lecture name\n" + + "Example: " + COMMAND_WORDS + MESSAGE_EXAMPLE + "\n" + + Messages.MESSAGE_CAN_DO_MULTIPLE; + + /** + * Skeletal string for unsuccessful mark / unmark command due to video already being marked / unmarked + * + * @param 1 VideoNames + * @param 2 Command Word (mark / unmark) + * @param 3 -- + * @param 4 "s" or "". For grammatical correctness + * @param 5 Lecture Name + * @param 6 ModuleCode + */ + public static final String MESSAGE_VIDEO_MARK_NOT_CHANGED = "Video%4$s %1$s" + + " in Lecture %5$s Module %6$s already %2$s" + + "ed! No changes made to all specified videos..."; + + /** + * Skeletal string for successful mark / unmark command + * + * @param 1 Video Names + * @param 2 Command Word (mark / unmark) + * @param 3 Number of videos + " " + * @param 4 "s" or "". For grammatical correctness + * @param 5 Lecture Name + * @param 6 Module Code + */ + public static final String MESSAGE_MARK_VIDEO_SUCCESS = "Successfully %2$s" + + "ed %3$sVideo%4$s in Lecture %5$s Module %6$s: %1$s"; + +} diff --git a/src/main/java/seedu/address/logic/commands/mark/MarkMultipleAsUnwatchedCommand.java b/src/main/java/seedu/address/logic/commands/mark/MarkMultipleAsUnwatchedCommand.java new file mode 100644 index 00000000000..07527f355db --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/mark/MarkMultipleAsUnwatchedCommand.java @@ -0,0 +1,111 @@ +package seedu.address.logic.commands.mark; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandResult.VideoEditInfo; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.MultipleEventsParser; +import seedu.address.model.Model; +import seedu.address.model.lecture.LectureName; +import seedu.address.model.lecture.ReadOnlyLecture; +import seedu.address.model.module.ModuleCode; +import seedu.address.model.video.Video; +import seedu.address.model.video.VideoName; + +/** + * Marks multiple videos identified using its name, within a lecture, within a module, as unwatched + */ +public class MarkMultipleAsUnwatchedCommand extends MarkCommand { + + private final ModuleCode moduleCode; + private final LectureName lectureName; + private final ArrayList videoNames; + + /** + * Creates a Mark Multiple As Unwatched Command that marks multiple videos with {@videoNames} + * from lecture with {@code lectureName} in module of {@code moduleCode} as unwatched + * + * @param videonames + * @param moduleCode + * @param lectureName + */ + public MarkMultipleAsUnwatchedCommand(VideoName[] videoNames, ModuleCode moduleCode, LectureName lectureName) { + requireNonNull(videoNames); + requireNonNull(moduleCode); + requireNonNull(lectureName); + + this.moduleCode = moduleCode; + this.lectureName = lectureName; + + ArrayList vids = new ArrayList<>(); + for (VideoName each: videoNames) { + vids.add(each); + } + + this.videoNames = vids; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (!model.hasModule(moduleCode)) { + throw new CommandException(String.format(Messages.MESSAGE_MODULE_DOES_NOT_EXIST, moduleCode)); + } + + if (!model.hasLecture(moduleCode, lectureName)) { + throw new CommandException(String.format(Messages.MESSAGE_LECTURE_DOES_NOT_EXIST, lectureName, moduleCode)); + } + + ArrayList invalidVideoNames = new ArrayList<>(); + for (VideoName each: this.videoNames) { + if (!model.hasVideo(moduleCode, lectureName, each)) { + invalidVideoNames.add(each); + } + } + + if (invalidVideoNames.size() != 0) { + throw new CommandException(String.format(Messages.MESSAGE_VIDEOS_DO_NOT_EXIST, + MultipleEventsParser.convertArrayListToString(invalidVideoNames), + this.moduleCode, this.lectureName)); + } + + ReadOnlyLecture lecture = model.getLecture(moduleCode, lectureName); + VideoEditInfo[] editedVideos = new VideoEditInfo[this.videoNames.size()]; + for (int i = 0; i < this.videoNames.size(); i++) { + VideoName targetVideoName = this.videoNames.get(i); + Video targetVideo = model.getVideo(moduleCode, lectureName, targetVideoName); + Video newVideo = new Video(targetVideoName, false, targetVideo.getTimestamp(), targetVideo.getTags()); + editedVideos[i] = new VideoEditInfo(moduleCode, lectureName, targetVideo, newVideo); + model.setVideo(lecture, targetVideo, newVideo); + } + + return new CommandResult(String.format(MESSAGE_MARK_VIDEO_SUCCESS, + MultipleEventsParser.convertArrayListToString(this.videoNames), + MarkAsUnwatchedCommand.COMMAND_WORD, + this.videoNames.size() + " ", + this.videoNames.size() == 1 + ? "" + : "s", + lectureName, + moduleCode), + editedVideos); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof MarkMultipleAsUnwatchedCommand)) { + return false; + } + + MarkMultipleAsUnwatchedCommand markCommand = (MarkMultipleAsUnwatchedCommand) other; + return this.videoNames.equals(markCommand.videoNames) + && this.lectureName.equals(markCommand.lectureName) + && this.moduleCode.equals(markCommand.moduleCode); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 18a662da7ee..c005af3c11c 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -14,11 +14,6 @@ public class CliSyntax { public static final Prefix PREFIX_TIMESTAMP = new Prefix("/timestamp"); public static final Prefix PREFIX_BY_TAG = new Prefix("/byTag"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - - public static final Prefix PREFIX_ROOT = new Prefix("/r"); public static final Prefix PREFIX_MODULE = new Prefix("/mod"); public static final Prefix PREFIX_LECTURE = new Prefix("/lec"); diff --git a/src/main/java/seedu/address/logic/parser/ListCommandParser.java b/src/main/java/seedu/address/logic/parser/ListCommandParser.java index f3bd4799671..c8939dbcf66 100644 --- a/src/main/java/seedu/address/logic/parser/ListCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/ListCommandParser.java @@ -3,6 +3,7 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_LECTURE; import static seedu.address.logic.parser.CliSyntax.PREFIX_MODULE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ROOT; import java.util.Optional; @@ -26,6 +27,7 @@ public ListCommand parse(String args) throws ParseException { Optional moduleCodeOpt = argMultimap.getValue(PREFIX_MODULE); Optional lectureNameOpt = argMultimap.getValue(PREFIX_LECTURE); + Optional rootOpt = argMultimap.getValue(PREFIX_ROOT); if (lectureNameOpt.isPresent()) { return parseListVideoCommand(lectureNameOpt, moduleCodeOpt); @@ -33,10 +35,8 @@ public ListCommand parse(String args) throws ParseException { if (moduleCodeOpt.isPresent()) { return parseListLectureCommand(moduleCodeOpt); } - String trimmedArgs = args.trim(); - if (!trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListCommand.MESSAGE_USAGE)); + if (rootOpt.isPresent()) { + return new ListCommand(true); } return new ListCommand(); } diff --git a/src/main/java/seedu/address/logic/parser/MarkAsUnwatchedCommandParser.java b/src/main/java/seedu/address/logic/parser/MarkAsUnwatchedCommandParser.java index a56bae54d91..e8c7adb663b 100644 --- a/src/main/java/seedu/address/logic/parser/MarkAsUnwatchedCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/MarkAsUnwatchedCommandParser.java @@ -6,8 +6,9 @@ import java.util.Optional; -import seedu.address.logic.commands.MarkAsUnwatchedCommand; -import seedu.address.logic.commands.MarkCommand; +import seedu.address.logic.commands.mark.MarkAsUnwatchedCommand; +import seedu.address.logic.commands.mark.MarkCommand; +import seedu.address.logic.commands.mark.MarkMultipleAsUnwatchedCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.lecture.LectureName; import seedu.address.model.module.ModuleCode; @@ -16,14 +17,14 @@ /** * Parses input arguments and creates a new MarkAsUnwatchedCommand executable object */ -public class MarkAsUnwatchedCommandParser implements Parser { +public class MarkAsUnwatchedCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the MarkAsUnwatchedCommand * and returns a MarkAsUnwatchedCommand object for execution. * @throws ParseException if the user input does not conform to the expected format */ - public MarkAsUnwatchedCommand parse(String args) throws ParseException { + public MarkCommand parse(String args) throws ParseException { ArgumentMultimap argMultiMap = ArgumentTokenizer.tokenize(args, PREFIX_MODULE, PREFIX_LECTURE); @@ -42,9 +43,13 @@ public MarkAsUnwatchedCommand parse(String args) throws ParseException { } ModuleCode moduleCode = ParserUtil.parseModuleCode(moduleCodeOptional.get()); LectureName lectureName = ParserUtil.parseLectureName(lectureNameOptional.get()); - VideoName videoName = ParserUtil.parseVideoName(preamble); + VideoName[] videoNames = MultipleEventsParser.parseVideoNames(preamble); - return new MarkAsUnwatchedCommand(videoName, moduleCode, lectureName); + if (videoNames.length == 1) { + return new MarkAsUnwatchedCommand(videoNames[0], moduleCode, lectureName); + } else { + return new MarkMultipleAsUnwatchedCommand(videoNames, moduleCode, lectureName); + } } catch (ParseException pe) { throw new ParseException( diff --git a/src/main/java/seedu/address/logic/parser/MarkAsWatchedCommandParser.java b/src/main/java/seedu/address/logic/parser/MarkAsWatchedCommandParser.java index 725e415da88..259be5b79f5 100644 --- a/src/main/java/seedu/address/logic/parser/MarkAsWatchedCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/MarkAsWatchedCommandParser.java @@ -6,8 +6,8 @@ import java.util.Optional; -import seedu.address.logic.commands.MarkAsWatchedCommand; -import seedu.address.logic.commands.MarkCommand; +import seedu.address.logic.commands.mark.MarkAsWatchedCommand; +import seedu.address.logic.commands.mark.MarkCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.lecture.LectureName; import seedu.address.model.module.ModuleCode; @@ -41,9 +41,9 @@ public MarkAsWatchedCommand parse(String args) throws ParseException { } ModuleCode moduleCode = ParserUtil.parseModuleCode(moduleCodeOptional.get()); LectureName lectureName = ParserUtil.parseLectureName(lectureNameOptional.get()); - VideoName videoName = ParserUtil.parseVideoName(preamble); + VideoName[] videoNames = MultipleEventsParser.parseVideoNames(preamble); - return new MarkAsWatchedCommand(videoName, moduleCode, lectureName); + return new MarkAsWatchedCommand(moduleCode, lectureName, videoNames); } catch (ParseException pe) { throw new ParseException( diff --git a/src/main/java/seedu/address/logic/parser/TrackerParser.java b/src/main/java/seedu/address/logic/parser/TrackerParser.java index e8bd45ed0d9..9ad0251746c 100644 --- a/src/main/java/seedu/address/logic/parser/TrackerParser.java +++ b/src/main/java/seedu/address/logic/parser/TrackerParser.java @@ -13,13 +13,13 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.commands.MarkAsUnwatchedCommand; -import seedu.address.logic.commands.MarkAsWatchedCommand; import seedu.address.logic.commands.TagCommand; import seedu.address.logic.commands.UntagCommand; import seedu.address.logic.commands.add.AddCommand; import seedu.address.logic.commands.delete.DeleteCommand; import seedu.address.logic.commands.edit.EditCommand; +import seedu.address.logic.commands.mark.MarkAsUnwatchedCommand; +import seedu.address.logic.commands.mark.MarkAsWatchedCommand; import seedu.address.logic.commands.navigation.BackNavCommand; import seedu.address.logic.commands.navigation.NavCommand; import seedu.address.logic.parser.exceptions.ParseException; diff --git a/src/main/java/seedu/address/model/Name.java b/src/main/java/seedu/address/model/Name.java index bd684dd8cfd..b7190bfd794 100644 --- a/src/main/java/seedu/address/model/Name.java +++ b/src/main/java/seedu/address/model/Name.java @@ -7,7 +7,7 @@ * Represents a non-blank name of an entity in the tracker. * Guarantees: immutable, is valid as declared in {@link #isValidName(String)}. */ -public class Name { +public class Name implements Comparable { /* * The first character of the name must not be a whitespace, @@ -38,6 +38,11 @@ public Name(String name, String validationRegex, String constraintsMesssage) { this.name = name; } + @Override + public int compareTo(Name o) { + return name.toLowerCase().compareTo(o.name.toLowerCase()); + } + @Override public String toString() { return name; diff --git a/src/main/java/seedu/address/model/SortedUniqueDataList.java b/src/main/java/seedu/address/model/SortedUniqueDataList.java new file mode 100644 index 00000000000..e5daa9d0d16 --- /dev/null +++ b/src/main/java/seedu/address/model/SortedUniqueDataList.java @@ -0,0 +1,35 @@ +package seedu.address.model; + +import java.util.function.BiPredicate; +import java.util.function.Supplier; + +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; +import seedu.address.model.exceptions.DataNotFoundException; +import seedu.address.model.exceptions.DuplicateDataException; + +/** + * A {@code UniqueDataList} that is sorted using {@code T#compareTo(T)}. + * + * @see UniqueDataList + */ +public class SortedUniqueDataList> extends UniqueDataList { + /** + * Constructs a {@code SortedUniqueDataList}. + * + * @param isSameChecker Checks if 2 data are the same. + * @param duplicateExceptionCreator Creates an exception to throw in the event that duplicate data is detected. + * @param notFoundExceptionCreator Creates an exception to throw in the event that a data is not found. + */ + public SortedUniqueDataList(BiPredicate isSameChecker, + Supplier duplicateExceptionCreator, + Supplier notFoundExceptionCreator) { + + super(isSameChecker, duplicateExceptionCreator, notFoundExceptionCreator); + } + + @Override + public ObservableList asUnmodifiableObservableList() { + return new SortedList(super.asUnmodifiableObservableList(), T::compareTo); + } +} diff --git a/src/main/java/seedu/address/model/lecture/Lecture.java b/src/main/java/seedu/address/model/lecture/Lecture.java index e3a0ecef225..6f8f4977155 100644 --- a/src/main/java/seedu/address/model/lecture/Lecture.java +++ b/src/main/java/seedu/address/model/lecture/Lecture.java @@ -21,7 +21,7 @@ * Represents a lecture of a module.

* Guarantees: details are present and not null, field values are validated, immutable with the exception of video list. */ -public class Lecture implements ReadOnlyLecture { +public class Lecture implements ReadOnlyLecture, Comparable { private final LectureName name; @@ -91,6 +91,11 @@ public boolean isSameLecture(Lecture other) { && other.getName().equals(getName()); } + @Override + public int compareTo(Lecture other) { + return getName().compareTo(other.getName()); + } + /** * Adds a video to the lecture. * The video must not already exist in the lecture. diff --git a/src/main/java/seedu/address/model/lecture/UniqueLectureList.java b/src/main/java/seedu/address/model/lecture/UniqueLectureList.java index 0f70fbcfaa4..2f61515a51b 100644 --- a/src/main/java/seedu/address/model/lecture/UniqueLectureList.java +++ b/src/main/java/seedu/address/model/lecture/UniqueLectureList.java @@ -2,13 +2,15 @@ import java.util.List; -import seedu.address.model.UniqueDataList; +import seedu.address.model.SortedUniqueDataList; import seedu.address.model.lecture.exceptions.DuplicateLectureException; import seedu.address.model.lecture.exceptions.LectureNotFoundException; // TODO: Test this /** - * A list of lectures that enforces uniqueness between its elements and does not allow nulls.

+ * A list of sorted lectures that enforces uniqueness between its elements and does not allow nulls.

+ * + * A lecture is sorted by using the {@code Lecture#compareTo(Lecture)} method. * * A lecture is considered unique by comparing using {@code Lecture#isSameLecture(Lecture)}. As such, adding and * updating of lectures uses {@code Lecture#isSameLecture(Lecture)} for equality so as to ensure that the lecture being @@ -19,9 +21,10 @@ * * Supports a minimal set of list operations.

* + * @see Lecture#compareTo(Lecture) * @see Lecture#isSameLecture(Lecture) */ -public class UniqueLectureList extends UniqueDataList { +public class UniqueLectureList extends SortedUniqueDataList { /** * Constructs a {@code UniqueLectureList}. diff --git a/src/main/java/seedu/address/model/module/Module.java b/src/main/java/seedu/address/model/module/Module.java index 5470c332b41..ad657cd17d2 100644 --- a/src/main/java/seedu/address/model/module/Module.java +++ b/src/main/java/seedu/address/model/module/Module.java @@ -23,7 +23,7 @@ * Represents a module in the tracker.

* Guarantees: details are not null, field values are validated, immutable with exception of lecture list. */ -public class Module implements ReadOnlyModule { +public class Module implements ReadOnlyModule, Comparable { private final ModuleCode code; @@ -103,6 +103,11 @@ public boolean isSameModule(Module other) { && other.getCode().equals(getCode()); } + @Override + public int compareTo(Module other) { + return getCode().compareTo(other.getCode()); + } + /** * Adds a lecture to the module.

* The lecture must not already exist in the module. diff --git a/src/main/java/seedu/address/model/module/ModuleCode.java b/src/main/java/seedu/address/model/module/ModuleCode.java index 94dfd46c93e..d95b0c55bf8 100644 --- a/src/main/java/seedu/address/model/module/ModuleCode.java +++ b/src/main/java/seedu/address/model/module/ModuleCode.java @@ -7,7 +7,7 @@ * Represents a module's code in the tracker.

* Guarantees: immutable, is valid as declared in {@link #isValidCode(String)}. */ -public class ModuleCode { +public class ModuleCode implements Comparable { public static final String MESSAGE_CONSTRAINTS = "Module codes should begin with uppercase alphabet characters, followed by numeric characters, optionally " @@ -39,6 +39,11 @@ public static boolean isValidCode(String test) { return test.matches(VALIDATION_REGEX); } + @Override + public int compareTo(ModuleCode o) { + return code.compareTo(o.code); + } + @Override public String toString() { return code; diff --git a/src/main/java/seedu/address/model/module/UniqueModuleList.java b/src/main/java/seedu/address/model/module/UniqueModuleList.java index ebfb71f1cf0..d26a97b4a15 100644 --- a/src/main/java/seedu/address/model/module/UniqueModuleList.java +++ b/src/main/java/seedu/address/model/module/UniqueModuleList.java @@ -2,13 +2,15 @@ import java.util.List; -import seedu.address.model.UniqueDataList; +import seedu.address.model.SortedUniqueDataList; import seedu.address.model.module.exceptions.DuplicateModuleException; import seedu.address.model.module.exceptions.ModuleNotFoundException; // TODO: Test this /** - * A list of modules that enforces uniqueness between its elements and does not allow nulls.

+ * A list of sorted modules that enforces uniqueness between its elements and does not allow nulls.

+ * + * A module is sorted by using the {@code Module#compareTo(Module)} method. * * A module is considered unique by comparing using {@code Module#isSameModule(Module)}. As such, adding and updating of * modules uses {@code Module#isSameModule(Module)} for equality so as to ensure that the module being added or updated @@ -19,9 +21,10 @@ * * Supports a minimal set of list operations.

* + * @see Module#compareTo(Module) * @see Module#isSameModule(Module) */ -public class UniqueModuleList extends UniqueDataList { +public class UniqueModuleList extends SortedUniqueDataList { /** * Constructs a {@code UniqueModuleList}. diff --git a/src/main/java/seedu/address/model/video/UniqueVideoList.java b/src/main/java/seedu/address/model/video/UniqueVideoList.java index b6d26c151b0..0b55cc7b763 100644 --- a/src/main/java/seedu/address/model/video/UniqueVideoList.java +++ b/src/main/java/seedu/address/model/video/UniqueVideoList.java @@ -2,13 +2,15 @@ import java.util.List; -import seedu.address.model.UniqueDataList; +import seedu.address.model.SortedUniqueDataList; import seedu.address.model.video.exceptions.DuplicateVideoException; import seedu.address.model.video.exceptions.VideoNotFoundException; // TODO: Test this /** - * A list of videos that enforces uniqueness between its elements and does not allow nulls.

+ * A list of sorted videos that enforces uniqueness between its elements and does not allow nulls.

+ * + * A video is sorted by using the {@code Video#compareTo(Video)} method. * * A video is considered unique by comparing using {@code Video#isSameVideo(Video)}. As such, adding and updating of * videos uses {@code Video#isSameVideo(Video)} for equality so as to ensure that the video being added or updated is @@ -19,9 +21,10 @@ * * Supports a minimal set of list operations.

* + * @see Video#compareTo(Video) * @see Video#isSameVideo(Video) */ -public class UniqueVideoList extends UniqueDataList

* Guarantees: details are present and not null, field values are validated, immutable. */ -public class Video { +public class Video implements Comparable

+ * + * This could occur if the platform is Linux. + */ + private void openUrlWithXdgOpen() { + try { + String[] command = new String[] {XDG_OPEN, USERGUIDE_URL}; + Runtime.getRuntime().exec(command); + } catch (IOException e) { + logger.warning("Cannot open user guide URL"); + } catch (SecurityException e) { + logger.warning("Cannot open user guide URL due to system's security manager"); + } + } } diff --git a/src/main/java/seedu/address/ui/LectureCard.java b/src/main/java/seedu/address/ui/LectureCard.java index a6cc0db9d4f..cd3a0d00631 100644 --- a/src/main/java/seedu/address/ui/LectureCard.java +++ b/src/main/java/seedu/address/ui/LectureCard.java @@ -4,6 +4,7 @@ import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; @@ -15,6 +16,8 @@ public class LectureCard extends UiPart { private static final String FXML = "LectureListCard.fxml"; + private static final String NO_VIDEOS_FOUND_TEXT = "No videos found"; + private static final String VIDEO_PROGRESS_FORMAT = "Watched %o/%o videos"; private final ReadOnlyLecture lecture; @@ -25,6 +28,8 @@ public class LectureCard extends UiPart { @FXML private Label lectureName; @FXML + private ProgressBar progressBar; + @FXML private Label progress; @FXML private FlowPane tags; @@ -45,12 +50,17 @@ public LectureCard(ReadOnlyLecture lecture, int displayedIndex) { private String getProgressText(ReadOnlyLecture lecture) { int videoCount = lecture.getVideoList().size(); - String progressText = "No videos added"; + String progressText = NO_VIDEOS_FOUND_TEXT; + + double progressPerc = 0; if (videoCount > 0) { int watched = lecture.getVideoList().filtered(vid -> vid.hasWatched()).size(); - progressText = String.format("Progress: %o/%o videos watched", watched, videoCount); + progressText = String.format(VIDEO_PROGRESS_FORMAT, watched, videoCount); + progressPerc = (double) watched / videoCount; } + + progressBar.setProgress(progressPerc); return progressText; } diff --git a/src/main/java/seedu/address/ui/ModuleCard.java b/src/main/java/seedu/address/ui/ModuleCard.java index 8ce5f359272..818e3688698 100644 --- a/src/main/java/seedu/address/ui/ModuleCard.java +++ b/src/main/java/seedu/address/ui/ModuleCard.java @@ -4,6 +4,7 @@ import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; @@ -15,6 +16,8 @@ public class ModuleCard extends UiPart { private static final String FXML = "ModuleListCard.fxml"; + private static final String NO_LECTURES_FOUND_TEXT = "No lectures found"; + private static final String LECTURE_PROGRESS_FORMAT = "Covered %o/%o lectures"; private final ReadOnlyModule module; @@ -27,6 +30,8 @@ public class ModuleCard extends UiPart { @FXML private Label moduleName; @FXML + private ProgressBar progressBar; + @FXML private Label progress; @FXML private FlowPane tags; @@ -40,24 +45,30 @@ public ModuleCard(ReadOnlyModule module, int displayedIndex) { id.setText(displayedIndex + ". "); moduleCode.setText(module.getCode().toString()); moduleName.setText(module.getName().toString()); - progress.setText(getProgressText(module)); + + setProgressUi(module); module.getTags().stream().sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } - private String getProgressText(ReadOnlyModule module) { + private void setProgressUi(ReadOnlyModule module) { int totalLectureCount = module.getLectureList().size(); + int lectureCompletedCount = 0; - if (totalLectureCount == 0) { - return "No lectures found!"; - } else { - int lectureCompletedCount = module.getLectureList().filtered(lecture -> { + if (totalLectureCount > 0) { + lectureCompletedCount = module.getLectureList().filtered(lecture -> { int watchCount = lecture.getVideoList().filtered(vid -> vid.hasWatched()).size(); - return watchCount == lecture.getVideoList().size(); + return watchCount > 0 && watchCount == lecture.getVideoList().size(); }).size(); - return String.format("Progress: %o/%o lectures covered", lectureCompletedCount, totalLectureCount); } + + progress.setText(getProgressText(lectureCompletedCount, totalLectureCount)); + progressBar.setProgress(totalLectureCount == 0 ? 0 : (double) lectureCompletedCount / totalLectureCount); + } + + private String getProgressText(int completed, int total) { + return total == 0 ? NO_LECTURES_FOUND_TEXT : String.format(LECTURE_PROGRESS_FORMAT, completed, total); } @Override diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 12061738cb6..81b8f5829c6 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,14 +1,24 @@ @font-face { - font-family: "Roboto"; - src: url('Roboto-Medium.ttf'); + font-family: "Roboto", sans-serif; + src: url('fonts/Roboto-Regular.ttf'); +} + +@font-face { + font-family: "Roboto Medium", sans-serif; + src: url('fonts/Roboto-Medium.ttf'); +} + +@font-face { + font-family: "Roboto Bold", sans-serif; + src: url('fonts/Roboto-Bold.ttf'); } .root { - -fx-font-family: "Roboto"; + -fx-font-family: 'Roboto'; } .background { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #292929; background-color: #292929; /* Used in the default.html file */ } @@ -95,26 +105,27 @@ } .list-cell { - -fx-label-padding: 0 0 0 0; + -fx-label-padding: 2 2 2 2; -fx-graphic-text-gap: 0; - -fx-padding: 0 0 0 0; + -fx-padding: 5 5 5 5; + -fx-background-color: #1B1B1B; } .list-cell:filled:even { -fx-background-color: #292929; + -fx-border-color: #4E4E4E; } .list-cell:filled:odd { -fx-background-color: #292929; + -fx-border-color: #4E4E4E; } .list-cell:filled:selected { - -fx-background-color: #424d5f; + -fx-background-color: #292929; } .list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; - -fx-border-width: 1; } .list-cell .label { @@ -122,8 +133,9 @@ } .cell_big_label { - -fx-font-size: 16px; - -fx-text-fill: #010504; + -fx-font-family: 'Roboto Medium'; + -fx-font-size: 18px; + -fx-text-fill: #010505; } .cell_small_label { @@ -131,6 +143,34 @@ -fx-text-fill: #010504; } +.cell_progress_label { + -fx-font-size: 11px; + -fx-text-fill: #010504; +} + +.progress { +} + +.progress-bar .bar { + -fx-padding: 0; + -fx-background-insets: 0; + -fx-background-color: linear-gradient(to left, derive(#5D81DF,100%), derive(#5D81DF,0%)); +} + +.progress-bar .track { + -fx-padding: 0; + -fx-background-insets: 0; + -fx-background-color: #6D6D6D; +} + +.card-header { + -fx-padding: 0 0 0 0; +} + +.card-tags { + -fx-padding: 10 0 10 0; +} + .stack-pane { -fx-background-color: #292929; } @@ -152,15 +192,28 @@ } .result-display { - -fx-background-color: transparent; + -fx-background-color: #1B1B1B; + -fx-control-inner-background: #1B1B1B; -fx-font-size: 13pt; -fx-text-fill: white; + -fx-border-color: #4E4E4E; + -fx-border-width: 1.5; + -fx-border-radius: 8; + -fx-background-insets: 0; + -fx-text-box-border: transparent; + -fx-faint-focus-color: transparent; + -fx-focus-color: transparent; } .result-display .label { -fx-text-fill: black !important; } +#resultDisplay .content { + -fx-background-radius: 0; + -fx-background-color: #1B1B1B; +} + .status-bar .label { -fx-text-fill: white; -fx-padding: 4px; @@ -196,7 +249,7 @@ } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: #292929 } .menu-bar .label { @@ -286,12 +339,16 @@ } .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-border-color:#505050; + -fx-border-radius: 32px; + -fx-background-color: transparent; + -fx-padding: 0; } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: #4a4a4a; -fx-background-insets: 3; + -fx-background-radius: 32px; } .scroll-bar .increment-button, @@ -320,9 +377,9 @@ } .context-label { + -fx-font-family: 'Roboto Bold'; -fx-font-size: 12px; -fx-text-fill: white; - -fx-font-weight: bold; } .context-label-container { @@ -354,7 +411,6 @@ -fx-border-width: 1.5; -fx-border-radius: 64; -fx-background-color: transparent #616060 transparent #616060; - -fx-background-insets: 0; -fx-font-size: 13pt; -fx-text-fill: white; } @@ -365,13 +421,6 @@ -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); } -#resultDisplay .content { - -fx-background-color: #1B1B1B; - -fx-border-color: #616060; - -fx-border-width: 1.5; - -fx-border-radius: 0; -} - #tags { -fx-hgap: 7; -fx-vgap: 3; diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 98e291e26be..21392094698 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -1,5 +1,10 @@ +@font-face { + font-family: "Roboto Medium"; + src: url('Roboto-Medium.ttf'); +} + .root { - -fx-font-family: 'Roboto'; + -fx-font-family: 'Roboto Medium'; } #copyButton, diff --git a/src/main/resources/view/LectureListCard.fxml b/src/main/resources/view/LectureListCard.fxml index 531c987a640..2c6203bac34 100644 --- a/src/main/resources/view/LectureListCard.fxml +++ b/src/main/resources/view/LectureListCard.fxml @@ -2,6 +2,8 @@ + + @@ -10,7 +12,7 @@ - + @@ -19,7 +21,7 @@ - + - - - + + + + - + - + @@ -55,10 +58,10 @@ - + - + diff --git a/src/main/resources/view/ModuleListCard.fxml b/src/main/resources/view/ModuleListCard.fxml index d6feb960143..f186ae72aba 100644 --- a/src/main/resources/view/ModuleListCard.fxml +++ b/src/main/resources/view/ModuleListCard.fxml @@ -2,6 +2,8 @@ + + @@ -10,7 +12,7 @@ - + @@ -19,7 +21,7 @@ - + - +