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
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.

View File

@ -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.");

View File

@ -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;

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.recorder.Recorder;
import ctbrec.ui.tabs.recorded.RecordedModelsTab.ModelName;
import javafx.scene.image.ImageView;
public class ModelNameTableCell extends IconTableCell<ModelName> {

View File

@ -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<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) {
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<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);
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<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() {
ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() {
@Override
@ -467,23 +155,8 @@ public class RecordLaterTab extends Tab implements TabSelectionListener {
}
}
private 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)) //
.afterwards(table::refresh)
.contributeToMenu(selectedModels, menu);
return menu;
}
private void stopAction(List<JavaFxModel> selectedModels) {
@Override
void stopAction(List<JavaFxModel> 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<JavaFxModel, ?> 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<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;
}
}
}
@Override
void setColumnWidths(double[] widths) {
Config.getInstance().getSettings().recordLaterColumnWidths = widths;
}
private void restoreColumnOrder() {
String[] columnIds = Config.getInstance().getSettings().recordLaterColumnIds;
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);
}
}
}
@Override
List<String> 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<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;
}
@Override
boolean getMarkModelsForLaterRecording() {
return true;
}
}

View File

@ -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<List<JavaFxModel>> updateService;
private Recorder recorder;
private List<Site> sites;
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 resumeAll = new Button("Resume All");
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) {
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<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");
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>(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<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");
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<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;
}
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<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);
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<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) {
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<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() {
ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() {
@Override
@ -704,28 +344,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
}
private 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;
}
private boolean stopAction(List<JavaFxModel> selectedModels) {
@Override
void stopAction(List<JavaFxModel> 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<JavaFxModel> 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<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>> {
@Override
public TableCell<JavaFxModel, Number> call(TableColumn<JavaFxModel, Number> 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> 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<JavaFxModel, ?> 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<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);
@Override
void setSortColumn(String column) {
Config.getInstance().getSettings().recordedModelsSortColumn = column;
}
private boolean isColumnEnabled(TableColumn<JavaFxModel, ?> 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<String> getDisabledColumns() {
return Config.getInstance().getSettings().recordedModelsDisabledTableColumns;
}
}

View File

@ -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<String> disabledRecordedModelsTableColumns = new ArrayList<>();
public List<String> disabledSites = new ArrayList<>();
public String downloadFilename = "${modelSanitizedName}-${localDateTime}";
public List<EventHandlerConfiguration> 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<String> recordLaterDisabledColumns = new ArrayList<>();
public String recordLaterSortColumn = "";
public String recordLaterSortType = "";
public double[] recordedModelsColumnWidths = new double[0];
public String[] recordedModelsColumnIds = new String[0];
public List<String> recordedModelsDisabledTableColumns = new ArrayList<>();
public String recordedModelsSortColumn = "";
public String recordedModelsSortType = "";
public double[] recordingsColumnWidths = new double[0];