From df91a71eb72ee162af7d49a664c84f9956b98d50 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 12 Jun 2021 12:23:10 +0200 Subject: [PATCH] Introduce new system to create the context menus --- .../ui/StreamSourceSelectionDialog.java | 24 +- .../java/ctbrec/ui/action/OpenUrlAction.java | 15 + .../ctbrec/ui/action/SetStopDateAction.java | 2 +- .../java/ctbrec/ui/action/TriConsumer.java | 7 + .../ctbrec/ui/menu/ModelGroupMenuBuilder.java | 2 + .../ctbrec/ui/menu/ModelMenuContributor.java | 268 ++++++++++++++++++ .../main/java/ctbrec/ui/tabs/ThumbCell.java | 16 +- .../java/ctbrec/ui/tabs/ThumbOverviewTab.java | 127 ++------- .../ui/tabs/recorded/RecordedModelsTab.java | 18 +- .../ctbrec/recorder/NextGenLocalRecorder.java | 6 +- 10 files changed, 359 insertions(+), 126 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/action/OpenUrlAction.java create mode 100644 client/src/main/java/ctbrec/ui/action/TriConsumer.java create mode 100644 client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java diff --git a/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java b/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java index b23558fa..17fc2828 100644 --- a/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java +++ b/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java @@ -1,11 +1,10 @@ package ctbrec.ui; -import java.io.InputStream; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; -import java.util.function.Function; +import java.util.function.Consumer; import ctbrec.GlobalThreadPool; import ctbrec.Model; @@ -23,7 +22,7 @@ public class StreamSourceSelectionDialog { private StreamSourceSelectionDialog() { } - public static void show(Scene parent, Model model, Function onSuccess, Function onFail) { + public static void show(Scene parent, Model model, Consumer onSuccess, Consumer onFail) { Task> selectStreamSource = new Task>() { @Override protected List call() throws Exception { @@ -37,14 +36,14 @@ public class StreamSourceSelectionDialog { List sources; try { sources = selectStreamSource.get(); - int selectedIndex = model.getStreamUrlIndex() > -1 ? Math.min(model.getStreamUrlIndex(), sources.size() - 1) : sources.size() - 1; - ChoiceDialog choiceDialog = new ChoiceDialog<>(sources.get(selectedIndex), sources); + var selectedIndex = model.getStreamUrlIndex() > -1 ? Math.min(model.getStreamUrlIndex(), sources.size() - 1) : sources.size() - 1; + var choiceDialog = new ChoiceDialog(sources.get(selectedIndex), sources); choiceDialog.setTitle("Stream Quality"); - choiceDialog.setHeaderText("Select your preferred stream quality"); + choiceDialog.setHeaderText("Select your preferred stream quality for " + model.getDisplayName()); choiceDialog.setResizable(true); - Stage stage = (Stage) choiceDialog.getDialogPane().getScene().getWindow(); + var stage = (Stage) choiceDialog.getDialogPane().getScene().getWindow(); stage.getScene().getStylesheets().addAll(parent.getStylesheets()); - InputStream icon = Dialogs.class.getResourceAsStream("/icon.png"); + var icon = Dialogs.class.getResourceAsStream("/icon.png"); stage.getIcons().add(new Image(icon)); Optional selectedSource = choiceDialog.showAndWait(); if (selectedSource.isPresent()) { @@ -53,16 +52,17 @@ public class StreamSourceSelectionDialog { index = sources.indexOf(selectedSource.get()); } model.setStreamUrlIndex(index); - onSuccess.apply(model); + System.err.println(model + " " + model.getStreamUrlIndex()); + onSuccess.accept(model); } } catch (ExecutionException e1) { - onFail.apply(e1); + onFail.accept(e1); } catch (InterruptedException e1) { Thread.currentThread().interrupt(); - onFail.apply(e1); + onFail.accept(e1); } }); - selectStreamSource.setOnFailed(e -> onFail.apply(selectStreamSource.getException())); + selectStreamSource.setOnFailed(e -> onFail.accept(selectStreamSource.getException())); GlobalThreadPool.submit(selectStreamSource); } diff --git a/client/src/main/java/ctbrec/ui/action/OpenUrlAction.java b/client/src/main/java/ctbrec/ui/action/OpenUrlAction.java new file mode 100644 index 00000000..a0acc666 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/OpenUrlAction.java @@ -0,0 +1,15 @@ +package ctbrec.ui.action; + +import java.util.List; + +import ctbrec.Model; +import ctbrec.ui.DesktopIntegration; +import javafx.scene.Node; + +public class OpenUrlAction extends ModelMassEditAction { + + public OpenUrlAction(Node source, List models) { + super(source, models); + action = m -> DesktopIntegration.open(m.getUrl()); + } +} diff --git a/client/src/main/java/ctbrec/ui/action/SetStopDateAction.java b/client/src/main/java/ctbrec/ui/action/SetStopDateAction.java index a30ae45b..df183fad 100644 --- a/client/src/main/java/ctbrec/ui/action/SetStopDateAction.java +++ b/client/src/main/java/ctbrec/ui/action/SetStopDateAction.java @@ -68,7 +68,7 @@ public class SetStopDateAction { var localDate = LocalDateTime.ofInstant(model.getRecordUntil(), ZoneId.systemDefault()); datePicker.setDateTimeValue(localDate); } - boolean userClickedOk = Dialogs.showCustomInput(source.getScene(), "Stop Recording at", gridPane); + boolean userClickedOk = Dialogs.showCustomInput(source.getScene(), "Stop Recording of " + model.getDisplayName() + " at", gridPane); return CompletableFuture.supplyAsync(() -> { if (userClickedOk) { SubsequentAction action = pauseButton.isSelected() ? PAUSE : REMOVE; diff --git a/client/src/main/java/ctbrec/ui/action/TriConsumer.java b/client/src/main/java/ctbrec/ui/action/TriConsumer.java new file mode 100644 index 00000000..0125400b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/TriConsumer.java @@ -0,0 +1,7 @@ +package ctbrec.ui.action; + +@FunctionalInterface +public interface TriConsumer { + + void accept(T t, U u, V v); +} diff --git a/client/src/main/java/ctbrec/ui/menu/ModelGroupMenuBuilder.java b/client/src/main/java/ctbrec/ui/menu/ModelGroupMenuBuilder.java index 03307196..24de7509 100644 --- a/client/src/main/java/ctbrec/ui/menu/ModelGroupMenuBuilder.java +++ b/client/src/main/java/ctbrec/ui/menu/ModelGroupMenuBuilder.java @@ -1,6 +1,7 @@ package ctbrec.ui.menu; import java.util.Objects; +import java.util.Optional; import java.util.function.Consumer; import ctbrec.Model; @@ -44,6 +45,7 @@ public class ModelGroupMenuBuilder { Objects.requireNonNull(model, "Model has to be set"); Objects.requireNonNull(recorder, "Recorder has to be set"); Objects.requireNonNull(source, "Node has to be set"); + callback = Optional.ofNullable(callback).orElse(m -> {}); var menu = new Menu("Group"); diff --git a/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java new file mode 100644 index 00000000..b9e98ab2 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/menu/ModelMenuContributor.java @@ -0,0 +1,268 @@ +package ctbrec.ui.menu; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.GlobalThreadPool; +import ctbrec.Model; +import ctbrec.ModelGroup; +import ctbrec.recorder.Recorder; +import ctbrec.ui.AutosizeAlert; +import ctbrec.ui.SiteUiFactory; +import ctbrec.ui.StreamSourceSelectionDialog; +import ctbrec.ui.action.AddToGroupAction; +import ctbrec.ui.action.PlayAction; +import ctbrec.ui.action.SetStopDateAction; +import ctbrec.ui.action.StartRecordingAction; +import ctbrec.ui.action.StopRecordingAction; +import ctbrec.ui.action.TriConsumer; +import ctbrec.ui.controls.Dialogs; +import ctbrec.ui.tabs.FollowedTab; +import javafx.application.Platform; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; + +public class ModelMenuContributor { + + private static final Logger LOG = LoggerFactory.getLogger(ModelMenuContributor.class); + + private static final String COULDNT_START_STOP_RECORDING = "Couldn't start/stop recording"; + private static final String ERROR = "Error"; + + private Config config; + private Recorder recorder; + private Node source; + private Consumer startStopCallback; + private TriConsumer followCallback; + + private ModelMenuContributor(Node source, Config config, Recorder recorder) { + this.source = source; + this.config = config; + this.recorder = recorder; + } + + public static ModelMenuContributor newContributor(Node source, Config config, Recorder recorder) { + return new ModelMenuContributor(source, config, recorder); + } + + public ModelMenuContributor withStartStopCallback(Consumer callback) { + this.startStopCallback = callback; + return this; + } + + public ModelMenuContributor withFollowCallback(TriConsumer callback) { + this.followCallback = callback; + return this; + } + + public void contributeToMenu(List selectedModels, ContextMenu menu) { + startStopCallback = Optional.ofNullable(startStopCallback).orElse(m -> {}); + followCallback = Optional.ofNullable(followCallback).orElse((m, f, s) -> {}); + addOpenInPlayer(menu, selectedModels); + menu.getItems().add(new SeparatorMenuItem()); + addStartOrStop(menu, selectedModels); + addStartRecordingWithTimeLimit(menu, selectedModels); + addStartPaused(menu, selectedModels); + addRecordLater(menu, selectedModels); + addPauseResume(menu, selectedModels); + addGroupMenu(menu, selectedModels); + menu.getItems().add(new SeparatorMenuItem()); + addFollowUnfollow(menu, selectedModels); + } + + private void addFollowUnfollow(ContextMenu menu, List selectedModels) { + var site = selectedModels.get(0).getSite(); + if (site.supportsFollow()) { + var follow = new MenuItem("Follow"); + follow.setOnAction(e -> follow(selectedModels, true)); + var unfollow = new MenuItem("Unfollow"); + unfollow.setOnAction(e -> follow(selectedModels, false)); + + var followOrUnFollow = (source instanceof FollowedTab) ? unfollow : follow; // TODO source instanceof FollowedTab does not work + followOrUnFollow.setDisable(!site.credentialsAvailable()); + menu.getItems().add(followOrUnFollow); + followOrUnFollow.setDisable(!site.credentialsAvailable()); + } + } + + protected void follow(List selectedModels, boolean follow) { + for (Model model : selectedModels) { + follow(model, follow); + } + } + + CompletableFuture follow(Model model, boolean follow) { + source.setCursor(Cursor.WAIT); + return CompletableFuture.supplyAsync(() -> { + var success = true; + try { + if (follow) { + SiteUiFactory.getUi(model.getSite()).login(); + boolean followed = model.follow(); + if (followed) { + success = true; + } else { + Dialogs.showError(source.getScene(), "Couldn't follow model", "", null); + success = false; + } + } else { + SiteUiFactory.getUi(model.getSite()).login(); + boolean unfollowed = model.unfollow(); + if (unfollowed) { + success = true; + } else { + Dialogs.showError(source.getScene(), "Couldn't unfollow model", "", null); + success = false; + } + } + return success; + } catch (Exception e1) { + LOG.error("Couldn't follow/unfollow model {}", model.getName(), e1); + String msg = "I/O error while following/unfollowing model " + model.getName() + ": "; + Dialogs.showError(source.getScene(), "Couldn't follow/unfollow model", msg, e1); + return false; + } finally { + final boolean result = success; + Platform.runLater(() -> { + source.setCursor(Cursor.DEFAULT); + followCallback.accept(model, follow, result); + }); + } + }, GlobalThreadPool.get()); + } + + private void addGroupMenu(ContextMenu menu, List selectedModels) { + var model = selectedModels.get(0); + var addToGroup = new MenuItem("Add to group"); + addToGroup.setOnAction(e -> new AddToGroupAction(source, recorder, model).execute()); + var groupSubMenu = new ModelGroupMenuBuilder().model(model).recorder(recorder).node(source).build(); + Optional modelGroup = recorder.getModelGroup(model); + menu.getItems().add(modelGroup.isEmpty() ? addToGroup : groupSubMenu); + } + + private void addPauseResume(ContextMenu menu, List selectedModels) { + var first = selectedModels.get(0); + if (recorder.isTracked(first)) { + var pause = new MenuItem("Pause Recording"); + pause.setOnAction(e -> pauseResumeAction(selectedModels, true)); + var resume = new MenuItem("Resume Recording"); + resume.setOnAction(e -> pauseResumeAction(selectedModels, false)); + var pauseResume = recorder.isSuspended(first) ? resume : pause; + menu.getItems().add(pauseResume); + } + } + + private void pauseResumeAction(List selectedModels, boolean pause) { + selectedModels.forEach(m -> m.setSuspended(pause)); + startStopAction(selectedModels, true); + } + + private void addRecordLater(ContextMenu menu, List selectedModels) { + var first = selectedModels.get(0); + var recordLater = new MenuItem("Record Later"); + recordLater.setOnAction(e -> recordLater(selectedModels, true)); + var removeRecordLater = new MenuItem("Forget Model"); + removeRecordLater.setOnAction(e -> recordLater(selectedModels, false)); + var addRemoveBookmark = recorder.isMarkedForLaterRecording(first) ? removeRecordLater : recordLater; + if (recorder.isTracked(first)) { + menu.getItems().add(recordLater); + } else { + menu.getItems().add(addRemoveBookmark); + } + } + + private void recordLater(List selectedModels, boolean recordLater) { + selectedModels.forEach(m -> m.setMarkedForLaterRecording(recordLater)); + startStopAction(selectedModels, recordLater); + } + + private void addStartPaused(ContextMenu menu, List selectedModels) { + if (!recorder.isTracked(selectedModels.get(0))) { + var addPaused = new MenuItem("Add in paused state"); + menu.getItems().add(addPaused); + addPaused.setOnAction(e -> { + for (Model model : selectedModels) { + model.setMarkedForLaterRecording(false); + model.setSuspended(true); + } + startStopAction(selectedModels, true); + }); + } + } + + private void addStartRecordingWithTimeLimit(ContextMenu menu, List selectedModels) { + var text = recorder.isTracked(selectedModels.get(0)) ? "Record Until" : "Start Recording Until"; + var start = new MenuItem(text); + menu.getItems().add(start); + start.setOnAction(e -> { + startStopAction(selectedModels, true); + selectedModels.forEach(m -> new SetStopDateAction(source, m, recorder).execute()); + }); + } + + private void addOpenInPlayer(ContextMenu menu, List selectedModels) { + var openInPlayer = new MenuItem("Open in Player"); + openInPlayer.setOnAction(e -> selectedModels.forEach(m -> new PlayAction(source, m).execute())); + menu.getItems().add(openInPlayer); + + if (config.getSettings().singlePlayer && selectedModels.size() > 1) { + openInPlayer.setDisable(true); + } + } + + private void addStartOrStop(ContextMenu menu, List selectedModels) { + var start = new MenuItem("Start Recording"); + start.setOnAction(e -> { + for (Model model : selectedModels) { + model.setMarkedForLaterRecording(false); + model.setSuspended(false); + } + startStopAction(selectedModels, true); + }); + var stop = new MenuItem("Stop Recording"); + stop.setOnAction(e -> startStopAction(selectedModels, false)); + var startStop = recorder.isTracked(selectedModels.get(0)) ? stop : start; + menu.getItems().add(startStop); + } + + private void startStopAction(List selection, boolean start) { + if (start) { + boolean selectSource = Config.getInstance().getSettings().chooseStreamQuality; + if (selectSource) { + for (Model model : selection) { + Consumer onSuccess = modl -> startRecording(List.of(modl)); + Consumer onFail = throwable -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, source.getScene()); + alert.setTitle(ERROR); + alert.setHeaderText(COULDNT_START_STOP_RECORDING); + alert.setContentText("I/O error while starting/stopping the recording: " + throwable.getLocalizedMessage()); + alert.showAndWait(); + }; + StreamSourceSelectionDialog.show(source.getScene(), model, onSuccess, onFail); + } + } else { + startRecording(selection); + } + } else { + stopRecording(selection); + } + } + + private void startRecording(List models) { + new StartRecordingAction(source, models, recorder).execute(startStopCallback); + } + + private void stopRecording(List models) { + new StopRecordingAction(source, models, recorder).execute(startStopCallback); + } +} diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java index 12498677..458b46dd 100644 --- a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java @@ -10,7 +10,7 @@ import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.function.Function; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -335,8 +335,12 @@ public class ThumbCell extends StackPane { public void setSelected(boolean selected) { selectionProperty.set(selected); - selectionOverlay.getStyleClass().add("selection-background"); selectionOverlay.setOpacity(selected ? .75 : 0); + if (selected) { + selectionOverlay.getStyleClass().add("selection-background"); + } else { + selectionOverlay.getStyleClass().remove("selection-background"); + } } public boolean isSelected() { @@ -512,17 +516,13 @@ public class ThumbCell extends StackPane { boolean selectSource = Config.getInstance().getSettings().chooseStreamQuality; if (selectSource && start) { - Function onSuccess = modl -> { - startStopActionAsync(modl, true); - return null; - }; - Function onFail = throwable -> { + Consumer onSuccess = modl -> startStopActionAsync(modl, true); + Consumer onFail = throwable -> { Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getScene()); alert.setTitle(ERROR); alert.setHeaderText(COULDNT_START_STOP_RECORDING); alert.setContentText("I/O error while starting/stopping the recording: " + throwable.getLocalizedMessage()); alert.showAndWait(); - return null; }; StreamSourceSelectionDialog.show(getScene(), model, onSuccess, onFail); } else { diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java index b190540a..676653b5 100644 --- a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java @@ -35,17 +35,15 @@ import ctbrec.ui.AutosizeAlert; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SiteUiFactory; import ctbrec.ui.TokenLabel; -import ctbrec.ui.action.AddToGroupAction; import ctbrec.ui.action.IgnoreModelsAction; import ctbrec.ui.action.OpenRecordingsDir; -import ctbrec.ui.action.SetStopDateAction; import ctbrec.ui.action.TipAction; import ctbrec.ui.controls.CustomMouseBehaviorContextMenu; import ctbrec.ui.controls.FasterVerticalScrollPaneSkin; import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.SearchPopover; import ctbrec.ui.controls.SearchPopoverTreeList; -import ctbrec.ui.menu.ModelGroupMenuBuilder; +import ctbrec.ui.menu.ModelMenuContributor; import javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.ParallelTransition; @@ -64,6 +62,7 @@ import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.Alert; @@ -74,7 +73,6 @@ import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; -import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.Tab; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; @@ -461,42 +459,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { private ContextMenu createContextMenu(ThumbCell cell) { var model = cell.getModel(); - boolean modelIsTrackedByRecorder = recorder.isTracked(model); - - var openInPlayer = new MenuItem("Open in Player"); - openInPlayer.setOnAction(e -> startPlayer(getSelectedThumbCells(cell))); - - var start = new MenuItem("Start Recording"); - start.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), true)); - - var stop = new MenuItem("Stop Recording"); - stop.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), false)); - var startStop = recorder.isTracked(model) ? stop : start; - - var recordUntil = new MenuItem("Start Recording Until"); - recordUntil.setOnAction(e -> startRecordingWithTimeLimit(getSelectedThumbCells(cell))); - var addPaused = new MenuItem("Add in paused state"); - addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell))); - var recordLater = new MenuItem("Record Later"); - recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), true)); - var removeRecordLater = new MenuItem("Forget Model"); - removeRecordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), false)); - var addRemoveBookmark = recorder.isMarkedForLaterRecording(model) ? removeRecordLater : recordLater; - - var pause = new MenuItem("Pause Recording"); - pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true)); - var resume = new MenuItem("Resume Recording"); - resume.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), false)); - var pauseResume = recorder.isSuspended(model) ? resume : pause; - - var addToGroup = new MenuItem("Add to group"); - addToGroup.setOnAction(e -> new AddToGroupAction(this.getContent(), recorder, model).execute()); - var groupSubMenu = new ModelGroupMenuBuilder().model(model).recorder(recorder).node(grid).build(); - - var follow = new MenuItem("Follow"); - follow.setOnAction(e -> follow(getSelectedThumbCells(cell), true)); - var unfollow = new MenuItem("Unfollow"); - unfollow.setOnAction(e -> follow(getSelectedThumbCells(cell), false)); + var selectedModels = getSelectedThumbCells(cell).stream().map(ThumbCell::getModel).collect(Collectors.toList()); var ignore = new MenuItem("Ignore"); ignore.setOnAction(e -> ignore(getSelectedThumbCells(cell))); @@ -511,26 +474,28 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { var openInBrowser = createOpenInBrowser(cell); var sendTip = createTipMenuItem(cell); - configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip); + configureItemsForSelection(cell, copyUrl, sendTip); ContextMenu contextMenu = new CustomMouseBehaviorContextMenu(); contextMenu.setAutoHide(true); contextMenu.setHideOnEscape(true); contextMenu.setAutoFix(true); - contextMenu.getItems().addAll(openInPlayer, new SeparatorMenuItem(), startStop); - if (modelIsTrackedByRecorder) { - contextMenu.getItems().addAll(pauseResume, recordLater); - } else { - contextMenu.getItems().addAll(recordUntil, addPaused, addRemoveBookmark); - } - Optional modelGroup = recorder.getModelGroup(model); - contextMenu.getItems().add(modelGroup.isEmpty() ? addToGroup : groupSubMenu); - contextMenu.getItems().add(new SeparatorMenuItem()); - if (site.supportsFollow()) { - var followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow; - followOrUnFollow.setDisable(!site.credentialsAvailable()); - contextMenu.getItems().add(followOrUnFollow); - } + + ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) + .withStartStopCallback(m -> { + getTabPane().setCursor(Cursor.DEFAULT); + getSelectedThumbCells(cell).forEach(ThumbCell::update); + }) + .withFollowCallback( (mdl, fllw, success) -> { + if (Boolean.TRUE.equals(fllw) && Boolean.TRUE.equals(success)) { + getThumbCell(mdl).ifPresent(this::showAddToFollowedAnimation); + } + if (Boolean.FALSE.equals(fllw)) { + selectedThumbCells.clear(); + } + }) + .contributeToMenu(selectedModels, contextMenu); + if (site.supportsTips()) { contextMenu.getItems().add(sendTip); } @@ -544,18 +509,16 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { return contextMenu; } - private void recordLater(List list, boolean recordLater) { - for (ThumbCell cell : list) { - cell.recordLater(recordLater); - } - } - - private void startRecordingWithTimeLimit(List list) { - for (ThumbCell cell : list) { - cell.getModel().setMarkedForLaterRecording(false); - cell.startStopAction(true); - new SetStopDateAction(cell, cell.getModel(), recorder).execute(); + private Optional getThumbCell(Model model) { + for (Node node : grid.getChildren()) { + if (node instanceof ThumbCell) { + var thumbCell = (ThumbCell) node; + if (Objects.equals(thumbCell.getModel(), model)) { + return Optional.of(thumbCell); + } + } } + return Optional.empty(); } private void refresh() { @@ -570,12 +533,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { * check, if other cells are selected, too. in that case, we have to disable menu items, which make sense only for single selections. but only do that, if * the popup has been triggered on a selected cell. otherwise remove the selection and show the normal menu */ - private void configureItemsForSelection(ThumbCell cell, MenuItem openInPlayer, MenuItem copyUrl, MenuItem sendTip) { + private void configureItemsForSelection(ThumbCell cell, MenuItem copyUrl, MenuItem sendTip) { if (selectedThumbCells.size() > 1 || selectedThumbCells.size() == 1 && selectedThumbCells.get(0) != cell) { if (cell.isSelected()) { - if (Config.getInstance().getSettings().singlePlayer) { - openInPlayer.setDisable(true); - } copyUrl.setDisable(true); sendTip.setDisable(true); } else { @@ -616,6 +576,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } protected void follow(List selection, boolean follow) { + for (ThumbCell thumbCell : selection) { thumbCell.follow(follow).thenAccept(success -> { if (follow && Boolean.TRUE.equals(success)) { @@ -691,32 +652,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { return 0; } - private void startStopAction(List selection, boolean start) { - for (ThumbCell thumbCell : selection) { - thumbCell.getModel().setSuspended(false); - thumbCell.getModel().setMarkedForLaterRecording(false); - thumbCell.startStopAction(start); - } - } - - private void addPaused(List selection) { - for (ThumbCell thumbCell : selection) { - thumbCell.addInPausedState(); - } - } - - private void pauseResumeAction(List selection, boolean pause) { - for (ThumbCell thumbCell : selection) { - thumbCell.pauseResumeAction(pause); - } - } - - private void startPlayer(List selection) { - for (ThumbCell thumbCell : selection) { - thumbCell.startPlayer(); - } - } - private final EventHandler mouseClickListener = e -> { ThumbCell cell = (ThumbCell) e.getSource(); if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) { diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java index dc026e47..eb125c2e 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java @@ -17,7 +17,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Function; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -32,7 +32,6 @@ import ctbrec.StringUtil; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.ui.AutosizeAlert; -import ctbrec.ui.DesktopIntegration; import ctbrec.ui.JavaFxModel; import ctbrec.ui.PreviewPopupHandler; import ctbrec.ui.StreamSourceSelectionDialog; @@ -42,6 +41,7 @@ import ctbrec.ui.action.EditNotesAction; import ctbrec.ui.action.FollowAction; import ctbrec.ui.action.IgnoreModelsAction; import ctbrec.ui.action.OpenRecordingsDir; +import ctbrec.ui.action.OpenUrlAction; import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.RemoveTimeLimitAction; @@ -662,7 +662,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { var removeTimeLimit = new MenuItem("Remove Time Limit"); removeTimeLimit.setOnAction(e -> removeTimeLimit(selectedModels.get(0))); var openInBrowser = new MenuItem("Open in Browser"); - openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl())); + openInBrowser.setOnAction(e -> openInBrowser(selectedModels)); var openInPlayer = new MenuItem("Open in Player"); openInPlayer.setOnAction(e -> openInPlayer(selectedModels.get(0))); var switchStreamSource = new MenuItem("Switch resolution"); @@ -698,7 +698,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { if (selectedModels.size() > 1) { copyUrl.setDisable(true); openInPlayer.setDisable(true); - openInBrowser.setDisable(true); switchStreamSource.setDisable(true); notes.setDisable(true); } @@ -706,6 +705,11 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { return menu; } + private void openInBrowser(ObservableList selectedModels) { + List models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList()); + new OpenUrlAction(getTabPane(), models).execute(); + } + private Menu createModelGroupMenu(ObservableList selectedModels) { return new ModelGroupMenuBuilder() // .model(selectedModels.get(0)) // @@ -760,19 +764,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { return; } - Function onSuccess = m -> { + Consumer onSuccess = m -> { try { recorder.switchStreamSource(m); } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { LOG.error(couldntSwitchHeaderText, e); showStreamSwitchErrorDialog(e); } - return null; }; - Function onFail = t -> { + Consumer onFail = t -> { LOG.error(couldntSwitchHeaderText, t); showStreamSwitchErrorDialog(t); - return null; }; StreamSourceSelectionDialog.show(getTabPane().getScene(), fxModel.getDelegate(), onSuccess, onFail); } diff --git a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java index b6f2dc4c..4d61ea04 100644 --- a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java @@ -266,7 +266,11 @@ public class NextGenLocalRecorder implements Recorder { if (existing.isPresent()) { existing.get().setSuspended(model.isSuspended()); existing.get().setMarkedForLaterRecording(model.isMarkedForLaterRecording()); - startRecordingProcess(existing.get()); + if (model.isMarkedForLaterRecording() && getRecordingProcesses().containsKey(model)) { + stopRecording(model); + } else { + startRecordingProcess(existing.get()); + } } else { LOG.info("Model {} added", model); recorderLock.lock();