diff --git a/client/src/main/java/ctbrec/ui/action/AddToGroupAction.java b/client/src/main/java/ctbrec/ui/action/AddToGroupAction.java index 2e677a27..29ce46d4 100644 --- a/client/src/main/java/ctbrec/ui/action/AddToGroupAction.java +++ b/client/src/main/java/ctbrec/ui/action/AddToGroupAction.java @@ -40,6 +40,10 @@ public class AddToGroupAction { } public void execute() { + execute(() -> {}); + } + + public void execute(Runnable callback) { source.setCursor(Cursor.WAIT); try { var dialog = new AddModelGroupDialog(); @@ -66,6 +70,7 @@ public class AddToGroupAction { } catch (IOException | InvalidKeyException | NoSuchAlgorithmException e) { Dialogs.showError(source.getScene(), "Add model to group", "Saving model group failed", e); } finally { + callback.run(); source.setCursor(Cursor.DEFAULT); } } diff --git a/client/src/main/java/ctbrec/ui/action/EditGroupAction.java b/client/src/main/java/ctbrec/ui/action/EditGroupAction.java index a8a2e88a..59682185 100644 --- a/client/src/main/java/ctbrec/ui/action/EditGroupAction.java +++ b/client/src/main/java/ctbrec/ui/action/EditGroupAction.java @@ -3,6 +3,7 @@ package ctbrec.ui.action; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import ctbrec.Model; import ctbrec.ModelGroup; @@ -36,6 +37,10 @@ public class EditGroupAction { } public void execute() { + execute(m -> {}); + } + + public void execute(Consumer callback) { source.setCursor(Cursor.WAIT); try { var dialog = new EditModelGroupDialog(model); @@ -56,6 +61,7 @@ public class EditGroupAction { } catch (Exception e) { Dialogs.showError(source.getScene(), DIALOG_TITLE, "Editing model group failed", e); } finally { + Optional.ofNullable(callback).ifPresent(c -> c.accept(model)); source.setCursor(Cursor.DEFAULT); } } @@ -102,6 +108,7 @@ public class EditGroupAction { urlList = FXCollections.observableList(modelGroup.getModelUrls()); urlList.addListener((ListChangeListener) change -> urls = new ArrayList<>(urlList)); urlListView = new ListView<>(urlList); + urlListView.setPrefWidth(600); GridPane.setHgrow(urlListView, Priority.ALWAYS); var row = 0; diff --git a/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java b/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java index d3304153..65f42c20 100644 --- a/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java +++ b/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java @@ -1,21 +1,33 @@ package ctbrec.ui.action; +import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import ctbrec.GlobalThreadPool; import ctbrec.Model; +import ctbrec.ui.controls.Dialogs; import javafx.application.Platform; import javafx.scene.Cursor; import javafx.scene.Node; public class ModelMassEditAction { + private static final Logger LOG = LoggerFactory.getLogger(ModelMassEditAction.class); + protected List models; protected Consumer action; protected Node source; + protected ModelMassEditAction() { + } + protected ModelMassEditAction(Node source, List models) { this.source = source; this.models = models; @@ -32,14 +44,36 @@ public class ModelMassEditAction { } public void execute(Consumer callback) { - Consumer cb = Objects.requireNonNull(callback, "Callback is null, call execute() instead"); - source.setCursor(Cursor.WAIT); - for (Model model : models) { - GlobalThreadPool.submit(() -> { - action.accept(model); - cb.accept(model); - Platform.runLater(() -> source.setCursor(Cursor.DEFAULT)); - }); - } + GlobalThreadPool.submit(() -> { + Platform.runLater(() -> source.setCursor(Cursor.WAIT)); + Consumer cb = Objects.requireNonNull(callback, "Callback is null, call execute() instead"); + List> futures = new LinkedList<>(); + for (Model model : getModels()) { + futures.add(GlobalThreadPool.submit(() -> { + action.accept(model); + cb.accept(model); + })); + } + Exception ex = null; + for (Future future : futures) { + try { + future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + ex = e; + } + } + if (ex != null) { + LOG.error("Error while executing model mass edit", ex); + Dialogs.showError(source.getScene(), "Error", "Error while execution action", ex); + } + Platform.runLater(() -> source.setCursor(Cursor.DEFAULT)); + }); + } + + @SuppressWarnings("unchecked") + protected List getModels() { + return (List) models; } } diff --git a/client/src/main/java/ctbrec/ui/action/PauseGroupAction.java b/client/src/main/java/ctbrec/ui/action/PauseGroupAction.java new file mode 100644 index 00000000..76839341 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/PauseGroupAction.java @@ -0,0 +1,49 @@ +package ctbrec.ui.action; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import ctbrec.Model; +import ctbrec.ModelGroup; +import ctbrec.recorder.Recorder; +import ctbrec.ui.controls.Dialogs; +import javafx.application.Platform; +import javafx.scene.Node; + +public class PauseGroupAction extends ModelMassEditAction { + + private Recorder recorder; + private Model model; + + public PauseGroupAction(Node source, Recorder recorder, Model model) { + super.source = source; + this.recorder = recorder; + this.model = model; + + action = m -> { + try { + recorder.suspendRecording(m); + } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) { + Platform.runLater(() -> Dialogs.showError(source.getScene(), "Couldn't pause model", "Pausing recording of " + m.getName() + " failed", e)); + } + }; + } + + @Override + protected List getModels() { + Optional optionalGroup = recorder.getModelGroup(model); + if (optionalGroup.isPresent()) { + ModelGroup group = optionalGroup.get(); + return recorder.getModels().stream() // + .filter(m -> group.getModelUrls().contains(m.getUrl())) // + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/action/ResumeGroupAction.java b/client/src/main/java/ctbrec/ui/action/ResumeGroupAction.java new file mode 100644 index 00000000..3e60e88e --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/ResumeGroupAction.java @@ -0,0 +1,49 @@ +package ctbrec.ui.action; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import ctbrec.Model; +import ctbrec.ModelGroup; +import ctbrec.recorder.Recorder; +import ctbrec.ui.controls.Dialogs; +import javafx.application.Platform; +import javafx.scene.Node; + +public class ResumeGroupAction extends ModelMassEditAction { + + private Recorder recorder; + private Model model; + + public ResumeGroupAction(Node source, Recorder recorder, Model model) { + super.source = source; + this.recorder = recorder; + this.model = model; + + action = m -> { + try { + recorder.resumeRecording(m); + } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) { + Platform.runLater(() -> Dialogs.showError(source.getScene(), "Couldn't resume model", "Resuming recording of " + m.getName() + " failed", e)); + } + }; + } + + @Override + protected List getModels() { + Optional optionalGroup = recorder.getModelGroup(model); + if (optionalGroup.isPresent()) { + ModelGroup group = optionalGroup.get(); + return recorder.getModels().stream() // + .filter(m -> group.getModelUrls().contains(m.getUrl())) // + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/action/StopGroupAction.java b/client/src/main/java/ctbrec/ui/action/StopGroupAction.java new file mode 100644 index 00000000..cc50a969 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/StopGroupAction.java @@ -0,0 +1,49 @@ +package ctbrec.ui.action; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import ctbrec.Model; +import ctbrec.ModelGroup; +import ctbrec.recorder.Recorder; +import ctbrec.ui.controls.Dialogs; +import javafx.application.Platform; +import javafx.scene.Node; + +public class StopGroupAction extends ModelMassEditAction { + + private Recorder recorder; + private Model model; + + public StopGroupAction(Node source, Recorder recorder, Model model) { + super.source = source; + this.recorder = recorder; + this.model = model; + + action = m -> { + try { + recorder.stopRecording(m); + } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) { + Platform.runLater(() -> Dialogs.showError(source.getScene(), "Couldn't stop model", "Stopping recording of " + m.getName() + " failed", e)); + } + }; + } + + @Override + protected List getModels() { + Optional optionalGroup = recorder.getModelGroup(model); + if (optionalGroup.isPresent()) { + ModelGroup group = optionalGroup.get(); + return recorder.getModels().stream() // + .filter(m -> group.getModelUrls().contains(m.getUrl())) // + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/action/TipAction.java b/client/src/main/java/ctbrec/ui/action/TipAction.java new file mode 100644 index 00000000..1270b850 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/TipAction.java @@ -0,0 +1,60 @@ +package ctbrec.ui.action; + +import static ctbrec.ui.controls.Dialogs.*; + +import java.io.IOException; +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.event.EventBusHolder; +import ctbrec.ui.SiteUiFactory; +import ctbrec.ui.TipDialog; +import javafx.scene.Cursor; +import javafx.scene.Node; + +public class TipAction { + + private static final Logger LOG = LoggerFactory.getLogger(TipAction.class); + + private Model model; + private Node source; + + public TipAction(Model model, Node source) { + this.model = model; + this.source = source; + } + + public void execute() { + source.setCursor(Cursor.WAIT); + try { + var site = model.getSite(); + var tipDialog = new TipDialog(source.getScene(), site); + tipDialog.showAndWait(); + String tipText = tipDialog.getResult(); + if (tipText != null) { + var df = new DecimalFormat("0.##"); + try { + Number tokens = df.parse(tipText); + SiteUiFactory.getUi(site).login(); + model.receiveTip(tokens.doubleValue()); + Map event = new HashMap<>(); + event.put("event", "tokens.sent"); + event.put("amount", tokens.doubleValue()); + EventBusHolder.BUS.post(event); + } catch (IOException ex) { + LOG.error("An error occurred while sending tip", ex); + showError(source.getScene(), "Couldn't send tip", "An error occurred while sending tip:", ex); + } catch (Exception ex) { + showError(source.getScene(), "Couldn't send tip", "You entered an invalid amount of tokens", ex); + } + } + } finally { + source.setCursor(Cursor.DEFAULT); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/menu/ModelGroupMenuBuilder.java b/client/src/main/java/ctbrec/ui/menu/ModelGroupMenuBuilder.java new file mode 100644 index 00000000..4b8bb240 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/menu/ModelGroupMenuBuilder.java @@ -0,0 +1,65 @@ +package ctbrec.ui.menu; + +import java.util.Objects; +import java.util.function.Consumer; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.action.EditGroupAction; +import ctbrec.ui.action.PauseGroupAction; +import ctbrec.ui.action.ResumeGroupAction; +import ctbrec.ui.action.StopGroupAction; +import javafx.scene.Node; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; + +public class ModelGroupMenuBuilder { + + private Model model; + private Recorder recorder; + private Node source; + private Consumer callback; + + public ModelGroupMenuBuilder model(Model model) { + this.model = Objects.requireNonNull(model, "Model cannot be null"); + return this; + } + + public ModelGroupMenuBuilder recorder(Recorder recorder) { + this.recorder = Objects.requireNonNull(recorder, "Recorder cannot be null"); + return this; + } + + public ModelGroupMenuBuilder node(Node source) { + this.source = Objects.requireNonNull(source, "Node cannot be null"); + return this; + } + + public ModelGroupMenuBuilder callback(Consumer callback) { + this.callback = callback; + return this; + } + + public Menu build() { + Objects.requireNonNull(model, "Model has to be set"); + Objects.requireNonNull(recorder, "Recorder has to be set"); + Objects.requireNonNull(source, "Node has to be set"); + + var menu = new Menu("Group"); + + var editGroup = new MenuItem("Edit group"); + editGroup.setOnAction(e -> new EditGroupAction(source, recorder, model).execute(callback)); + + var resumeAllOfGroup = new MenuItem("Resume all"); + resumeAllOfGroup.setOnAction(e -> new ResumeGroupAction(source, recorder, model).execute(callback)); + + var pauseAllOfGroup = new MenuItem("Pause all"); + pauseAllOfGroup.setOnAction(e -> new PauseGroupAction(source, recorder, model).execute(callback)); + + var stopAllOfGroup = new MenuItem("Remove all"); + stopAllOfGroup.setOnAction(e -> new StopGroupAction(source, recorder, model).execute(callback)); + + menu.getItems().addAll(editGroup, resumeAllOfGroup, pauseAllOfGroup, stopAllOfGroup); + return menu; + } +} diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java index 18e58339..05682868 100644 --- a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java @@ -614,7 +614,7 @@ public class ThumbCell extends StackPane { this.index = index; } - private void update() { + protected void update() { model.setSuspended(recorder.isSuspended(model)); model.setMarkedForLaterRecording(recorder.isMarkedForLaterRecording(model)); setRecording(recorder.isTracked(model)); diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java index ae6f96ad..032ca925 100644 --- a/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java @@ -1,10 +1,7 @@ package ctbrec.ui.tabs; -import static ctbrec.ui.controls.Dialogs.*; - import java.io.IOException; import java.net.SocketTimeoutException; -import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -30,7 +27,6 @@ import ctbrec.Config; import ctbrec.GlobalThreadPool; import ctbrec.Model; import ctbrec.ModelGroup; -import ctbrec.event.EventBusHolder; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.sites.mfc.MyFreeCamsClient; @@ -38,18 +34,18 @@ import ctbrec.sites.mfc.MyFreeCamsModel; import ctbrec.ui.AutosizeAlert; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SiteUiFactory; -import ctbrec.ui.TipDialog; import ctbrec.ui.TokenLabel; import ctbrec.ui.action.AddToGroupAction; -import ctbrec.ui.action.EditGroupAction; 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 javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.ParallelTransition; @@ -80,7 +76,6 @@ import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; @@ -96,7 +91,6 @@ import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; -import javafx.scene.transform.Transform; import javafx.util.Duration; public class ThumbOverviewTab extends Tab implements TabSelectionListener { @@ -152,7 +146,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { SearchBox filterInput = new SearchBox(false); filterInput.setPromptText("Filter models on this page"); - filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> { + filterInput.textProperty().addListener((observableValue, oldValue, newValue) -> { filter = filterInput.getText(); gridLock.lock(); try { @@ -173,7 +167,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { searchInput.prefWidth(200); searchInput.textProperty().addListener(search()); searchInput.addEventHandler(KeyEvent.KEY_PRESSED, evt -> { - if(evt.getCode() == KeyCode.ESCAPE) { + if (evt.getCode() == KeyCode.ESCAPE) { popover.hide(); } }); @@ -201,7 +195,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { topBar.getChildren().addAll(tokenBalance, buyTokens); tokenBalance.loadBalance(); } - if(site.supportsSearch()) { + if (site.supportsSearch()) { topBar.getChildren().add(searchInput); } BorderPane.setMargin(topBar, new Insets(0, 5, 0, 5)); @@ -232,7 +226,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { HBox thumbSizeSelector = new HBox(5); Label l = new Label("Thumb Size"); - l.setPadding(new Insets(5,0,0,0)); + l.setPadding(new Insets(5, 0, 0, 0)); thumbSizeSelector.getChildren().add(l); List thumbWidths = new ArrayList<>(); thumbWidths.add(180); @@ -249,7 +243,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { thumbSizeSelector.getChildren().add(thumbWidth); BorderPane.setMargin(thumbSizeSelector, new Insets(5)); - BorderPane bottomPane = new BorderPane(); bottomPane.setLeft(pagination); bottomPane.setRight(thumbSizeSelector); @@ -281,7 +274,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { changePageTo(page); } - private void previousPage() { int page = updateService.getPage(); page = Math.max(1, --page); @@ -296,10 +288,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { private ChangeListener search() { return (observableValue, oldValue, newValue) -> { - if(searchTask != null) { + if (searchTask != null) { searchTask.cancel(true); } - if(newValue.length() < 2) { + if (newValue.length() < 2) { return; } searchTask = new ThumbOverviewTabSearchTask(site, popover, popoverTreeList, newValue); @@ -311,7 +303,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { int width = Config.getInstance().getSettings().thumbWidth; thumbWidth.getSelectionModel().select(Integer.valueOf(width)); for (Node node : grid.getChildren()) { - if(node instanceof ThumbCell) { + if (node instanceof ThumbCell) { ThumbCell cell = (ThumbCell) node; cell.setThumbWidth(width); } @@ -326,7 +318,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { int page = Integer.parseInt(pageInput.getText()); page = Math.max(1, page); changePageTo(page); - } catch(NumberFormatException e) { + } catch (NumberFormatException e) { // noop } finally { pageInput.setText(Integer.toString(updateService.getPage())); @@ -353,7 +345,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } protected void onSuccess() { - if(updatesSuspended) { + if (updatesSuspended) { return; } List models = filterIgnoredModels(updateService.getValue()); @@ -362,9 +354,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { private List filterIgnoredModels(List models) { List ignored = Config.getInstance().getSettings().ignoredModels; - return models.stream() - .filter(m -> !ignored.contains(m.getUrl())) - .collect(Collectors.toList()); + return models.stream().filter(m -> !ignored.contains(m.getUrl())).collect(Collectors.toList()); } protected void updateGrid(List models) { @@ -397,19 +387,20 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { for (Model model : models) { boolean found = false; for (Node node : nodes) { // NOSONAR - if (!(node instanceof ThumbCell)) continue; + if (!(node instanceof ThumbCell)) + continue; ThumbCell cell = (ThumbCell) node; - if(cell.getModel().equals(model)) { + if (cell.getModel().equals(model)) { found = true; cell.setModel(model); - if(index != cell.getIndex()) { + if (index != cell.getIndex()) { cell.setIndex(index); positionChangedOrNew.add(cell); } break; } } - if(!found) { + if (!found) { ThumbCell newCell = createThumbCell(model, recorder); newCell.setIndex(index); positionChangedOrNew.add(newCell); @@ -422,7 +413,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { private void rearrangeCells(ObservableList nodes, List positionChangedOrNew) { for (ThumbCell thumbCell : positionChangedOrNew) { nodes.remove(thumbCell); - if(thumbCell.getIndex() < nodes.size()) { + if (thumbCell.getIndex() < nodes.size()) { nodes.add(thumbCell.getIndex(), thumbCell); } else { nodes.add(thumbCell); @@ -433,7 +424,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { private void removeModelsMissingInUpdate(ObservableList nodes, List models) { for (Iterator iterator = nodes.iterator(); iterator.hasNext();) { Node node = iterator.next(); - if (!(node instanceof ThumbCell)) continue; + if (!(node instanceof ThumbCell)) + continue; ThumbCell cell = (ThumbCell) node; if (!models.contains(cell.getModel())) { iterator.remove(); @@ -453,7 +445,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { event.consume(); }); newCell.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { - if(popup != null) { + if (popup != null) { popup.hide(); popup = null; } @@ -469,14 +461,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { return newCell; } - // private ContextMenu createContextMenu(ThumbCell cell) { - // return new ModelContextMenu.Builder() - // .model(cell.getModel()) - // .node(grid) - // .recorder(recorder) - // .build(); - // } - private ContextMenu createContextMenu(ThumbCell cell) { var model = cell.getModel(); boolean modelIsTrackedByRecorder = recorder.isTracked(model); @@ -507,16 +491,15 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { 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 addToGroup = new MenuItem("Add to group"); - addToGroup.setOnAction(e -> addToGroup(model)); - var editGroup = new MenuItem("Edit group"); - editGroup.setOnAction(e -> editGroup(model)); - var ignore = new MenuItem("Ignore"); ignore.setOnAction(e -> ignore(getSelectedThumbCells(cell))); @@ -542,6 +525,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } 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; @@ -551,8 +536,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { if (site.supportsTips()) { contextMenu.getItems().add(sendTip); } - Optional modelGroup = recorder.getModelGroup(model); - contextMenu.getItems().add(modelGroup.isEmpty() ? addToGroup : editGroup); contextMenu.getItems().addAll(copyUrl, openInBrowser, ignore, refresh, openRecDir); if (model instanceof MyFreeCamsModel && Objects.equals(System.getenv("CTBREC_DEV"), "1")) { var debug = new MenuItem("debug"); @@ -563,14 +546,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { return contextMenu; } - private void editGroup(Model model) { - new EditGroupAction(this.getContent(), recorder, model).execute(); - } - - private void addToGroup(Model model) { - new AddToGroupAction(this.getContent(), recorder, model).execute(); - } - private void recordLater(List list, boolean recordLater) { for (ThumbCell cell : list) { cell.recordLater(recordLater); @@ -593,14 +568,14 @@ 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 + /* + * 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) { if (selectedThumbCells.size() > 1 || selectedThumbCells.size() == 1 && selectedThumbCells.get(0) != cell) { - if(cell.isSelected()) { - if(Config.getInstance().getSettings().singlePlayer) { + if (cell.isSelected()) { + if (Config.getInstance().getSettings().singlePlayer) { openInPlayer.setDisable(true); } copyUrl.setDisable(true); @@ -612,52 +587,30 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } private MenuItem createOpenInBrowser(ThumbCell cell) { - MenuItem openInBrowser = new MenuItem("Open in browser"); + var openInBrowser = new MenuItem("Open in browser"); openInBrowser.setOnAction(e -> DesktopIntegration.open(cell.getModel().getUrl())); return openInBrowser; } private MenuItem createCopyUrlMenuItem(ThumbCell cell) { - MenuItem copyUrl = new MenuItem("Copy URL"); + var copyUrl = new MenuItem("Copy URL"); copyUrl.setOnAction(e -> { - final Clipboard clipboard = Clipboard.getSystemClipboard(); - final ClipboardContent content = new ClipboardContent(); + final var content = new ClipboardContent(); content.putString(cell.getModel().getUrl()); - clipboard.setContent(content); + Clipboard.getSystemClipboard().setContent(content); }); return copyUrl; } private MenuItem createTipMenuItem(ThumbCell cell) { - MenuItem sendTip = new MenuItem("Send Tip"); - sendTip.setOnAction(e -> { - TipDialog tipDialog = new TipDialog(getTabPane().getScene(), site); - tipDialog.showAndWait(); - String tipText = tipDialog.getResult(); - if(tipText != null) { - DecimalFormat df = new DecimalFormat("0.##"); - try { - Number tokens = df.parse(tipText); - SiteUiFactory.getUi(site).login(); - cell.getModel().receiveTip(tokens.doubleValue()); - Map event = new HashMap<>(); - event.put("event", "tokens.sent"); - event.put("amount", tokens.doubleValue()); - EventBusHolder.BUS.post(event); - } catch (IOException ex) { - LOG.error("An error occurred while sending tip", ex); - showError(getTabPane().getScene(), "Couldn't send tip", "An error occurred while sending tip:", ex); - } catch (Exception ex) { - showError(getTabPane().getScene(), "Couldn't send tip", "You entered an invalid amount of tokens", ex); - } - } - }); + var sendTip = new MenuItem("Send Tip"); + sendTip.setOnAction(e -> new TipAction(cell.getModel(), cell)); sendTip.setDisable(!site.credentialsAvailable()); return sendTip; } private List getSelectedThumbCells(ThumbCell cell) { - if(selectedThumbCells.isEmpty()) { + if (selectedThumbCells.isEmpty()) { return Collections.singletonList(cell); } else { return selectedThumbCells; @@ -672,22 +625,19 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } }); } - if(!follow) { + if (!follow) { selectedThumbCells.clear(); } } protected void ignore(List selection) { Map thumbcells = new HashMap<>(); - List selectedModels = selection.stream() - .map(tc -> { - thumbcells.put(tc.getModel(), tc); - return tc; - }) - .map(ThumbCell::getModel) - .collect(Collectors.toList()); + List selectedModels = selection.stream().map(tc -> { + thumbcells.put(tc.getModel(), tc); + return tc; + }).map(ThumbCell::getModel).collect(Collectors.toList()); new IgnoreModelsAction(grid, selectedModels, recorder, false).execute(m -> { - ThumbCell thumbCell = thumbcells.get(m); + var thumbCell = thumbcells.get(m); grid.getChildren().remove(thumbCell); selectedThumbCells.remove(thumbCell); }); @@ -695,45 +645,45 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { private void showAddToFollowedAnimation(ThumbCell thumbCell) { Platform.runLater(() -> { - Transform tx = thumbCell.getLocalToParentTransform(); - ImageView iv = new ImageView(); + var tx = thumbCell.getLocalToParentTransform(); + var iv = new ImageView(); iv.setFitWidth(thumbCell.getWidth()); root.getChildren().add(iv); StackPane.setAlignment(iv, Pos.TOP_LEFT); iv.setImage(thumbCell.getImage()); double scrollPaneTopLeft = scrollPane.getVvalue() * (grid.getHeight() - scrollPane.getViewportBounds().getHeight()); double offsetInViewPort = tx.getTy() - scrollPaneTopLeft; - int duration = 500; - TranslateTransition translate = new TranslateTransition(Duration.millis(duration), iv); + var duration = 500; + var translate = new TranslateTransition(Duration.millis(duration), iv); translate.setFromX(0); translate.setFromY(0); translate.setByX(-tx.getTx() - 200); - TabProvider tabProvider = SiteUiFactory.getUi(site).getTabProvider(); - Tab followedTab = tabProvider.getFollowedTab(); + var tabProvider = SiteUiFactory.getUi(site).getTabProvider(); + var followedTab = tabProvider.getFollowedTab(); translate.setByY(-offsetInViewPort + getFollowedTabYPosition(followedTab)); StackPane.setMargin(iv, new Insets(offsetInViewPort, 0, 0, tx.getTx())); translate.setInterpolator(Interpolator.EASE_BOTH); - FadeTransition fade = new FadeTransition(Duration.millis(duration), iv); + var fade = new FadeTransition(Duration.millis(duration), iv); fade.setFromValue(1); fade.setToValue(.3); - ScaleTransition scale = new ScaleTransition(Duration.millis(duration), iv); + var scale = new ScaleTransition(Duration.millis(duration), iv); scale.setToX(0.1); scale.setToY(0.1); - ParallelTransition pt = new ParallelTransition(translate, scale); + var pt = new ParallelTransition(translate, scale); pt.play(); pt.setOnFinished(evt -> root.getChildren().remove(iv)); - FollowTabBlinkTransition blink = new FollowTabBlinkTransition(followedTab); + var blink = new FollowTabBlinkTransition(followedTab); blink.play(); }); } private double getFollowedTabYPosition(Tab followedTab) { - TabPane tabPane = getTabPane(); + var tabPane = getTabPane(); int idx = Math.max(0, tabPane.getTabs().indexOf(followedTab)); for (Node node : tabPane.getChildrenUnmodifiable()) { Parent p = (Parent) node; for (Node child : p.getChildrenUnmodifiable()) { - if(child.getStyleClass().contains("headers-region")) { + if (child.getStyleClass().contains("headers-region")) { Parent tabContainer = (Parent) child; Node tab = tabContainer.getChildrenUnmodifiable().get(tabContainer.getChildrenUnmodifiable().size() - idx - 1); return tab.getLayoutX() - 85; @@ -784,14 +734,14 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { }; protected void onFail(WorkerStateEvent event) { - if(updatesSuspended) { + if (updatesSuspended) { return; } Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene()); alert.setTitle("Error"); alert.setHeaderText("Couldn't fetch model list"); - if(event.getSource().getException() != null) { - if(event.getSource().getException() instanceof SocketTimeoutException) { + if (event.getSource().getException() != null) { + if (event.getSource().getException() instanceof SocketTimeoutException) { LOG.debug("Fetching model list timed out"); return; } else { @@ -806,8 +756,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { void filter() { filteredThumbCells.sort((c1, c2) -> { - if (c1.getIndex() < c2.getIndex()) return -1; - if (c1.getIndex() > c2.getIndex()) return 1; + if (c1.getIndex() < c2.getIndex()) + return -1; + if (c1.getIndex() > c2.getIndex()) + return 1; return c1.getModel().getName().compareTo(c2.getModel().getName()); }); @@ -835,7 +787,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { for (Iterator iterator = filteredThumbCells.iterator(); iterator.hasNext();) { ThumbCell thumbCell = iterator.next(); Model m = thumbCell.getModel(); - if(matches(m, filter)) { + if (matches(m, filter)) { iterator.remove(); insert(thumbCell); } @@ -852,11 +804,11 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { private void moveActiveRecordingsToFront() { List thumbsToMove = new ArrayList<>(); ObservableList thumbs = grid.getChildren(); - for (int i = thumbs.size()-1; i >= 0; i--) { + for (int i = thumbs.size() - 1; i >= 0; i--) { Node node = thumbs.get(i); - if(node instanceof ThumbCell) { + if (node instanceof ThumbCell) { ThumbCell thumb = (ThumbCell) thumbs.get(i); - if(recorder.isTracked(thumb.getModel())) { + if (recorder.isTracked(thumb.getModel())) { thumbs.remove(i); thumbsToMove.add(0, thumb); } @@ -866,11 +818,11 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } private void insert(ThumbCell thumbCell) { - if(grid.getChildren().contains(thumbCell)) { + if (grid.getChildren().contains(thumbCell)) { return; } - if(thumbCell.getIndex() < grid.getChildren().size()-1) { + if (thumbCell.getIndex() < grid.getChildren().size() - 1) { grid.getChildren().add(thumbCell.getIndex(), thumbCell); } else { grid.getChildren().add(thumbCell); @@ -913,7 +865,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } } else { var negated = false; - if(token.startsWith("!")) { + if (token.startsWith("!")) { negated = true; token = token.substring(1); } @@ -990,6 +942,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } private static int threadCounter = 0; + private static ThreadFactory createThreadFactory() { return r -> { Thread t = new Thread(r); 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 cd7a00c1..da0eba87 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java @@ -38,7 +38,6 @@ import ctbrec.ui.PreviewPopupHandler; import ctbrec.ui.StreamSourceSelectionDialog; import ctbrec.ui.action.AddToGroupAction; import ctbrec.ui.action.CheckModelAccountAction; -import ctbrec.ui.action.EditGroupAction; import ctbrec.ui.action.EditNotesAction; import ctbrec.ui.action.FollowAction; import ctbrec.ui.action.IgnoreModelsAction; @@ -57,6 +56,7 @@ import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.autocomplete.AutoFillTextField; import ctbrec.ui.controls.autocomplete.ObservableListSuggester; +import ctbrec.ui.menu.ModelGroupMenuBuilder; import ctbrec.ui.tabs.TabSelectionListener; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; @@ -75,6 +75,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; +import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.SelectionMode; @@ -154,7 +155,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { scrollPane.setFitToWidth(true); BorderPane.setMargin(scrollPane, new Insets(5)); - table.setEditable(true); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); var previewPopupHandler = new PreviewPopupHandler(table); @@ -168,7 +168,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { preview.setCellValueFactory(cdf -> new SimpleStringProperty(" ▶ ")); preview.setEditable(false); preview.setId("preview"); - if(!Config.getInstance().getSettings().livePreviews) { + if (!Config.getInstance().getSettings().livePreviews) { preview.setVisible(false); } TableColumn name = new TableColumn<>("Model"); @@ -286,8 +286,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { HBox.setHgrow(model, Priority.ALWAYS); model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/"); model.onActionHandler(this::addModel); - model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" + - "press ENTER to confirm a suggested site name")); + model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" + "press ENTER to confirm a suggested site name")); BorderPane.setMargin(addModelBox, new Insets(5)); addModelButton.setOnAction(this::addModel); addModelButton.setPadding(new Insets(5)); @@ -303,8 +302,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { checkModelAccountExistance.setPadding(new Insets(5)); checkModelAccountExistance.setTooltip(new Tooltip("Go over all model URLs and check, if the account still exists")); HBox.setMargin(checkModelAccountExistance, new Insets(0, 0, 0, 20)); - checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder) - .execute(Predicate.not(Model::isMarkedForLaterRecording))); + checkModelAccountExistance + .setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder).execute(Predicate.not(Model::isMarkedForLaterRecording))); var filterContainer = new HBox(); filterContainer.setSpacing(0); @@ -317,7 +316,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { filter.minWidth(100); filter.prefWidth(150); filter.setPromptText("Filter"); - filter.textProperty().addListener( (observableValue, oldValue, newValue) -> { + filter.textProperty().addListener((observableValue, oldValue, newValue) -> { String q = filter.getText(); lock.lock(); try { @@ -529,11 +528,11 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private ChangeListener createPauseListener(JavaFxModel updatedModel) { return (obs, oldV, newV) -> { if (newV.booleanValue()) { - if(!recorder.isSuspended(updatedModel)) { + if (!recorder.isSuspended(updatedModel)) { pauseRecording(Collections.singletonList(updatedModel)); } } else { - if(recorder.isSuspended(updatedModel)) { + if (recorder.isSuspended(updatedModel)) { resumeRecording(Collections.singletonList(updatedModel)); } } @@ -591,26 +590,21 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { LOG.trace("Updating recorded models"); List recordings = recorder.getRecordings(); List onlineModels = recorder.getOnlineModels(); - return recorder.getModels() - .stream() - .filter(Predicate.not(Model::isMarkedForLaterRecording)) - .map(JavaFxModel::new) - .peek(fxm -> { // NOSONAR - for (Recording recording : recordings) { - if(recording.getStatus() == RECORDING && Objects.equals(recording.getModel(), fxm)){ - fxm.setRecordingProperty(true); - break; - } - } + return recorder.getModels().stream().filter(Predicate.not(Model::isMarkedForLaterRecording)).map(JavaFxModel::new).peek(fxm -> { // NOSONAR + for (Recording recording : recordings) { + if (recording.getStatus() == RECORDING && Objects.equals(recording.getModel(), fxm)) { + fxm.setRecordingProperty(true); + break; + } + } - for (Model onlineModel : onlineModels) { - if(Objects.equals(onlineModel, fxm)) { - fxm.setOnlineProperty(true); - break; - } - } - }) - .collect(Collectors.toList()); + for (Model onlineModel : onlineModels) { + if (Objects.equals(onlineModel, fxm)) { + fxm.setOnlineProperty(true); + break; + } + } + }).collect(Collectors.toList()); } }; } @@ -684,9 +678,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { openRecDir.setOnAction(e -> new OpenRecordingsDir(table, selectedModels.get(0)).execute()); var addToGroup = new MenuItem("Add to group"); - addToGroup.setOnAction(e -> addToGroup(selectedModels.get(0))); - var editGroup = new MenuItem("Edit group"); - editGroup.setOnAction(e -> editGroup(selectedModels.get(0))); + addToGroup.setOnAction(e -> new AddToGroupAction(this.getContent(), recorder, selectedModels.get(0)).execute(() -> table.refresh())); + var groupSubMenu = createModelGroupMenu(selectedModels); ContextMenu menu = new CustomMouseBehaviorContextMenu(stop, recordLater); if (selectedModels.size() == 1) { @@ -699,7 +692,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { menu.getItems().addAll(resumeRecording, pauseRecording); } Optional modelGroup = recorder.getModelGroup(selectedModels.get(0)); - menu.getItems().add(modelGroup.isEmpty() ? addToGroup : editGroup); + menu.getItems().add(modelGroup.isEmpty() ? addToGroup : groupSubMenu); menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, openRecDir, switchStreamSource, follow, notes, ignore); if (selectedModels.size() > 1) { @@ -713,14 +706,13 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { return menu; } - private void addToGroup(Model model) { - new AddToGroupAction(this.getContent(), recorder, model).execute(); - table.refresh(); - } - - private void editGroup(Model model) { - new EditGroupAction(this.getContent(), recorder, model).execute(); - table.refresh(); + private Menu createModelGroupMenu(ObservableList selectedModels) { + return new ModelGroupMenuBuilder() // + .model(selectedModels.get(0)) // + .recorder(recorder) // + .node(table) // + .callback(m -> table.refresh()) // + .build(); } private void setStopDate(JavaFxModel model) { @@ -818,13 +810,10 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private void recordLater(List selectedModels) { boolean confirmed = stopAction(selectedModels); if (confirmed) { - List models = selectedModels.stream() - .map(JavaFxModel::getDelegate) - .map(m -> { - m.setMarkedForLaterRecording(true); - return m; - }) - .collect(Collectors.toList()); + List models = selectedModels.stream().map(JavaFxModel::getDelegate).map(m -> { + m.setMarkedForLaterRecording(true); + return m; + }).collect(Collectors.toList()); new StartRecordingAction(table, models, recorder).execute(); } } @@ -878,10 +867,10 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private void restoreColumnOrder() { String[] columnIds = Config.getInstance().getSettings().recordedModelsColumnIds; - ObservableList> columns = table.getColumns(); + ObservableList> columns = table.getColumns(); for (var i = 0; i < columnIds.length; i++) { for (var j = 0; j < table.getColumns().size(); j++) { - if(Objects.equals(columnIds[i], columns.get(j).getId())) { + if (Objects.equals(columnIds[i], columns.get(j).getId())) { TableColumn col = columns.get(j); columns.remove(j); // NOSONAR columns.add(i, col); @@ -912,7 +901,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem(); - if(selectedModel != null) { + if (selectedModel != null) { new PlayAction(table, selectedModel).execute(); } } @@ -929,11 +918,11 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { TableCell tableCell = callback.call(param); tableCell.setOnScroll(event -> { - if(event.isControlDown()) { + if (event.isControlDown()) { event.consume(); JavaFxModel m = tableCell.getTableRow().getItem(); int prio = m.getPriority(); - if(event.getDeltaY() < 0) { + if (event.getDeltaY() < 0) { prio--; } else { prio++;