From 016b5dc7f1a04bf03a88c8b997850f8afec57a7d Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Mon, 16 Aug 2021 19:25:43 +0200 Subject: [PATCH] Introduce common base class for recorded models tabs --- CHANGELOG.md | 1 + .../ctbrec/ui/action/SetPortraitAction.java | 4 +- .../java/ctbrec/ui/tabs/RecordingsTab.java | 2 +- .../recorded/AbstractRecordedModelsTab.java | 601 +++++++++++++++++ .../ctbrec/ui/tabs/recorded/ModelName.java | 29 + .../ui/tabs/recorded/ModelNameTableCell.java | 1 - .../ui/tabs/recorded/RecordLaterTab.java | 467 ++------------ .../ui/tabs/recorded/RecordedModelsTab.java | 610 ++---------------- common/src/main/java/ctbrec/Settings.java | 3 +- 9 files changed, 748 insertions(+), 970 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java create mode 100644 client/src/main/java/ctbrec/ui/tabs/recorded/ModelName.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d73f221..b29f2a55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ======================== * Added portrait column to Recording tab. The image to show can be selected in the context menu. This feature is a client-side only feature. +* Added button to configure, which columns should be shown on the Recording tab * Added data transfer detection to HLS downloads, so that downloads don't get stuck in recording state. Recordings will stop now, if now segment was downloaded for 30 seconds. diff --git a/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java b/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java index e2c0cdcc..de3841e0 100644 --- a/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java +++ b/client/src/main/java/ctbrec/ui/action/SetPortraitAction.java @@ -5,6 +5,7 @@ import java.awt.Image; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Arrays; import java.util.UUID; @@ -42,7 +43,8 @@ public class SetPortraitAction { public void execute() { source.setCursor(Cursor.WAIT); - String portraitId = Config.getInstance().getSettings().modelPortraits.getOrDefault(model.getUrl(), UUID.randomUUID().toString()); + String portraitId = Config.getInstance().getSettings().modelPortraits.getOrDefault(model.getUrl(), + UUID.nameUUIDFromBytes(model.getUrl().getBytes(StandardCharsets.UTF_8)).toString()); GridPane pane = new GridPane(); Label l = new Label("Select a portrait image. Leave empty to remove a portrait again."); diff --git a/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java b/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java index c9eecf00..367c3f2b 100644 --- a/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java @@ -56,7 +56,7 @@ import ctbrec.ui.controls.DateTimeCellFactory; import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Toast; import ctbrec.ui.menu.ModelMenuContributor; -import ctbrec.ui.tabs.recorded.RecordedModelsTab.ModelName; +import ctbrec.ui.tabs.recorded.ModelName; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java new file mode 100644 index 00000000..befa3ec8 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java @@ -0,0 +1,601 @@ +package ctbrec.ui.tabs.recorded; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.StringUtil; +import ctbrec.recorder.Recorder; +import ctbrec.sites.Site; +import ctbrec.ui.AutosizeAlert; +import ctbrec.ui.JavaFxModel; +import ctbrec.ui.PreviewPopupHandler; +import ctbrec.ui.action.PlayAction; +import ctbrec.ui.action.SetPortraitAction; +import ctbrec.ui.controls.CustomMouseBehaviorContextMenu; +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.ModelMenuContributor; +import ctbrec.ui.tabs.TabSelectionListener; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringPropertyBase; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.Tab; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableColumn.SortType; +import javafx.scene.control.TableRow; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.image.Image; +import javafx.scene.input.ContextMenuEvent; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.util.Callback; + +public abstract class AbstractRecordedModelsTab extends Tab implements TabSelectionListener { + private static final Logger LOG = LoggerFactory.getLogger(AbstractRecordedModelsTab.class); + + protected ReentrantLock lock = new ReentrantLock(); + protected ObservableList observableModels = FXCollections.observableArrayList(); + protected ObservableList filteredModels = FXCollections.observableArrayList(); + + protected TableView table = new TableView<>(); + protected List> columns = new ArrayList<>(); + protected LoadingCache portraitCache = CacheBuilder.newBuilder() + .expireAfterAccess(1, TimeUnit.DAYS) + .maximumSize(1000) + .build(CacheLoader.from(AbstractRecordedModelsTab::loadModelPortrait)); + + protected AutoFillTextField modelInputField; + protected List sites; + protected Recorder recorder; + + protected HBox addModelBox = new HBox(5); + protected HBox filterContainer = new HBox(5); + protected Label modelLabel = new Label("Model"); + protected Button addModelButton = new Button("Record"); + protected Button checkModelAccountExistance = new Button("Check URLs"); + protected TextField filter; + + protected FlowPane grid = new FlowPane(); + protected ScrollPane scrollPane = new ScrollPane(); + protected ContextMenu popup; + + AbstractRecordedModelsTab(String text) { + super(text); + } + + protected void createGui() { + grid.setPadding(new Insets(5)); + grid.setHgap(5); + grid.setVgap(5); + + scrollPane.setContent(grid); + scrollPane.setFitToHeight(true); + scrollPane.setFitToWidth(true); + BorderPane.setMargin(scrollPane, new Insets(5)); + + table.setEditable(true); + table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + var previewPopupHandler = new PreviewPopupHandler(table); + table.setRowFactory(tableview -> { + TableRow row = new TableRow<>(); + row.addEventHandler(MouseEvent.ANY, previewPopupHandler); + return row; + }); + table.setItems(observableModels); + table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { + popup = createContextMenu(); + if (popup != null) { + popup.show(table, event.getScreenX(), event.getScreenY()); + } + event.consume(); + }); + table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { + if (popup != null) { + popup.hide(); + } + }); + table.addEventHandler(KeyEvent.KEY_PRESSED, event -> { + List selectedModels = table.getSelectionModel().getSelectedItems(); + if (event.getCode() == KeyCode.DELETE) { + stopAction(selectedModels); + } else { + jumpToNextModel(event.getCode()); + } + }); + + scrollPane.setContent(table); + + 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)); + + modelLabel.setPadding(new Insets(5, 0, 0, 0)); + ObservableList suggestions = FXCollections.observableArrayList(); + sites.forEach(site -> suggestions.add(site.getClass().getSimpleName())); + modelInputField = new AutoFillTextField(new ObservableListSuggester(suggestions)); + modelInputField.minWidth(150); + modelInputField.prefWidth(600); + HBox.setHgrow(modelInputField, Priority.ALWAYS); + modelInputField.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/"); + modelInputField.onActionHandler(this::addModel); + modelInputField.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)); + addModelBox.getChildren().addAll(modelLabel, modelInputField, addModelButton, checkModelAccountExistance); + + filterContainer.setPadding(new Insets(0)); + filterContainer.setAlignment(Pos.CENTER_RIGHT); + filterContainer.minWidth(100); + filterContainer.prefWidth(150); + HBox.setHgrow(filterContainer, Priority.ALWAYS); + filter = new SearchBox(false); + filter.minWidth(100); + filter.prefWidth(150); + filter.setPromptText("Filter"); + filter.textProperty().addListener((observableValue, oldValue, newValue) -> { + String q = filter.getText(); + lock.lock(); + try { + filter(q); + } finally { + lock.unlock(); + } + }); + filter.getStyleClass().remove("search-box-icon"); + + var columnSelection = new Button("⚙"); + columnSelection.setOnAction(this::showColumnSelection); + columnSelection.setTooltip(new Tooltip("Select columns")); + columnSelection.prefHeightProperty().bind(filter.prefHeightProperty()); + columnSelection.prefWidthProperty().bind(columnSelection.prefHeightProperty()); + + filterContainer.getChildren().addAll(columnSelection, filter); + addModelBox.getChildren().add(filterContainer); + } + + protected void addPreviewColumn(int columnIdx) { + TableColumn preview = addTableColumn("preview", "🎥", columnIdx, 35); + preview.setCellValueFactory(cdf -> new SimpleStringProperty(" ▶ ")); + preview.setEditable(false); + if (!Config.getInstance().getSettings().livePreviews) { + preview.setVisible(false); + } + } + + protected void addPortraitColumn(int columnIdx) { + TableColumn portrait = addTableColumn("portrait", "Portrait", columnIdx, 80); + portrait.setCellValueFactory(param -> { + Model mdl = param.getValue().getDelegate(); + Image image = null; + try { + image = portraitCache.get(mdl); + } catch (ExecutionException e) { + LOG.error("Error while loading portrait from cache for {}", mdl, e); + } + return new SimpleObjectProperty(image); + }); + portrait.setCellFactory(param -> new ImageTableCell()); + portrait.setEditable(false); + } + + protected void addModelColumn(int columnIdx) { + TableColumn name = addTableColumn("name", "Model", columnIdx, 200); + name.setCellValueFactory(param -> { + var modelName = new ModelName(param.getValue(), recorder); + return new SimpleObjectProperty<>(modelName); + }); + name.setCellFactory(param -> new ModelNameTableCell(recorder)); + name.setEditable(false); + } + + protected void addUrlColumn(int columnIdx) { + TableColumn url = addTableColumn("url", "URL", columnIdx, 400); + url.setCellValueFactory(new PropertyValueFactory<>("url")); + url.setCellFactory(new ClickableCellFactory<>()); + url.setEditable(false); + } + + protected void addNotesColumn(int columnIdx) { + TableColumn notes = addTableColumn("notes", "Notes", columnIdx, 400); + notes.setCellValueFactory(cdf -> { + JavaFxModel m = cdf.getValue(); + return new StringPropertyBase() { + @Override + public String getName() { + return "Model Notes"; + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String get() { + String modelNotes = Config.getInstance().getModelNotes(m); + return modelNotes; + } + }; + }); + notes.setEditable(false); + } + + abstract void stopAction(List selectedModels); + + protected TableColumn addTableColumn(String id, String text, int index, int width) { + TableColumn tc = new TableColumn<>(text); + tc.setId(id); + tc.setText(text); + tc.setUserData(index); + tc.setPrefWidth(width); + columns.add(tc); + addTableColumnIfEnabled(tc); + return tc; + } + + protected void addTableColumnIfEnabled(TableColumn tc) { + if(isColumnEnabled(tc)) { + table.getColumns().add(tc); + } + } + + protected ContextMenu createContextMenu() { + List selectedModels = table.getSelectionModel().getSelectedItems().stream().map(JavaFxModel::getDelegate).collect(Collectors.toList()); + if (selectedModels.isEmpty()) { + return null; + } + + ContextMenu menu = new CustomMouseBehaviorContextMenu(); + + ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) // + .withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) // + .removeModelAfterIgnore(true) // + .withPortraitCallback(m -> { + portraitCache.invalidate(m); + table.refresh(); + }) + .afterwards(table::refresh) // + .contributeToMenu(selectedModels, menu); + + return menu; + } + + protected void addModel(ActionEvent e) { + String input = modelInputField.getText().trim(); + if (StringUtil.isBlank(input)) { + return; + } + + if (input.startsWith("http")) { + addModelByUrl(input); + } else { + addModelByName(input); + } + } + + protected void addModelByUrl(String url) { + for (Site site : sites) { + var newModel = site.createModelFromUrl(url); + if (newModel != null) { + try { + newModel.setMarkedForLaterRecording(getMarkModelsForLaterRecording()); + recorder.addModel(newModel); + } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { + Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + newModel.getName() + " could not be added: ", e1); + } + return; + } + } + + Dialogs.showError(getTabPane().getScene(), "Unknown URL format", + "The URL you entered has an unknown format or the function does not support this site, yet", null); + } + + abstract boolean getMarkModelsForLaterRecording(); + + protected void addModelByName(String siteModelCombo) { + String[] parts = siteModelCombo.trim().split(":"); + if (parts.length != 2) { + Dialogs.showError(getTabPane().getScene(), "Wrong input format", "Use something like \"MyFreeCams:ModelName\"", null); + return; + } + + String siteName = parts[0]; + String modelName = parts[1]; + for (Site site : sites) { + if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) { + try { + var m = site.createModel(modelName); + m.setMarkedForLaterRecording(getMarkModelsForLaterRecording()); + recorder.addModel(m); + } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { + Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", e1); + } + return; + } + } + + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene()); + alert.setTitle("Unknown site"); + alert.setHeaderText("Couldn't add model"); + alert.setContentText("The site you entered is unknown"); + alert.showAndWait(); + } + + protected void jumpToNextModel(KeyCode code) { + if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) { + // determine where to start looking for the next model + var startAt = getJumpToStartIndex(); + + String c = code.getChar().toLowerCase(); + int i = startAt; + do { + JavaFxModel current = table.getItems().get(i); + if (current.getName().toLowerCase().replaceAll("[^0-9a-z]", "").startsWith(c)) { + table.getSelectionModel().clearAndSelect(i); + table.scrollTo(i); + break; + } + + i++; + if (i >= table.getItems().size()) { + i = 0; + } + } while (i != startAt); + } + } + + protected int getJumpToStartIndex() { + var startAt = 0; + if (table.getSelectionModel().getSelectedIndex() >= 0) { + startAt = table.getSelectionModel().getSelectedIndex() + 1; + if (startAt >= table.getItems().size()) { + startAt = 0; + } + } + return startAt; + } + + protected void filter(String filter) { + lock.lock(); + try { + if (StringUtil.isBlank(filter)) { + observableModels.addAll(filteredModels); + filteredModels.clear(); + return; + } + + String[] tokens = filter.split(" "); + observableModels.addAll(filteredModels); + filteredModels.clear(); + for (var i = 0; i < table.getItems().size(); i++) { + var sb = new StringBuilder(); + for (TableColumn tc : table.getColumns()) { + Object cellData = tc.getCellData(i); + if (cellData != null) { + var content = cellData.toString(); + sb.append(content).append(' '); + } + } + var searchText = sb.toString(); + + var tokensMissing = false; + for (String token : tokens) { + if (!searchText.toLowerCase().contains(token.toLowerCase())) { + tokensMissing = true; + break; + } + } + if (tokensMissing) { + JavaFxModel filteredModel = table.getItems().get(i); + filteredModels.add(filteredModel); + } + } + observableModels.removeAll(filteredModels); + } finally { + lock.unlock(); + } + } + + abstract String getSortColumn(); + abstract void setSortColumn(String column); + abstract String getSortType(); + abstract void setSortType(String sortType); + abstract String[] getColumnIds(); + abstract void setColumnIds(String[] ids); + abstract double[] getColumnWidths(); + abstract void setColumnWidths(double[] widths); + abstract List getDisabledColumns(); + + public void saveState() { + if (!table.getSortOrder().isEmpty()) { + TableColumn col = table.getSortOrder().get(0); + setSortColumn(col.getText()); + setSortType(col.getSortType().toString()); + } + int tableColumns = table.getColumns().size(); + var columnWidths = new double[tableColumns]; + var columnIds = new String[tableColumns]; + for (var i = 0; i < columnWidths.length; i++) { + columnWidths[i] = table.getColumns().get(i).getWidth(); + columnIds[i] = table.getColumns().get(i).getId(); + } + setColumnWidths(columnWidths); + setColumnIds(columnIds); + } + + protected void restoreState() { + restoreColumnOrder(); + restoreColumnWidths(); + restoreSorting(); + } + + + private void restoreSorting() { + String sortCol = getSortColumn(); + if (StringUtil.isNotBlank(sortCol)) { + for (TableColumn col : table.getColumns()) { + if (Objects.equals(sortCol, col.getText())) { + col.setSortType(SortType.valueOf(getSortType())); + table.getSortOrder().clear(); + table.getSortOrder().add(col); + break; + } + } + } + } + + private void restoreColumnOrder() { + String[] columnIds = getColumnIds(); + ObservableList> tableColumns = table.getColumns(); + for (var i = 0; i < columnIds.length; i++) { + for (var j = 0; j < table.getColumns().size(); j++) { + if(Objects.equals(columnIds[i], tableColumns.get(j).getId())) { + TableColumn col = tableColumns.get(j); + tableColumns.remove(j); // NOSONAR + tableColumns.add(Math.min(i, tableColumns.size()), col); + } + } + } + } + + private void restoreColumnWidths() { + double[] columnWidths = getColumnWidths(); + if (columnWidths != null && columnWidths.length == table.getColumns().size()) { + for (var i = 0; i < columnWidths.length; i++) { + table.getColumns().get(i).setPrefWidth(columnWidths[i]); + } + } + } + + protected class ClickableCellFactory implements Callback, TableCell> { + @Override + public TableCell call(TableColumn param) { + TableCell cell = new TableCell<>() { + @Override + protected void updateItem(Object item, boolean empty) { + setText(empty ? "" : Objects.toString(item)); + } + }; + + cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { + JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem(); + if (selectedModel != null) { + new PlayAction(table, selectedModel).execute(); + } + } + }); + return cell; + } + } + + protected static Image loadModelPortrait(Model model) { + String portraitId = Config.getInstance().getSettings().modelPortraits.get(model.getUrl()); + if (StringUtil.isNotBlank(portraitId)) { + File configDir = Config.getInstance().getConfigDir(); + File portraitDir = new File(configDir, "portraits"); + File portraitFile = new File(portraitDir, portraitId + '.' + SetPortraitAction.FORMAT); + try { + return new Image(new FileInputStream(portraitFile)); + } catch (FileNotFoundException e) { + LOG.error("Couldn't load portrait file {}", portraitFile, e); + } + } + return new Image(AbstractRecordedModelsTab.class.getResourceAsStream("/silhouette_256.png")); + } + + protected void showColumnSelection(ActionEvent evt) { + ContextMenu menu = new CustomMouseBehaviorContextMenu(); + for (TableColumn tc : columns) { + var item = new CheckMenuItem(tc.getText()); + item.setSelected(isColumnEnabled(tc)); + menu.getItems().add(item); + item.setOnAction(e -> { + try { + if (item.isSelected()) { + getDisabledColumns().remove(tc.getText()); + boolean added = false; + for (int i = table.getColumns().size() - 1; i >= 0; i--) { + TableColumn other = table.getColumns().get(i); + if (!other.isVisible()) { + continue; + } + int idx = (int) tc.getUserData(); + int otherIdx = (int) other.getUserData(); + if (otherIdx < idx) { + table.getColumns().add(i + 1, tc); + added = true; + break; + } + } + if (!added) { + table.getColumns().add(0, tc); + } + } else { + getDisabledColumns().add(tc.getText()); + table.getColumns().remove(tc); + } + } catch (Exception ex) { + LOG.error("Couldn't activate column {}", tc, ex); + } + }); + } + Button src = (Button) evt.getSource(); + Point2D location = src.localToScreen(src.getTranslateX(), src.getTranslateY()); + menu.show(getTabPane().getScene().getWindow(), location.getX(), location.getY() + src.getHeight() + 5); + } + + private boolean isColumnEnabled(TableColumn tc) { + return !getDisabledColumns().contains(tc.getText()); + } + + +} diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/ModelName.java b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelName.java new file mode 100644 index 00000000..336210b3 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelName.java @@ -0,0 +1,29 @@ +package ctbrec.ui.tabs.recorded; + +import java.util.Optional; + +import ctbrec.Model; +import ctbrec.ModelGroup; +import ctbrec.recorder.Recorder; + +public class ModelName { + private Model mdl; + private Recorder rec; + + public ModelName(Model model, Recorder recorder) { + mdl = model; + rec = recorder; + } + + @Override + public String toString() { + Optional modelGroup = rec.getModelGroup(mdl); + String s; + if (modelGroup.isPresent()) { + s = modelGroup.get().getName() + " (aka " + mdl.getDisplayName() + ')'; + } else { + return mdl.toString(); + } + return s; + } +} \ No newline at end of file diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/ModelNameTableCell.java b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelNameTableCell.java index 76ef57c9..59122b4b 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/ModelNameTableCell.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelNameTableCell.java @@ -7,7 +7,6 @@ import java.util.stream.Collectors; import ctbrec.Model; import ctbrec.recorder.Recorder; -import ctbrec.ui.tabs.recorded.RecordedModelsTab.ModelName; import javafx.scene.image.ImageView; public class ModelNameTableCell extends IconTableCell { diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java index 258c0bf9..f5ff66fa 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordLaterTab.java @@ -5,11 +5,9 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.List; -import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -17,83 +15,25 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Model; -import ctbrec.StringUtil; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; -import ctbrec.ui.AutosizeAlert; import ctbrec.ui.JavaFxModel; -import ctbrec.ui.PreviewPopupHandler; import ctbrec.ui.action.CheckModelAccountAction; -import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.StopRecordingAction; -import ctbrec.ui.controls.CustomMouseBehaviorContextMenu; 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.ModelMenuContributor; import ctbrec.ui.tabs.TabSelectionListener; -import ctbrec.ui.tabs.recorded.RecordedModelsTab.ModelName; import javafx.application.Platform; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringPropertyBase; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.concurrent.WorkerStateEvent; -import javafx.event.ActionEvent; import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Cursor; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.SelectionMode; -import javafx.scene.control.Tab; -import javafx.scene.control.TableCell; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableColumn.SortType; -import javafx.scene.control.TableRow; -import javafx.scene.control.TableView; -import javafx.scene.control.TextField; -import javafx.scene.control.Tooltip; -import javafx.scene.control.cell.PropertyValueFactory; -import javafx.scene.input.ContextMenuEvent; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.FlowPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.util.Callback; import javafx.util.Duration; -public class RecordLaterTab extends Tab implements TabSelectionListener { +public class RecordLaterTab extends AbstractRecordedModelsTab implements TabSelectionListener { private static final Logger LOG = LoggerFactory.getLogger(RecordLaterTab.class); - private ReentrantLock lock = new ReentrantLock(); private ScheduledService> updateService; - private Recorder recorder; - private List sites; - - FlowPane grid = new FlowPane(); - ScrollPane scrollPane = new ScrollPane(); - TableView table = new TableView<>(); - ObservableList observableModels = FXCollections.observableArrayList(); - ObservableList filteredModels = FXCollections.observableArrayList(); - ContextMenu popup; - - Label modelLabel = new Label("Model"); - AutoFillTextField model; - Button addModelButton = new Button("Record"); - Button checkModelAccountExistance = new Button("Check URLs"); - TextField filter; public RecordLaterTab(String title, Recorder recorder, List sites) { super(title); @@ -104,141 +44,16 @@ public class RecordLaterTab extends Tab implements TabSelectionListener { initializeUpdateService(); } - @SuppressWarnings("unchecked") - private void createGui() { - grid.setPadding(new Insets(5)); - grid.setHgap(5); - grid.setVgap(5); + @Override + protected void createGui() { + super.createGui(); - scrollPane.setContent(grid); - scrollPane.setFitToHeight(true); - scrollPane.setFitToWidth(true); - BorderPane.setMargin(scrollPane, new Insets(5)); - - table.setEditable(true); - table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - var previewPopupHandler = new PreviewPopupHandler(table); - table.setRowFactory(tableview -> { - TableRow row = new TableRow<>(); - row.addEventHandler(MouseEvent.ANY, previewPopupHandler); - return row; - }); - TableColumn preview = new TableColumn<>("🎥"); - preview.setPrefWidth(35); - preview.setCellValueFactory(cdf -> new SimpleStringProperty(" ▶ ")); - preview.setEditable(false); - preview.setId("preview"); - if (!Config.getInstance().getSettings().livePreviews) { - preview.setVisible(false); - } - TableColumn name = new TableColumn<>("Model"); - name.setPrefWidth(200); - name.setCellValueFactory(param -> { - var modelName = new ModelName(param.getValue(), recorder); - return new SimpleObjectProperty<>(modelName); - }); - name.setCellFactory(param -> new ModelNameTableCell(recorder)); - name.setEditable(false); - name.setId("name"); - TableColumn url = new TableColumn<>("URL"); - url.setCellValueFactory(new PropertyValueFactory<>("url")); - url.setCellFactory(new ClickableCellFactory<>()); - url.setPrefWidth(400); - url.setEditable(false); - url.setId("url"); - TableColumn notes = new TableColumn<>("Notes"); - notes.setCellValueFactory(cdf -> { - JavaFxModel m = cdf.getValue(); - return new StringPropertyBase() { - @Override - public String getName() { - return "Model Notes"; - } - - @Override - public Object getBean() { - return null; - } - - @Override - public String get() { - String modelNotes = Config.getInstance().getModelNotes(m); - return modelNotes; - } - }; - }); - notes.setPrefWidth(400); - notes.setEditable(false); - notes.setId("notes"); - table.getColumns().addAll(preview, name, url, notes); - table.setItems(observableModels); - table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { - popup = createContextMenu(); - if (popup != null) { - popup.show(table, event.getScreenX(), event.getScreenY()); - } - event.consume(); - }); - table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { - if (popup != null) { - popup.hide(); - } - }); - table.addEventHandler(KeyEvent.KEY_PRESSED, event -> { - List selectedModels = table.getSelectionModel().getSelectedItems(); - if (event.getCode() == KeyCode.DELETE) { - stopAction(selectedModels); - } else { - jumpToNextModel(event.getCode()); - } - }); - - scrollPane.setContent(table); - - var addModelBox = new HBox(5); - modelLabel.setPadding(new Insets(5, 0, 0, 0)); - ObservableList suggestions = FXCollections.observableArrayList(); - sites.forEach(site -> suggestions.add(site.getClass().getSimpleName())); - model = new AutoFillTextField(new ObservableListSuggester(suggestions)); - model.minWidth(150); - model.prefWidth(600); - 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")); - BorderPane.setMargin(addModelBox, new Insets(5)); - addModelButton.setOnAction(this::addModel); - addModelButton.setPadding(new Insets(5)); - addModelBox.getChildren().addAll(modelLabel, model, addModelButton, checkModelAccountExistance); - 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(Model::isMarkedForLaterRecording)); - - var filterContainer = new HBox(); - filterContainer.setSpacing(0); - filterContainer.setPadding(new Insets(0)); - filterContainer.setAlignment(Pos.CENTER_RIGHT); - filterContainer.minWidth(100); - filterContainer.prefWidth(150); - HBox.setHgrow(filterContainer, Priority.ALWAYS); - filter = new SearchBox(false); - filter.minWidth(100); - filter.prefWidth(150); - filter.setPromptText("Filter"); - filter.textProperty().addListener((observableValue, oldValue, newValue) -> { - String q = filter.getText(); - lock.lock(); - try { - filter(q); - } finally { - lock.unlock(); - } - }); - filter.getStyleClass().remove("search-box-icon"); - filterContainer.getChildren().add(filter); - addModelBox.getChildren().add(filterContainer); + int columnIdx = 0; + addPreviewColumn(columnIdx++); + addPortraitColumn(columnIdx++); + addModelColumn(columnIdx++); + addUrlColumn(columnIdx++); + addNotesColumn(columnIdx); var root = new BorderPane(); root.setPadding(new Insets(5)); @@ -246,98 +61,12 @@ public class RecordLaterTab extends Tab implements TabSelectionListener { root.setCenter(scrollPane); setContent(root); + checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder) + .execute(Model::isMarkedForLaterRecording)); + restoreState(); } - private void jumpToNextModel(KeyCode code) { - if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) { - // determine where to start looking for the next model - var startAt = 0; - if (table.getSelectionModel().getSelectedIndex() >= 0) { - startAt = table.getSelectionModel().getSelectedIndex() + 1; - if (startAt >= table.getItems().size()) { - startAt = 0; - } - } - - String c = code.getChar().toLowerCase(); - int i = startAt; - do { - JavaFxModel current = table.getItems().get(i); - if (current.getName().toLowerCase().replaceAll("[^0-9a-z]", "").startsWith(c)) { - table.getSelectionModel().clearAndSelect(i); - table.scrollTo(i); - break; - } - - i++; - if (i >= table.getItems().size()) { - i = 0; - } - } while (i != startAt); - } - } - - private void addModel(ActionEvent e) { - String input = model.getText().trim(); - if (StringUtil.isBlank(input)) { - return; - } - - if (input.startsWith("http")) { - addModelByUrl(input); - } else { - addModelByName(input); - } - } - - private void addModelByUrl(String url) { - for (Site site : sites) { - var newModel = site.createModelFromUrl(url); - if (newModel != null) { - try { - newModel.setMarkedForLaterRecording(true); - recorder.addModel(newModel); - } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + newModel.getName() + " could not be added: ", e1); - } - return; - } - } - - Dialogs.showError(getTabPane().getScene(), "Unknown URL format", - "The URL you entered has an unknown format or the function does not support this site, yet", null); - } - - private void addModelByName(String siteModelCombo) { - String[] parts = siteModelCombo.trim().split(":"); - if (parts.length != 2) { - Dialogs.showError(getTabPane().getScene(), "Wrong input format", "Use something like \"MyFreeCams:ModelName\"", null); - return; - } - - String siteName = parts[0]; - String modelName = parts[1]; - for (Site site : sites) { - if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) { - try { - var m = site.createModel(modelName); - m.setMarkedForLaterRecording(true); - recorder.addModel(m); - } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", e1); - } - return; - } - } - - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene()); - alert.setTitle("Unknown site"); - alert.setHeaderText("Couldn't add model"); - alert.setContentText("The site you entered is unknown"); - alert.showAndWait(); - } - void initializeUpdateService() { updateService = createUpdateService(); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2))); @@ -388,47 +117,6 @@ public class RecordLaterTab extends Tab implements TabSelectionListener { } } - private void filter(String filter) { - lock.lock(); - try { - if (StringUtil.isBlank(filter)) { - observableModels.addAll(filteredModels); - filteredModels.clear(); - return; - } - - String[] tokens = filter.split(" "); - observableModels.addAll(filteredModels); - filteredModels.clear(); - for (var i = 0; i < table.getItems().size(); i++) { - var sb = new StringBuilder(); - for (TableColumn tc : table.getColumns()) { - Object cellData = tc.getCellData(i); - if (cellData != null) { - var content = cellData.toString(); - sb.append(content).append(' '); - } - } - var searchText = sb.toString(); - - var tokensMissing = false; - for (String token : tokens) { - if (!searchText.toLowerCase().contains(token.toLowerCase())) { - tokensMissing = true; - break; - } - } - if (tokensMissing) { - JavaFxModel filteredModel = table.getItems().get(i); - filteredModels.add(filteredModel); - } - } - observableModels.removeAll(filteredModels); - } finally { - lock.unlock(); - } - } - private ScheduledService> createUpdateService() { ScheduledService> modelUpdateService = new ScheduledService>() { @Override @@ -467,23 +155,8 @@ public class RecordLaterTab extends Tab implements TabSelectionListener { } } - private ContextMenu createContextMenu() { - List selectedModels = table.getSelectionModel().getSelectedItems().stream().map(JavaFxModel::getDelegate).collect(Collectors.toList()); - if (selectedModels.isEmpty()) { - return null; - } - - ContextMenu menu = new CustomMouseBehaviorContextMenu(); - - ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) // - .withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) // - .afterwards(table::refresh) - .contributeToMenu(selectedModels, menu); - - return menu; - } - - private void stopAction(List selectedModels) { + @Override + void stopAction(List selectedModels) { var confirmed = true; if (Config.getInstance().getSettings().confirmationForDangerousActions) { int n = selectedModels.size(); @@ -500,85 +173,53 @@ public class RecordLaterTab extends Tab implements TabSelectionListener { } } - public void saveState() { - if (!table.getSortOrder().isEmpty()) { - TableColumn col = table.getSortOrder().get(0); - Config.getInstance().getSettings().recordLaterSortColumn = col.getText(); - Config.getInstance().getSettings().recordLaterSortType = col.getSortType().toString(); - } - int columns = table.getColumns().size(); - var columnWidths = new double[columns]; - var columnIds = new String[columns]; - for (var i = 0; i < columnWidths.length; i++) { - columnWidths[i] = table.getColumns().get(i).getWidth(); - columnIds[i] = table.getColumns().get(i).getId(); - } - Config.getInstance().getSettings().recordLaterColumnWidths = columnWidths; + @Override + String getSortColumn() { + return Config.getInstance().getSettings().recordLaterSortColumn; + } + + @Override + void setSortColumn(String column) { + Config.getInstance().getSettings().recordLaterSortColumn = column; + } + + @Override + String getSortType() { + return Config.getInstance().getSettings().recordLaterSortType; + } + + @Override + void setSortType(String sortType) { + Config.getInstance().getSettings().recordLaterSortType = sortType; + } + + @Override + String[] getColumnIds() { + return Config.getInstance().getSettings().recordLaterColumnIds; + } + + @Override + void setColumnIds(String[] columnIds) { Config.getInstance().getSettings().recordLaterColumnIds = columnIds; } - private void restoreState() { - restoreColumnOrder(); - restoreColumnWidths(); - restoreSorting(); + @Override + double[] getColumnWidths() { + return Config.getInstance().getSettings().recordLaterColumnWidths; } - private void restoreSorting() { - String sortCol = Config.getInstance().getSettings().recordLaterSortColumn; - if (StringUtil.isNotBlank(sortCol)) { - for (TableColumn col : table.getColumns()) { - if (Objects.equals(sortCol, col.getText())) { - col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordLaterSortType)); - table.getSortOrder().clear(); - table.getSortOrder().add(col); - break; - } - } - } + @Override + void setColumnWidths(double[] widths) { + Config.getInstance().getSettings().recordLaterColumnWidths = widths; } - private void restoreColumnOrder() { - String[] columnIds = Config.getInstance().getSettings().recordLaterColumnIds; - 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())) { - TableColumn col = columns.get(j); - columns.remove(j); // NOSONAR - columns.add(i, col); - } - } - } + @Override + List getDisabledColumns() { + return Config.getInstance().getSettings().recordLaterDisabledColumns; } - private void restoreColumnWidths() { - double[] columnWidths = Config.getInstance().getSettings().recordLaterColumnWidths; - if (columnWidths != null && columnWidths.length == table.getColumns().size()) { - for (var i = 0; i < columnWidths.length; i++) { - table.getColumns().get(i).setPrefWidth(columnWidths[i]); - } - } - } - - private class ClickableCellFactory implements Callback, TableCell> { - @Override - public TableCell call(TableColumn param) { - TableCell cell = new TableCell<>() { - @Override - protected void updateItem(Object item, boolean empty) { - setText(empty ? "" : Objects.toString(item)); - } - }; - - cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { - if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { - JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem(); - if (selectedModel != null) { - new PlayAction(table, selectedModel).execute(); - } - } - }); - return cell; - } + @Override + boolean getMarkModelsForLaterRecording() { + return true; } } 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 99f65314..8a53e677 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/RecordedModelsTab.java @@ -2,141 +2,70 @@ package ctbrec.ui.tabs.recorded; import static ctbrec.Recording.State.*; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Instant; -import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ExecutionException; 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.Predicate; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; - import ctbrec.Config; import ctbrec.Model; -import ctbrec.ModelGroup; import ctbrec.Recording; -import ctbrec.StringUtil; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; -import ctbrec.ui.AutosizeAlert; import ctbrec.ui.JavaFxModel; -import ctbrec.ui.PreviewPopupHandler; import ctbrec.ui.action.CheckModelAccountAction; import ctbrec.ui.action.PauseAction; -import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.ResumeAction; -import ctbrec.ui.action.SetPortraitAction; import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.action.ToggleRecordingAction; -import ctbrec.ui.controls.CustomMouseBehaviorContextMenu; import ctbrec.ui.controls.DateTimeCellFactory; 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.ModelMenuContributor; import ctbrec.ui.tabs.TabSelectionListener; import javafx.application.Platform; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringPropertyBase; import javafx.beans.value.ChangeListener; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.concurrent.WorkerStateEvent; import javafx.event.ActionEvent; import javafx.geometry.Insets; -import javafx.geometry.Point2D; -import javafx.geometry.Pos; -import javafx.scene.Cursor; -import javafx.scene.control.Alert; import javafx.scene.control.Button; -import javafx.scene.control.CheckMenuItem; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.SelectionMode; -import javafx.scene.control.Tab; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.CellEditEvent; -import javafx.scene.control.TableColumn.SortType; -import javafx.scene.control.TableRow; -import javafx.scene.control.TableView; -import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; -import javafx.scene.control.Tooltip; import javafx.scene.control.cell.CheckBoxTableCell; -import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.TextFieldTableCell; -import javafx.scene.image.Image; -import javafx.scene.input.ContextMenuEvent; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; import javafx.util.Callback; import javafx.util.Duration; import javafx.util.StringConverter; import javafx.util.converter.NumberStringConverter; -public class RecordedModelsTab extends Tab implements TabSelectionListener { +public class RecordedModelsTab extends AbstractRecordedModelsTab implements TabSelectionListener { private static final Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class); private static final String STYLE_ALIGN_CENTER = "-fx-alignment: CENTER;"; - private ReentrantLock lock = new ReentrantLock(); private ScheduledService> updateService; - private Recorder recorder; - private List sites; private volatile boolean cellEditing = false; - FlowPane grid = new FlowPane(); - ScrollPane scrollPane = new ScrollPane(); - TableView table = new TableView<>(); - private List> columns = new ArrayList<>(); - ObservableList observableModels = FXCollections.observableArrayList(); - ObservableList filteredModels = FXCollections.observableArrayList(); - ContextMenu popup; - - Label modelLabel = new Label("Model"); - AutoFillTextField model; - Button addModelButton = new Button("Record"); Button pauseAll = new Button("Pause All"); Button resumeAll = new Button("Resume All"); ToggleButton toggleRecording = new ToggleButton("Pause Recording"); - Button checkModelAccountExistance = new Button("Check URLs"); - TextField filter; - - LoadingCache portraitCache = CacheBuilder.newBuilder() - .expireAfterAccess(1, TimeUnit.DAYS) - .maximumSize(1000) - .build(CacheLoader.from(RecordedModelsTab::loadModelPortrait)); public RecordedModelsTab(String title, Recorder recorder, List sites) { super(title); @@ -147,77 +76,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { initializeUpdateService(); } - private void createGui() { - grid.setPadding(new Insets(5)); - grid.setHgap(5); - grid.setVgap(5); - - scrollPane.setContent(grid); - scrollPane.setFitToHeight(true); - scrollPane.setFitToWidth(true); - BorderPane.setMargin(scrollPane, new Insets(5)); + @Override + protected void createGui() { + super.createGui(); int idx = 0; - table.setEditable(true); - table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - var previewPopupHandler = new PreviewPopupHandler(table); - table.setRowFactory(tableview -> { - TableRow row = new TableRow<>(); - row.addEventHandler(MouseEvent.ANY, previewPopupHandler); - return row; - }); - TableColumn preview = new TableColumn<>("🎥"); - preview.setPrefWidth(35); - preview.setCellValueFactory(cdf -> new SimpleStringProperty(" ▶ ")); - preview.setEditable(false); - preview.setId("preview"); - preview.setUserData(idx++); - columns.add(preview); - addTableColumnIfEnabled(preview); - if (!Config.getInstance().getSettings().livePreviews) { - preview.setVisible(false); - } - TableColumn portrait = new TableColumn<>("Portrait"); - portrait.setPrefWidth(80); - portrait.setCellValueFactory(param -> { - Model mdl = param.getValue().getDelegate(); - Image image = null; - try { - image = portraitCache.get(mdl); - } catch (ExecutionException e) { - LOG.error("Error while loading portrait from cache for {}", mdl, e); - } - return new SimpleObjectProperty(image); - }); - portrait.setCellFactory(param -> new ImageTableCell()); - portrait.setEditable(false); - portrait.setId("portrait"); - portrait.setUserData(idx++); - columns.add(portrait); - addTableColumnIfEnabled(portrait); + addPreviewColumn(idx++); + addPortraitColumn(idx++); + addModelColumn(idx++); + addUrlColumn(idx++); - TableColumn name = new TableColumn<>("Model"); - name.setPrefWidth(200); - name.setCellValueFactory(param -> { - var modelName = new ModelName(param.getValue(), recorder); - return new SimpleObjectProperty<>(modelName); - }); - name.setCellFactory(param -> new ModelNameTableCell(recorder)); - name.setEditable(false); - name.setId("name"); - name.setUserData(idx++); - columns.add(name); - addTableColumnIfEnabled(name); - TableColumn url = new TableColumn<>("URL"); - url.setCellValueFactory(new PropertyValueFactory<>("url")); - url.setCellFactory(new ClickableCellFactory<>()); - url.setPrefWidth(400); - url.setEditable(false); - url.setId("url"); - url.setUserData(idx++); - columns.add(url); - addTableColumnIfEnabled(url); TableColumn online = new TableColumn<>("Online"); online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty()); online.setCellFactory(param -> new OnlineTableCell()); @@ -277,73 +146,12 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { lastRecorded.setUserData(idx++); columns.add(lastRecorded); addTableColumnIfEnabled(lastRecorded); - TableColumn notes = new TableColumn<>("Notes"); - notes.setCellValueFactory(cdf -> { - JavaFxModel m = cdf.getValue(); - return new StringPropertyBase() { - @Override - public String getName() { - return "Model Notes"; - } - @Override - public Object getBean() { - return null; - } + addNotesColumn(idx); - @Override - public String get() { - String modelNotes = Config.getInstance().getModelNotes(m); - return modelNotes; - } - }; - }); - notes.setPrefWidth(400); - notes.setEditable(false); - notes.setId("notes"); - notes.setUserData(idx); - columns.add(notes); - addTableColumnIfEnabled(notes); - - table.setItems(observableModels); - table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { - popup = createContextMenu(); - if (popup != null) { - popup.show(table, event.getScreenX(), event.getScreenY()); - } - event.consume(); - }); - table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { - if (popup != null) { - popup.hide(); - } - }); - table.addEventHandler(KeyEvent.KEY_PRESSED, event -> { - List selectedModels = table.getSelectionModel().getSelectedItems(); - if (event.getCode() == KeyCode.DELETE) { - stopAction(selectedModels); - } else { - jumpToNextModel(event.getCode()); - } - }); - - scrollPane.setContent(table); - - var addModelBox = new HBox(5); - modelLabel.setPadding(new Insets(5, 0, 0, 0)); - ObservableList suggestions = FXCollections.observableArrayList(); - sites.forEach(site -> suggestions.add(site.getClass().getSimpleName())); - model = new AutoFillTextField(new ObservableListSuggester(suggestions)); - model.minWidth(150); - model.prefWidth(600); - 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")); - BorderPane.setMargin(addModelBox, new Insets(5)); - addModelButton.setOnAction(this::addModel); - addModelButton.setPadding(new Insets(5)); - addModelBox.getChildren().addAll(modelLabel, model, addModelButton, pauseAll, resumeAll, toggleRecording, checkModelAccountExistance); + addModelBox.getChildren().add(3, pauseAll); + addModelBox.getChildren().add(4, resumeAll); + addModelBox.getChildren().add(5, toggleRecording); HBox.setMargin(pauseAll, new Insets(0, 0, 0, 20)); pauseAll.setOnAction(this::pauseAll); resumeAll.setOnAction(this::resumeAll); @@ -352,44 +160,10 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { toggleRecording.setPadding(new Insets(5)); toggleRecording.setOnAction(this::toggleRecording); HBox.setMargin(toggleRecording, new Insets(0, 0, 0, 20)); - 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))); - - var filterContainer = new HBox(); - filterContainer.setSpacing(5); - filterContainer.setPadding(new Insets(0)); - filterContainer.setAlignment(Pos.CENTER_RIGHT); - filterContainer.minWidth(100); - filterContainer.prefWidth(150); - HBox.setHgrow(filterContainer, Priority.ALWAYS); - filter = new SearchBox(false); - filter.minWidth(100); - filter.prefWidth(150); - filter.setPromptText("Filter"); - filter.textProperty().addListener((observableValue, oldValue, newValue) -> { - String q = filter.getText(); - lock.lock(); - try { - filter(q); - } finally { - lock.unlock(); - } - }); - filter.getStyleClass().remove("search-box-icon"); - - var columnSelection = new Button("⚙"); - columnSelection.setOnAction(this::showColumnSelection); - columnSelection.setTooltip(new Tooltip("Select columns")); - columnSelection.prefHeightProperty().bind(filter.prefHeightProperty()); - columnSelection.prefWidthProperty().bind(columnSelection.prefHeightProperty()); - - filterContainer.getChildren().addAll(columnSelection, filter); - addModelBox.getChildren().add(filterContainer); - var root = new BorderPane(); root.setPadding(new Insets(5)); root.setTop(addModelBox); @@ -399,41 +173,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { restoreState(); } - private void addTableColumnIfEnabled(TableColumn tc) { - if(isColumnEnabled(tc)) { - table.getColumns().add(tc); - } - } - - private void jumpToNextModel(KeyCode code) { - if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) { - // determine where to start looking for the next model - var startAt = 0; - if (table.getSelectionModel().getSelectedIndex() >= 0) { - startAt = table.getSelectionModel().getSelectedIndex() + 1; - if (startAt >= table.getItems().size()) { - startAt = 0; - } - } - - var c = code.getChar().toLowerCase(); - int i = startAt; - do { - JavaFxModel current = table.getItems().get(i); - if (current.getName().toLowerCase().replaceAll("[^0-9a-z]", "").startsWith(c)) { - table.getSelectionModel().clearAndSelect(i); - table.scrollTo(i); - break; - } - - i++; - if (i >= table.getItems().size()) { - i = 0; - } - } while (i != startAt); - } - } - private void onUpdatePriority(CellEditEvent evt) { try { int prio = Optional.ofNullable(evt.getNewValue()).map(Number::intValue).orElse(-1); @@ -459,64 +198,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } } - private void addModel(ActionEvent e) { - String input = model.getText().trim(); - if (StringUtil.isBlank(input)) { - return; - } - - if (input.startsWith("http")) { - addModelByUrl(input); - } else { - addModelByName(input); - } - } - - private void addModelByUrl(String url) { - for (Site site : sites) { - var newModel = site.createModelFromUrl(url); - if (newModel != null) { - try { - recorder.addModel(newModel); - } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + newModel.getName() + " could not be added: ", e1); - } - return; - } - } - - Dialogs.showError(getTabPane().getScene(), "Unknown URL format", - "The URL you entered has an unknown format or the function does not support this site, yet", null); - } - - private void addModelByName(String siteModelCombo) { - String[] parts = siteModelCombo.trim().split(":"); - if (parts.length != 2) { - Dialogs.showError(getTabPane().getScene(), "Wrong input format", "Use something like \"MyFreeCams:ModelName\"", null); - return; - } - - String siteName = parts[0]; - String modelName = parts[1]; - for (Site site : sites) { - if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) { - try { - var m = site.createModel(modelName); - recorder.addModel(m); - } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", e1); - } - return; - } - } - - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene()); - alert.setTitle("Unknown site"); - alert.setHeaderText("Couldn't add model"); - alert.setContentText("The site you entered is unknown"); - alert.showAndWait(); - } - private void pauseAll(ActionEvent evt) { boolean yes = Dialogs.showConfirmDialog("Pause all models", "", "Pause the recording of all models?", getTabPane().getScene()); if (yes) { @@ -609,47 +290,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { }; } - private void filter(String filter) { - lock.lock(); - try { - if (StringUtil.isBlank(filter)) { - observableModels.addAll(filteredModels); - filteredModels.clear(); - return; - } - - String[] tokens = filter.split(" "); - observableModels.addAll(filteredModels); - filteredModels.clear(); - for (var i = 0; i < table.getItems().size(); i++) { - var sb = new StringBuilder(); - for (TableColumn tc : table.getColumns()) { - Object cellData = tc.getCellData(i); - if (cellData != null) { - var content = cellData.toString(); - sb.append(content).append(' '); - } - } - var searchText = sb.toString(); - - var tokensMissing = false; - for (String token : tokens) { - if (!searchText.toLowerCase().contains(token.toLowerCase())) { - tokensMissing = true; - break; - } - } - if (tokensMissing) { - JavaFxModel filteredModel = table.getItems().get(i); - filteredModels.add(filteredModel); - } - } - observableModels.removeAll(filteredModels); - } finally { - lock.unlock(); - } - } - private ScheduledService> createUpdateService() { ScheduledService> modelUpdateService = new ScheduledService>() { @Override @@ -704,28 +344,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } } - private ContextMenu createContextMenu() { - List selectedModels = table.getSelectionModel().getSelectedItems().stream().map(JavaFxModel::getDelegate).collect(Collectors.toList()); - if (selectedModels.isEmpty()) { - return null; - } - - ContextMenu menu = new CustomMouseBehaviorContextMenu(); - - ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) // - .withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) // - .removeModelAfterIgnore(true) // - .withPortraitCallback(m -> { - portraitCache.invalidate(m); - table.refresh(); - }) - .afterwards(table::refresh) // - .contributeToMenu(selectedModels, menu); - - return menu; - } - - private boolean stopAction(List selectedModels) { + @Override + void stopAction(List selectedModels) { var confirmed = true; if (Config.getInstance().getSettings().confirmationForDangerousActions) { int n = selectedModels.size(); @@ -740,7 +360,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { table.getItems().remove(m); })); } - return confirmed; } private void pauseRecording(List selectedModels) { @@ -753,88 +372,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { new ResumeAction(getTabPane(), models, recorder).execute(); } - public void saveState() { - if (!table.getSortOrder().isEmpty()) { - TableColumn col = table.getSortOrder().get(0); - Config.getInstance().getSettings().recordedModelsSortColumn = col.getText(); - Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString(); - } - int columns = table.getColumns().size(); - var columnWidths = new double[columns]; - var columnIds = new String[columns]; - for (var i = 0; i < columnWidths.length; i++) { - columnWidths[i] = table.getColumns().get(i).getWidth(); - columnIds[i] = table.getColumns().get(i).getId(); - } - Config.getInstance().getSettings().recordedModelsColumnWidths = columnWidths; - Config.getInstance().getSettings().recordedModelsColumnIds = columnIds; - } - - private void restoreState() { - restoreColumnOrder(); - restoreColumnWidths(); - restoreSorting(); - } - - private void restoreSorting() { - String sortCol = Config.getInstance().getSettings().recordedModelsSortColumn; - if (StringUtil.isNotBlank(sortCol)) { - for (TableColumn col : table.getColumns()) { - if (Objects.equals(sortCol, col.getText())) { - col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordedModelsSortType)); - table.getSortOrder().clear(); - table.getSortOrder().add(col); - break; - } - } - } - } - - private void restoreColumnOrder() { - String[] columnIds = Config.getInstance().getSettings().recordedModelsColumnIds; - 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())) { - TableColumn col = columns.get(j); - columns.remove(j); // NOSONAR - columns.add(i, col); - } - } - } - } - - private void restoreColumnWidths() { - double[] columnWidths = Config.getInstance().getSettings().recordedModelsColumnWidths; - if (columnWidths != null && columnWidths.length == table.getColumns().size()) { - for (var i = 0; i < columnWidths.length; i++) { - table.getColumns().get(i).setPrefWidth(columnWidths[i]); - } - } - } - - private class ClickableCellFactory implements Callback, TableCell> { - @Override - public TableCell call(TableColumn param) { - TableCell cell = new TableCell<>() { - @Override - protected void updateItem(Object item, boolean empty) { - setText(empty ? "" : Objects.toString(item)); - } - }; - - cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { - if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { - JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem(); - if (selectedModel != null) { - new PlayAction(table, selectedModel).execute(); - } - } - }); - return cell; - } - } - private class PriorityCellFactory implements Callback, TableCell> { @Override public TableCell call(TableColumn param) { @@ -862,86 +399,53 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } } - public static class ModelName { - private Model mdl; - private Recorder rec; - - public ModelName(Model model, Recorder recorder) { - mdl = model; - rec = recorder; - } - - @Override - public String toString() { - Optional modelGroup = rec.getModelGroup(mdl); - String s; - if (modelGroup.isPresent()) { - s = modelGroup.get().getName() + " (aka " + mdl.getDisplayName() + ')'; - } else { - return mdl.toString(); - } - return s; - } + @Override + boolean getMarkModelsForLaterRecording() { + return false; } - private static Image loadModelPortrait(Model model) { - String portraitId = Config.getInstance().getSettings().modelPortraits.get(model.getUrl()); - if (StringUtil.isNotBlank(portraitId)) { - File configDir = Config.getInstance().getConfigDir(); - File portraitDir = new File(configDir, "portraits"); - File portraitFile = new File(portraitDir, portraitId + '.' + SetPortraitAction.FORMAT); - try { - return new Image(new FileInputStream(portraitFile)); - } catch (FileNotFoundException e) { - LOG.error("Couldn't load portrait file {}", portraitFile, e); - } - } - return new Image(RecordedModelsTab.class.getResourceAsStream("/silhouette_256.png")); + @Override + String getSortColumn() { + return Config.getInstance().getSettings().recordedModelsSortColumn; } - private void showColumnSelection(ActionEvent evt) { - ContextMenu menu = new CustomMouseBehaviorContextMenu(); - for (TableColumn tc : columns) { - var item = new CheckMenuItem(tc.getText()); - item.setSelected(isColumnEnabled(tc)); - menu.getItems().add(item); - item.setOnAction(e -> { - try { - if (item.isSelected()) { - Config.getInstance().getSettings().disabledRecordedModelsTableColumns.remove(tc.getText()); - boolean added = false; - for (int i = table.getColumns().size() - 1; i >= 0; i--) { - TableColumn other = table.getColumns().get(i); - if (!other.isVisible()) { - continue; - } - int idx = (int) tc.getUserData(); - LOG.debug("otherIdx {}", other.getText()); - int otherIdx = (int) other.getUserData(); - if (otherIdx < idx) { - table.getColumns().add(i + 1, tc); - added = true; - break; - } - } - if (!added) { - table.getColumns().add(0, tc); - } - } else { - Config.getInstance().getSettings().disabledRecordedModelsTableColumns.add(tc.getText()); - table.getColumns().remove(tc); - } - } catch (Exception ex) { - LOG.error("Couldn't activate column {}", tc, ex); - } - }); - } - Button src = (Button) evt.getSource(); - Point2D location = src.localToScreen(src.getTranslateX(), src.getTranslateY()); - menu.show(getTabPane().getScene().getWindow(), location.getX(), location.getY() + src.getHeight() + 5); + @Override + void setSortColumn(String column) { + Config.getInstance().getSettings().recordedModelsSortColumn = column; } - private boolean isColumnEnabled(TableColumn tc) { - return !Config.getInstance().getSettings().disabledRecordedModelsTableColumns.contains(tc.getText()); + @Override + String getSortType() { + return Config.getInstance().getSettings().recordedModelsSortType; + } + + @Override + void setSortType(String sortType) { + Config.getInstance().getSettings().recordedModelsSortType = sortType; + } + + @Override + String[] getColumnIds() { + return Config.getInstance().getSettings().recordedModelsColumnIds; + } + + @Override + void setColumnIds(String[] ids) { + Config.getInstance().getSettings().recordedModelsColumnIds = ids; + } + + @Override + double[] getColumnWidths() { + return Config.getInstance().getSettings().recordedModelsColumnWidths; + } + + @Override + void setColumnWidths(double[] widths) { + Config.getInstance().getSettings().recordedModelsColumnWidths = widths; + } + + @Override + List getDisabledColumns() { + return Config.getInstance().getSettings().recordedModelsDisabledTableColumns; } } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 2bf3aa39..2beaed21 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -64,7 +64,6 @@ public class Settings { public boolean confirmationForDangerousActions = false; public String contactsheetTimestampLook = "font=sans-serif:fontcolor=white:fontsize=60:box=1:boxcolor=black@0.5:boxborderw=5"; public boolean determineResolution = false; - public List disabledRecordedModelsTableColumns = new ArrayList<>(); public List disabledSites = new ArrayList<>(); public String downloadFilename = "${modelSanitizedName}-${localDateTime}"; public List eventHandlers = new ArrayList<>(); @@ -139,10 +138,12 @@ public class Settings { public boolean recentlyWatched = true; public double[] recordLaterColumnWidths = new double[0]; public String[] recordLaterColumnIds = new String[0]; + public List recordLaterDisabledColumns = new ArrayList<>(); public String recordLaterSortColumn = ""; public String recordLaterSortType = ""; public double[] recordedModelsColumnWidths = new double[0]; public String[] recordedModelsColumnIds = new String[0]; + public List recordedModelsDisabledTableColumns = new ArrayList<>(); public String recordedModelsSortColumn = ""; public String recordedModelsSortType = ""; public double[] recordingsColumnWidths = new double[0];