Introduce common base class for recorded models tabs

This commit is contained in:
0xb00bface 2021-08-16 19:25:43 +02:00
parent 82c51bab40
commit 016b5dc7f1
9 changed files with 748 additions and 970 deletions

View File

@ -2,6 +2,7 @@
======================== ========================
* Added portrait column to Recording tab. The image to show can be selected in * 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. 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 * 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 get stuck in recording state. Recordings will stop now, if now segment was
downloaded for 30 seconds. downloaded for 30 seconds.

View File

@ -5,6 +5,7 @@ import java.awt.Image;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Arrays; import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
@ -42,7 +43,8 @@ public class SetPortraitAction {
public void execute() { public void execute() {
source.setCursor(Cursor.WAIT); 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(); GridPane pane = new GridPane();
Label l = new Label("Select a portrait image. Leave empty to remove a portrait again."); Label l = new Label("Select a portrait image. Leave empty to remove a portrait again.");

View File

@ -56,7 +56,7 @@ import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.Toast; import ctbrec.ui.controls.Toast;
import ctbrec.ui.menu.ModelMenuContributor; import ctbrec.ui.menu.ModelMenuContributor;
import ctbrec.ui.tabs.recorded.RecordedModelsTab.ModelName; import ctbrec.ui.tabs.recorded.ModelName;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;

View File

@ -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<JavaFxModel> observableModels = FXCollections.observableArrayList();
protected ObservableList<JavaFxModel> filteredModels = FXCollections.observableArrayList();
protected TableView<JavaFxModel> table = new TableView<>();
protected List<TableColumn<JavaFxModel, ?>> columns = new ArrayList<>();
protected LoadingCache<Model, Image> portraitCache = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.DAYS)
.maximumSize(1000)
.build(CacheLoader.from(AbstractRecordedModelsTab::loadModelPortrait));
protected AutoFillTextField modelInputField;
protected List<Site> 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<JavaFxModel> 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<JavaFxModel> 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<String> 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<JavaFxModel, String> 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<JavaFxModel, Image> 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>(image);
});
portrait.setCellFactory(param -> new ImageTableCell());
portrait.setEditable(false);
}
protected void addModelColumn(int columnIdx) {
TableColumn<JavaFxModel, ModelName> 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<JavaFxModel, String> url = addTableColumn("url", "URL", columnIdx, 400);
url.setCellValueFactory(new PropertyValueFactory<>("url"));
url.setCellFactory(new ClickableCellFactory<>());
url.setEditable(false);
}
protected void addNotesColumn(int columnIdx) {
TableColumn<JavaFxModel, String> 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<JavaFxModel> selectedModels);
protected <T> TableColumn<JavaFxModel, T> addTableColumn(String id, String text, int index, int width) {
TableColumn<JavaFxModel, T> 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<JavaFxModel, ?> tc) {
if(isColumnEnabled(tc)) {
table.getColumns().add(tc);
}
}
protected ContextMenu createContextMenu() {
List<Model> 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<JavaFxModel, ?> 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<String> getDisabledColumns();
public void saveState() {
if (!table.getSortOrder().isEmpty()) {
TableColumn<JavaFxModel, ?> 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<JavaFxModel, ?> 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<TableColumn<JavaFxModel,?>> 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<JavaFxModel, ?> 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<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
@Override
public TableCell<S, T> call(TableColumn<S, T> param) {
TableCell<S, T> 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<JavaFxModel, ?> 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<JavaFxModel, ?> 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<JavaFxModel, ?> tc) {
return !getDisabledColumns().contains(tc.getText());
}
}

View File

@ -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> modelGroup = rec.getModelGroup(mdl);
String s;
if (modelGroup.isPresent()) {
s = modelGroup.get().getName() + " (aka " + mdl.getDisplayName() + ')';
} else {
return mdl.toString();
}
return s;
}
}

View File

@ -7,7 +7,6 @@ import java.util.stream.Collectors;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.tabs.recorded.RecordedModelsTab.ModelName;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
public class ModelNameTableCell extends IconTableCell<ModelName> { public class ModelNameTableCell extends IconTableCell<ModelName> {

View File

@ -5,11 +5,9 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -17,83 +15,25 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.JavaFxModel; import ctbrec.ui.JavaFxModel;
import ctbrec.ui.PreviewPopupHandler;
import ctbrec.ui.action.CheckModelAccountAction; import ctbrec.ui.action.CheckModelAccountAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.Dialogs; 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.TabSelectionListener;
import ctbrec.ui.tabs.recorded.RecordedModelsTab.ModelName;
import javafx.application.Platform; 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.ScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent; import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.geometry.Insets; 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.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.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 static final Logger LOG = LoggerFactory.getLogger(RecordLaterTab.class);
private ReentrantLock lock = new ReentrantLock();
private ScheduledService<List<JavaFxModel>> updateService; private ScheduledService<List<JavaFxModel>> updateService;
private Recorder recorder;
private List<Site> sites;
FlowPane grid = new FlowPane();
ScrollPane scrollPane = new ScrollPane();
TableView<JavaFxModel> table = new TableView<>();
ObservableList<JavaFxModel> observableModels = FXCollections.observableArrayList();
ObservableList<JavaFxModel> 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<Site> sites) { public RecordLaterTab(String title, Recorder recorder, List<Site> sites) {
super(title); super(title);
@ -104,141 +44,16 @@ public class RecordLaterTab extends Tab implements TabSelectionListener {
initializeUpdateService(); initializeUpdateService();
} }
@SuppressWarnings("unchecked") @Override
private void createGui() { protected void createGui() {
grid.setPadding(new Insets(5)); super.createGui();
grid.setHgap(5);
grid.setVgap(5);
scrollPane.setContent(grid); int columnIdx = 0;
scrollPane.setFitToHeight(true); addPreviewColumn(columnIdx++);
scrollPane.setFitToWidth(true); addPortraitColumn(columnIdx++);
BorderPane.setMargin(scrollPane, new Insets(5)); addModelColumn(columnIdx++);
addUrlColumn(columnIdx++);
table.setEditable(true); addNotesColumn(columnIdx);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
var previewPopupHandler = new PreviewPopupHandler(table);
table.setRowFactory(tableview -> {
TableRow<JavaFxModel> row = new TableRow<>();
row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
return row;
});
TableColumn<JavaFxModel, String> 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<JavaFxModel, ModelName> 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<JavaFxModel, String> url = new TableColumn<>("URL");
url.setCellValueFactory(new PropertyValueFactory<>("url"));
url.setCellFactory(new ClickableCellFactory<>());
url.setPrefWidth(400);
url.setEditable(false);
url.setId("url");
TableColumn<JavaFxModel, String> 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<JavaFxModel> 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<String> 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);
var root = new BorderPane(); var root = new BorderPane();
root.setPadding(new Insets(5)); root.setPadding(new Insets(5));
@ -246,98 +61,12 @@ public class RecordLaterTab extends Tab implements TabSelectionListener {
root.setCenter(scrollPane); root.setCenter(scrollPane);
setContent(root); setContent(root);
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
.execute(Model::isMarkedForLaterRecording));
restoreState(); 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() { void initializeUpdateService() {
updateService = createUpdateService(); updateService = createUpdateService();
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2))); 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<JavaFxModel, ?> 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<List<JavaFxModel>> createUpdateService() { private ScheduledService<List<JavaFxModel>> createUpdateService() {
ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() { ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() {
@Override @Override
@ -467,23 +155,8 @@ public class RecordLaterTab extends Tab implements TabSelectionListener {
} }
} }
private ContextMenu createContextMenu() { @Override
List<Model> selectedModels = table.getSelectionModel().getSelectedItems().stream().map(JavaFxModel::getDelegate).collect(Collectors.toList()); void stopAction(List<JavaFxModel> selectedModels) {
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<JavaFxModel> selectedModels) {
var confirmed = true; var confirmed = true;
if (Config.getInstance().getSettings().confirmationForDangerousActions) { if (Config.getInstance().getSettings().confirmationForDangerousActions) {
int n = selectedModels.size(); int n = selectedModels.size();
@ -500,85 +173,53 @@ public class RecordLaterTab extends Tab implements TabSelectionListener {
} }
} }
public void saveState() { @Override
if (!table.getSortOrder().isEmpty()) { String getSortColumn() {
TableColumn<JavaFxModel, ?> col = table.getSortOrder().get(0); return Config.getInstance().getSettings().recordLaterSortColumn;
Config.getInstance().getSettings().recordLaterSortColumn = col.getText(); }
Config.getInstance().getSettings().recordLaterSortType = col.getSortType().toString();
} @Override
int columns = table.getColumns().size(); void setSortColumn(String column) {
var columnWidths = new double[columns]; Config.getInstance().getSettings().recordLaterSortColumn = column;
var columnIds = new String[columns]; }
for (var i = 0; i < columnWidths.length; i++) {
columnWidths[i] = table.getColumns().get(i).getWidth(); @Override
columnIds[i] = table.getColumns().get(i).getId(); String getSortType() {
} return Config.getInstance().getSettings().recordLaterSortType;
Config.getInstance().getSettings().recordLaterColumnWidths = columnWidths; }
@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; Config.getInstance().getSettings().recordLaterColumnIds = columnIds;
} }
private void restoreState() { @Override
restoreColumnOrder(); double[] getColumnWidths() {
restoreColumnWidths(); return Config.getInstance().getSettings().recordLaterColumnWidths;
restoreSorting();
} }
private void restoreSorting() { @Override
String sortCol = Config.getInstance().getSettings().recordLaterSortColumn; void setColumnWidths(double[] widths) {
if (StringUtil.isNotBlank(sortCol)) { Config.getInstance().getSettings().recordLaterColumnWidths = widths;
for (TableColumn<JavaFxModel, ?> 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;
}
}
}
} }
private void restoreColumnOrder() { @Override
String[] columnIds = Config.getInstance().getSettings().recordLaterColumnIds; List<String> getDisabledColumns() {
ObservableList<TableColumn<JavaFxModel,?>> columns = table.getColumns(); return Config.getInstance().getSettings().recordLaterDisabledColumns;
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<JavaFxModel, ?> col = columns.get(j);
columns.remove(j); // NOSONAR
columns.add(i, col);
}
}
}
} }
private void restoreColumnWidths() { @Override
double[] columnWidths = Config.getInstance().getSettings().recordLaterColumnWidths; boolean getMarkModelsForLaterRecording() {
if (columnWidths != null && columnWidths.length == table.getColumns().size()) { return true;
for (var i = 0; i < columnWidths.length; i++) {
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
}
}
}
private class ClickableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
@Override
public TableCell<S, T> call(TableColumn<S, T> param) {
TableCell<S, T> 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;
}
} }
} }

View File

@ -2,141 +2,70 @@ package ctbrec.ui.tabs.recorded;
import static ctbrec.Recording.State.*; import static ctbrec.Recording.State.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.StringUtil;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.JavaFxModel; import ctbrec.ui.JavaFxModel;
import ctbrec.ui.PreviewPopupHandler;
import ctbrec.ui.action.CheckModelAccountAction; import ctbrec.ui.action.CheckModelAccountAction;
import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.ResumeAction; import ctbrec.ui.action.ResumeAction;
import ctbrec.ui.action.SetPortraitAction;
import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.action.ToggleRecordingAction; import ctbrec.ui.action.ToggleRecordingAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.DateTimeCellFactory; import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.Dialogs; 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.TabSelectionListener;
import javafx.application.Platform; 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.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent; import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.geometry.Insets; 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.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.TableCell;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent; 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.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell; 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.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.util.Callback; import javafx.util.Callback;
import javafx.util.Duration; import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import javafx.util.converter.NumberStringConverter; 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 Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
private static final String STYLE_ALIGN_CENTER = "-fx-alignment: CENTER;"; private static final String STYLE_ALIGN_CENTER = "-fx-alignment: CENTER;";
private ReentrantLock lock = new ReentrantLock();
private ScheduledService<List<JavaFxModel>> updateService; private ScheduledService<List<JavaFxModel>> updateService;
private Recorder recorder;
private List<Site> sites;
private volatile boolean cellEditing = false; private volatile boolean cellEditing = false;
FlowPane grid = new FlowPane();
ScrollPane scrollPane = new ScrollPane();
TableView<JavaFxModel> table = new TableView<>();
private List<TableColumn<JavaFxModel, ?>> columns = new ArrayList<>();
ObservableList<JavaFxModel> observableModels = FXCollections.observableArrayList();
ObservableList<JavaFxModel> filteredModels = FXCollections.observableArrayList();
ContextMenu popup;
Label modelLabel = new Label("Model");
AutoFillTextField model;
Button addModelButton = new Button("Record");
Button pauseAll = new Button("Pause All"); Button pauseAll = new Button("Pause All");
Button resumeAll = new Button("Resume All"); Button resumeAll = new Button("Resume All");
ToggleButton toggleRecording = new ToggleButton("Pause Recording"); ToggleButton toggleRecording = new ToggleButton("Pause Recording");
Button checkModelAccountExistance = new Button("Check URLs");
TextField filter;
LoadingCache<Model, Image> portraitCache = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.DAYS)
.maximumSize(1000)
.build(CacheLoader.from(RecordedModelsTab::loadModelPortrait));
public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) { public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) {
super(title); super(title);
@ -147,77 +76,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
initializeUpdateService(); initializeUpdateService();
} }
private void createGui() { @Override
grid.setPadding(new Insets(5)); protected void createGui() {
grid.setHgap(5); super.createGui();
grid.setVgap(5);
scrollPane.setContent(grid);
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
BorderPane.setMargin(scrollPane, new Insets(5));
int idx = 0; int idx = 0;
table.setEditable(true);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
var previewPopupHandler = new PreviewPopupHandler(table);
table.setRowFactory(tableview -> {
TableRow<JavaFxModel> row = new TableRow<>();
row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
return row;
});
TableColumn<JavaFxModel, String> 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<JavaFxModel, Image> portrait = new TableColumn<>("Portrait"); addPreviewColumn(idx++);
portrait.setPrefWidth(80); addPortraitColumn(idx++);
portrait.setCellValueFactory(param -> { addModelColumn(idx++);
Model mdl = param.getValue().getDelegate(); addUrlColumn(idx++);
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>(image);
});
portrait.setCellFactory(param -> new ImageTableCell());
portrait.setEditable(false);
portrait.setId("portrait");
portrait.setUserData(idx++);
columns.add(portrait);
addTableColumnIfEnabled(portrait);
TableColumn<JavaFxModel, ModelName> 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<JavaFxModel, String> 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<JavaFxModel, Boolean> online = new TableColumn<>("Online"); TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online");
online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty()); online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty());
online.setCellFactory(param -> new OnlineTableCell()); online.setCellFactory(param -> new OnlineTableCell());
@ -277,73 +146,12 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
lastRecorded.setUserData(idx++); lastRecorded.setUserData(idx++);
columns.add(lastRecorded); columns.add(lastRecorded);
addTableColumnIfEnabled(lastRecorded); addTableColumnIfEnabled(lastRecorded);
TableColumn<JavaFxModel, String> notes = new TableColumn<>("Notes");
notes.setCellValueFactory(cdf -> {
JavaFxModel m = cdf.getValue();
return new StringPropertyBase() {
@Override
public String getName() {
return "Model Notes";
}
@Override addNotesColumn(idx);
public Object getBean() {
return null;
}
@Override addModelBox.getChildren().add(3, pauseAll);
public String get() { addModelBox.getChildren().add(4, resumeAll);
String modelNotes = Config.getInstance().getModelNotes(m); addModelBox.getChildren().add(5, toggleRecording);
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<JavaFxModel> 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<String> 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);
HBox.setMargin(pauseAll, new Insets(0, 0, 0, 20)); HBox.setMargin(pauseAll, new Insets(0, 0, 0, 20));
pauseAll.setOnAction(this::pauseAll); pauseAll.setOnAction(this::pauseAll);
resumeAll.setOnAction(this::resumeAll); resumeAll.setOnAction(this::resumeAll);
@ -352,44 +160,10 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
toggleRecording.setPadding(new Insets(5)); toggleRecording.setPadding(new Insets(5));
toggleRecording.setOnAction(this::toggleRecording); toggleRecording.setOnAction(this::toggleRecording);
HBox.setMargin(toggleRecording, new Insets(0, 0, 0, 20)); 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 checkModelAccountExistance
.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder).execute(Predicate.not(Model::isMarkedForLaterRecording))); .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(); var root = new BorderPane();
root.setPadding(new Insets(5)); root.setPadding(new Insets(5));
root.setTop(addModelBox); root.setTop(addModelBox);
@ -399,41 +173,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
restoreState(); restoreState();
} }
private void addTableColumnIfEnabled(TableColumn<JavaFxModel, ?> 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<JavaFxModel, Number> evt) { private void onUpdatePriority(CellEditEvent<JavaFxModel, Number> evt) {
try { try {
int prio = Optional.ofNullable(evt.getNewValue()).map(Number::intValue).orElse(-1); 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) { private void pauseAll(ActionEvent evt) {
boolean yes = Dialogs.showConfirmDialog("Pause all models", "", "Pause the recording of all models?", getTabPane().getScene()); boolean yes = Dialogs.showConfirmDialog("Pause all models", "", "Pause the recording of all models?", getTabPane().getScene());
if (yes) { 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<JavaFxModel, ?> 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<List<JavaFxModel>> createUpdateService() { private ScheduledService<List<JavaFxModel>> createUpdateService() {
ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() { ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() {
@Override @Override
@ -704,28 +344,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} }
} }
private ContextMenu createContextMenu() { @Override
List<Model> selectedModels = table.getSelectionModel().getSelectedItems().stream().map(JavaFxModel::getDelegate).collect(Collectors.toList()); void stopAction(List<JavaFxModel> selectedModels) {
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<JavaFxModel> selectedModels) {
var confirmed = true; var confirmed = true;
if (Config.getInstance().getSettings().confirmationForDangerousActions) { if (Config.getInstance().getSettings().confirmationForDangerousActions) {
int n = selectedModels.size(); int n = selectedModels.size();
@ -740,7 +360,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
table.getItems().remove(m); table.getItems().remove(m);
})); }));
} }
return confirmed;
} }
private void pauseRecording(List<JavaFxModel> selectedModels) { private void pauseRecording(List<JavaFxModel> selectedModels) {
@ -753,88 +372,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
new ResumeAction(getTabPane(), models, recorder).execute(); new ResumeAction(getTabPane(), models, recorder).execute();
} }
public void saveState() {
if (!table.getSortOrder().isEmpty()) {
TableColumn<JavaFxModel, ?> 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<JavaFxModel, ?> 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<TableColumn<JavaFxModel, ?>> 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<JavaFxModel, ?> 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<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
@Override
public TableCell<S, T> call(TableColumn<S, T> param) {
TableCell<S, T> 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<TableColumn<JavaFxModel, Number>, TableCell<JavaFxModel, Number>> { private class PriorityCellFactory implements Callback<TableColumn<JavaFxModel, Number>, TableCell<JavaFxModel, Number>> {
@Override @Override
public TableCell<JavaFxModel, Number> call(TableColumn<JavaFxModel, Number> param) { public TableCell<JavaFxModel, Number> call(TableColumn<JavaFxModel, Number> param) {
@ -862,86 +399,53 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} }
} }
public static class ModelName { @Override
private Model mdl; boolean getMarkModelsForLaterRecording() {
private Recorder rec; return false;
public ModelName(Model model, Recorder recorder) {
mdl = model;
rec = recorder;
}
@Override
public String toString() {
Optional<ModelGroup> modelGroup = rec.getModelGroup(mdl);
String s;
if (modelGroup.isPresent()) {
s = modelGroup.get().getName() + " (aka " + mdl.getDisplayName() + ')';
} else {
return mdl.toString();
}
return s;
}
} }
private static Image loadModelPortrait(Model model) { @Override
String portraitId = Config.getInstance().getSettings().modelPortraits.get(model.getUrl()); String getSortColumn() {
if (StringUtil.isNotBlank(portraitId)) { return Config.getInstance().getSettings().recordedModelsSortColumn;
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"));
} }
private void showColumnSelection(ActionEvent evt) { @Override
ContextMenu menu = new CustomMouseBehaviorContextMenu(); void setSortColumn(String column) {
for (TableColumn<JavaFxModel, ?> tc : columns) { Config.getInstance().getSettings().recordedModelsSortColumn = column;
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<JavaFxModel, ?> 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);
} }
private boolean isColumnEnabled(TableColumn<JavaFxModel, ?> tc) { @Override
return !Config.getInstance().getSettings().disabledRecordedModelsTableColumns.contains(tc.getText()); 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<String> getDisabledColumns() {
return Config.getInstance().getSettings().recordedModelsDisabledTableColumns;
} }
} }

View File

@ -64,7 +64,6 @@ public class Settings {
public boolean confirmationForDangerousActions = false; public boolean confirmationForDangerousActions = false;
public String contactsheetTimestampLook = "font=sans-serif:fontcolor=white:fontsize=60:box=1:boxcolor=black@0.5:boxborderw=5"; public String contactsheetTimestampLook = "font=sans-serif:fontcolor=white:fontsize=60:box=1:boxcolor=black@0.5:boxborderw=5";
public boolean determineResolution = false; public boolean determineResolution = false;
public List<String> disabledRecordedModelsTableColumns = new ArrayList<>();
public List<String> disabledSites = new ArrayList<>(); public List<String> disabledSites = new ArrayList<>();
public String downloadFilename = "${modelSanitizedName}-${localDateTime}"; public String downloadFilename = "${modelSanitizedName}-${localDateTime}";
public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>(); public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>();
@ -139,10 +138,12 @@ public class Settings {
public boolean recentlyWatched = true; public boolean recentlyWatched = true;
public double[] recordLaterColumnWidths = new double[0]; public double[] recordLaterColumnWidths = new double[0];
public String[] recordLaterColumnIds = new String[0]; public String[] recordLaterColumnIds = new String[0];
public List<String> recordLaterDisabledColumns = new ArrayList<>();
public String recordLaterSortColumn = ""; public String recordLaterSortColumn = "";
public String recordLaterSortType = ""; public String recordLaterSortType = "";
public double[] recordedModelsColumnWidths = new double[0]; public double[] recordedModelsColumnWidths = new double[0];
public String[] recordedModelsColumnIds = new String[0]; public String[] recordedModelsColumnIds = new String[0];
public List<String> recordedModelsDisabledTableColumns = new ArrayList<>();
public String recordedModelsSortColumn = ""; public String recordedModelsSortColumn = "";
public String recordedModelsSortType = ""; public String recordedModelsSortType = "";
public double[] recordingsColumnWidths = new double[0]; public double[] recordingsColumnWidths = new double[0];