475 lines
19 KiB
Java
475 lines
19 KiB
Java
package ctbrec.ui.menu;
|
|
|
|
import java.io.IOException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ExecutionException;
|
|
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.DesktopIntegration;
|
|
import ctbrec.ui.SiteUiFactory;
|
|
import ctbrec.ui.StreamSourceSelectionDialog;
|
|
import ctbrec.ui.action.AddToGroupAction;
|
|
import ctbrec.ui.action.EditNotesAction;
|
|
import ctbrec.ui.action.IgnoreModelsAction;
|
|
import ctbrec.ui.action.MarkForLaterRecordingAction;
|
|
import ctbrec.ui.action.OpenRecordingsDir;
|
|
import ctbrec.ui.action.PauseAction;
|
|
import ctbrec.ui.action.PlayAction;
|
|
import ctbrec.ui.action.RemoveTimeLimitAction;
|
|
import ctbrec.ui.action.ResumeAction;
|
|
import ctbrec.ui.action.SetStopDateAction;
|
|
import ctbrec.ui.action.StartRecordingAction;
|
|
import ctbrec.ui.action.StopRecordingAction;
|
|
import ctbrec.ui.action.TipAction;
|
|
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;
|
|
import javafx.scene.control.TabPane;
|
|
import javafx.scene.input.Clipboard;
|
|
import javafx.scene.input.ClipboardContent;
|
|
|
|
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<Model> startStopCallback;
|
|
private TriConsumer<Model, Boolean, Boolean> followCallback;
|
|
private Consumer<Model> ignoreCallback;
|
|
private boolean removeWithIgnore = false;
|
|
private Runnable callback;
|
|
|
|
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<Model> callback) {
|
|
this.startStopCallback = callback;
|
|
return this;
|
|
}
|
|
|
|
public ModelMenuContributor withFollowCallback(TriConsumer<Model, Boolean, Boolean> callback) {
|
|
this.followCallback = callback;
|
|
return this;
|
|
}
|
|
|
|
public ModelMenuContributor withIgnoreCallback(Consumer<Model> ignoreCallback) {
|
|
this.ignoreCallback = ignoreCallback;
|
|
return this;
|
|
}
|
|
|
|
public ModelMenuContributor removeModelAfterIgnore(boolean yes) {
|
|
this.removeWithIgnore = yes;
|
|
return this;
|
|
}
|
|
|
|
public void contributeToMenu(List<Model> selectedModels, ContextMenu menu) {
|
|
startStopCallback = Optional.ofNullable(startStopCallback).orElse(m -> {});
|
|
followCallback = Optional.ofNullable(followCallback).orElse((m, f, s) -> {});
|
|
ignoreCallback = Optional.ofNullable(ignoreCallback).orElse(m -> {});
|
|
callback = Optional.ofNullable(callback).orElse(() -> {});
|
|
addOpenInPlayer(menu, selectedModels);
|
|
addOpenInBrowser(menu, selectedModels);
|
|
addCopyUrl(menu, selectedModels);
|
|
menu.getItems().add(new SeparatorMenuItem());
|
|
|
|
addStartOrStop(menu, selectedModels);
|
|
addStartRecordingWithTimeLimit(menu, selectedModels);
|
|
addRemoveTimeLimit(menu, selectedModels);
|
|
addSwitchStreamSource(menu, selectedModels);
|
|
addStartPaused(menu, selectedModels);
|
|
addRecordLater(menu, selectedModels);
|
|
addPauseResume(menu, selectedModels);
|
|
addGroupMenu(menu, selectedModels);
|
|
menu.getItems().add(new SeparatorMenuItem());
|
|
|
|
addFollowUnfollow(menu, selectedModels);
|
|
addSendTip(menu, selectedModels);
|
|
addIgnore(menu, selectedModels);
|
|
addOpenRecDir(menu, selectedModels);
|
|
addNotes(menu, selectedModels);
|
|
}
|
|
|
|
public ModelMenuContributor afterwards(Runnable callback) {
|
|
this.callback = callback;
|
|
return this;
|
|
}
|
|
|
|
private void addNotes(ContextMenu menu, List<Model> selectedModels) {
|
|
var notes = new MenuItem("Notes");
|
|
notes.setDisable(selectedModels.size() != 1);
|
|
notes.setOnAction(e -> new EditNotesAction(source, selectedModels.get(0), callback).execute());
|
|
menu.getItems().add(notes);
|
|
}
|
|
|
|
private void addOpenRecDir(ContextMenu menu, List<Model> selectedModels) {
|
|
if (selectedModels == null || selectedModels.isEmpty()) {
|
|
return;
|
|
}
|
|
var model = selectedModels.get(0);
|
|
var openRecDir = new MenuItem("Open recording directory");
|
|
openRecDir.setDisable(selectedModels.size() != 1);
|
|
openRecDir.setOnAction(e -> {
|
|
new OpenRecordingsDir(source, model).execute();
|
|
executeCallback();
|
|
});
|
|
menu.getItems().add(openRecDir);
|
|
}
|
|
|
|
private void addOpenInBrowser(ContextMenu menu, List<Model> selectedModels) {
|
|
var openInBrowser = new MenuItem("Open in browser");
|
|
openInBrowser.setOnAction(e -> selectedModels.forEach(model -> DesktopIntegration.open(model.getUrl())));
|
|
menu.getItems().add(openInBrowser);
|
|
}
|
|
|
|
private void addCopyUrl(ContextMenu menu, List<Model> selectedModels) {
|
|
if (selectedModels == null || selectedModels.isEmpty()) {
|
|
return;
|
|
}
|
|
var copyUrl = new MenuItem("Copy URL");
|
|
copyUrl.setOnAction(e -> {
|
|
var sb = new StringBuilder();
|
|
for (Model model : selectedModels) {
|
|
sb.append(model.getUrl()).append('\n');
|
|
}
|
|
sb.deleteCharAt(sb.length() - 1);
|
|
final var content = new ClipboardContent();
|
|
content.putString(sb.toString());
|
|
Clipboard.getSystemClipboard().setContent(content);
|
|
});
|
|
menu.getItems().add(copyUrl);
|
|
}
|
|
|
|
private void addSendTip(ContextMenu menu, List<Model> selectedModels) {
|
|
var model = selectedModels.get(0);
|
|
var site = model.getSite();
|
|
if (site.supportsTips()) {
|
|
var sendTip = new MenuItem("Send Tip");
|
|
sendTip.setOnAction(e -> new TipAction(model, source).execute());
|
|
sendTip.setDisable(!site.credentialsAvailable());
|
|
sendTip.setDisable(selectedModels.size() != 1);
|
|
menu.getItems().add(sendTip);
|
|
}
|
|
}
|
|
|
|
private void addIgnore(ContextMenu menu, List<Model> selectedModels) {
|
|
var ignore = new MenuItem("Ignore");
|
|
ignore.setOnAction(e -> ignore(selectedModels));
|
|
menu.getItems().add(ignore);
|
|
}
|
|
|
|
private void ignore(List<Model> selectedModels) {
|
|
new IgnoreModelsAction(source, selectedModels, recorder, removeWithIgnore).execute(ignoreCallback);
|
|
}
|
|
|
|
private void addFollowUnfollow(ContextMenu menu, List<Model> 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 = isFollowedTab() ? unfollow : follow;
|
|
followOrUnFollow.setDisable(!site.credentialsAvailable());
|
|
menu.getItems().add(followOrUnFollow);
|
|
followOrUnFollow.setDisable(!site.credentialsAvailable());
|
|
}
|
|
}
|
|
|
|
private boolean isFollowedTab() {
|
|
if (source instanceof TabPane) {
|
|
var tabPane = (TabPane) source;
|
|
return tabPane.getSelectionModel().getSelectedItem() instanceof FollowedTab;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected void follow(List<Model> selectedModels, boolean follow) {
|
|
for (Model model : selectedModels) {
|
|
follow(model, follow);
|
|
}
|
|
}
|
|
|
|
CompletableFuture<Boolean> 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 addSwitchStreamSource(ContextMenu menu, List<Model> selectedModels) {
|
|
var model = selectedModels.get(0);
|
|
if (!recorder.isTracked(model)) {
|
|
return;
|
|
}
|
|
var switchStreamSource = new MenuItem("Switch resolution");
|
|
switchStreamSource.setOnAction(e -> switchStreamSource(selectedModels.get(0)));
|
|
menu.getItems().add(switchStreamSource);
|
|
switchStreamSource.setDisable(selectedModels.size() != 1);
|
|
}
|
|
|
|
private void switchStreamSource(Model selectedModel) {
|
|
var couldntSwitchHeaderText = "Couldn't switch stream resolution";
|
|
|
|
try {
|
|
if (!selectedModel.isOnline(true)) {
|
|
Dialogs.showError(source.getScene(), couldntSwitchHeaderText, "The resolution can only be changed, when the model is online", null);
|
|
return;
|
|
}
|
|
} catch (InterruptedException e1) {
|
|
Thread.currentThread().interrupt();
|
|
Dialogs.showError(source.getScene(), couldntSwitchHeaderText, "An error occured while checking, if the model is online", null);
|
|
return;
|
|
} catch (IOException | ExecutionException e1) {
|
|
Dialogs.showError(source.getScene(), couldntSwitchHeaderText, "An error occured while checking, if the model is online", null);
|
|
return;
|
|
}
|
|
|
|
Consumer<Model> onSuccess = m -> {
|
|
try {
|
|
recorder.switchStreamSource(m);
|
|
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
|
|
LOG.error(couldntSwitchHeaderText, e);
|
|
showStreamSwitchErrorDialog(e);
|
|
}
|
|
};
|
|
Consumer<Throwable> onFail = t -> {
|
|
LOG.error(couldntSwitchHeaderText, t);
|
|
showStreamSwitchErrorDialog(t);
|
|
};
|
|
StreamSourceSelectionDialog.show(source.getScene(), selectedModel, onSuccess, onFail);
|
|
}
|
|
|
|
private void showStreamSwitchErrorDialog(Throwable throwable) {
|
|
showErrorDialog(throwable, "Couldn't switch stream resolution", "Error while switching stream resolution");
|
|
}
|
|
|
|
private void showErrorDialog(Throwable throwable, String header, String msg) {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, source.getScene());
|
|
alert.setTitle("Error");
|
|
alert.setHeaderText(header);
|
|
alert.setContentText(msg + ": " + throwable.getLocalizedMessage());
|
|
alert.showAndWait();
|
|
}
|
|
|
|
private void addGroupMenu(ContextMenu menu, List<Model> selectedModels) {
|
|
var model = selectedModels.get(0);
|
|
var addToGroup = new MenuItem("Add to group");
|
|
addToGroup.setOnAction(e -> new AddToGroupAction(source, recorder, selectedModels).execute(callback));
|
|
var groupSubMenu = new ModelGroupMenuBuilder() // @formatter:off
|
|
.model(model)
|
|
.recorder(recorder)
|
|
.node(source)
|
|
.callback(m -> callback.run())
|
|
.build(); // @formatter:on
|
|
Optional<ModelGroup> modelGroup = recorder.getModelGroup(model);
|
|
menu.getItems().add(modelGroup.isEmpty() ? addToGroup : groupSubMenu);
|
|
}
|
|
|
|
private void addPauseResume(ContextMenu menu, List<Model> selectedModels) {
|
|
var first = selectedModels.get(0);
|
|
if (recorder.isTracked(first)) {
|
|
var pause = new MenuItem("Pause Recording");
|
|
pause.setOnAction(e -> new PauseAction(source, selectedModels, recorder).execute(m -> executeCallback()));
|
|
var resume = new MenuItem("Resume Recording");
|
|
resume.setOnAction(e -> new ResumeAction(source, selectedModels, recorder).execute(m -> executeCallback()));
|
|
var pauseResume = recorder.isSuspended(first) ? resume : pause;
|
|
menu.getItems().add(pauseResume);
|
|
}
|
|
}
|
|
|
|
private void addRecordLater(ContextMenu menu, List<Model> 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<Model> selectedModels, boolean recordLater) {
|
|
selectedModels.forEach(m -> m.setMarkedForLaterRecording(recordLater));
|
|
new MarkForLaterRecordingAction(source, selectedModels, recordLater, recorder).execute(m -> executeCallback());
|
|
}
|
|
|
|
private void addStartPaused(ContextMenu menu, List<Model> 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<Model> selectedModels) {
|
|
var model = selectedModels.get(0);
|
|
var text = recorder.isTracked(model) ? "Record Until" : "Start Recording Until";
|
|
var start = new MenuItem(text);
|
|
menu.getItems().add(start);
|
|
start.setOnAction(e -> {
|
|
selectedModels.forEach(m -> {
|
|
m.setMarkedForLaterRecording(false);
|
|
m.setSuspended(false);
|
|
});
|
|
startStopAction(selectedModels, true);
|
|
selectedModels.forEach(m -> new SetStopDateAction(source, m, recorder).execute() //
|
|
.thenAccept(b -> executeCallback()));
|
|
});
|
|
}
|
|
|
|
private void addRemoveTimeLimit(ContextMenu menu, List<Model> selectedModels) {
|
|
var model = selectedModels.get(0);
|
|
if (!model.isRecordingTimeLimited()) {
|
|
return;
|
|
}
|
|
var removeTimeLimit = new MenuItem("Remove Time Limit");
|
|
removeTimeLimit.setOnAction(e -> removeTimeLimit(model));
|
|
menu.getItems().add(removeTimeLimit);
|
|
}
|
|
|
|
private void removeTimeLimit(Model selectedModel) {
|
|
new RemoveTimeLimitAction(source, selectedModel, recorder) //
|
|
.execute() //
|
|
.whenComplete((result, exception) -> executeCallback());
|
|
}
|
|
|
|
private void addOpenInPlayer(ContextMenu menu, List<Model> 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<Model> 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<Model> selection, boolean start) {
|
|
if (start) {
|
|
boolean selectSource = Config.getInstance().getSettings().chooseStreamQuality;
|
|
if (selectSource) {
|
|
for (Model model : selection) {
|
|
Consumer<Model> onSuccess = modl -> startRecording(List.of(modl));
|
|
Consumer<Throwable> 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<Model> models) {
|
|
new StartRecordingAction(source, models, recorder).execute(startStopCallback);
|
|
}
|
|
|
|
private void stopRecording(List<Model> models) {
|
|
new StopRecordingAction(source, models, recorder).execute(startStopCallback);
|
|
}
|
|
|
|
private void executeCallback() {
|
|
try {
|
|
callback.run();
|
|
} catch (Exception e) {
|
|
LOG.error("Error while executing menu callback", e);
|
|
}
|
|
}
|
|
}
|