463 lines
20 KiB
Java
463 lines
20 KiB
Java
package ctbrec.ui;
|
|
|
|
import java.io.IOException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.BlockingQueue;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.ThreadFactory;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.function.Function;
|
|
import java.util.stream.Collectors;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import ctbrec.Model;
|
|
import ctbrec.Recording;
|
|
import ctbrec.recorder.Recorder;
|
|
import ctbrec.sites.Site;
|
|
import ctbrec.ui.autofilltextbox.AutoFillTextField;
|
|
import javafx.application.Platform;
|
|
import javafx.collections.FXCollections;
|
|
import javafx.collections.ObservableList;
|
|
import javafx.concurrent.ScheduledService;
|
|
import javafx.concurrent.Task;
|
|
import javafx.event.ActionEvent;
|
|
import javafx.geometry.Insets;
|
|
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.MenuItem;
|
|
import javafx.scene.control.ScrollPane;
|
|
import javafx.scene.control.Tab;
|
|
import javafx.scene.control.TableColumn;
|
|
import javafx.scene.control.TableView;
|
|
import javafx.scene.control.Tooltip;
|
|
import javafx.scene.control.cell.CheckBoxTableCell;
|
|
import javafx.scene.control.cell.PropertyValueFactory;
|
|
import javafx.scene.input.Clipboard;
|
|
import javafx.scene.input.ClipboardContent;
|
|
import javafx.scene.input.ContextMenuEvent;
|
|
import javafx.scene.input.KeyCode;
|
|
import javafx.scene.input.KeyEvent;
|
|
import javafx.scene.input.MouseEvent;
|
|
import javafx.scene.layout.BorderPane;
|
|
import javafx.scene.layout.FlowPane;
|
|
import javafx.scene.layout.HBox;
|
|
import javafx.util.Duration;
|
|
|
|
public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|
private static final transient Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
|
|
|
|
static BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
|
|
static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue);
|
|
|
|
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<JavaFxModel>();
|
|
ObservableList<JavaFxModel> observableModels = FXCollections.observableArrayList();
|
|
ContextMenu popup;
|
|
|
|
Label modelLabel = new Label("Model");
|
|
AutoFillTextField model;
|
|
Button addModelButton = new Button("Record");
|
|
|
|
public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) {
|
|
super(title);
|
|
this.recorder = recorder;
|
|
this.sites = sites;
|
|
createGui();
|
|
setClosable(false);
|
|
initializeUpdateService();
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
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));
|
|
|
|
table.setEditable(false);
|
|
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
|
|
name.setPrefWidth(200);
|
|
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("name"));
|
|
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
|
|
url.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("url"));
|
|
url.setPrefWidth(400);
|
|
TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online");
|
|
online.setCellValueFactory((cdf) -> cdf.getValue().getOnlineProperty());
|
|
online.setCellFactory(CheckBoxTableCell.forTableColumn(online));
|
|
online.setPrefWidth(100);
|
|
TableColumn<JavaFxModel, Boolean> recording = new TableColumn<>("Recording");
|
|
recording.setCellValueFactory((cdf) -> cdf.getValue().getRecordingProperty());
|
|
recording.setCellFactory(CheckBoxTableCell.forTableColumn(recording));
|
|
recording.setPrefWidth(100);
|
|
TableColumn<JavaFxModel, Boolean> paused = new TableColumn<>("Paused");
|
|
paused.setCellValueFactory((cdf) -> cdf.getValue().getPausedProperty());
|
|
paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused));
|
|
paused.setPrefWidth(100);
|
|
table.getColumns().addAll(name, url, online, recording, paused);
|
|
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.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
|
if(event.getCode() == KeyCode.DELETE) {
|
|
stopAction();
|
|
}
|
|
});
|
|
scrollPane.setContent(table);
|
|
|
|
HBox addModelBox = new HBox(5);
|
|
modelLabel.setPadding(new Insets(5, 0, 0, 0));
|
|
ObservableList<String> suggestions = FXCollections.observableArrayList();
|
|
sites.forEach(site -> suggestions.add(site.getName()));
|
|
model = new AutoFillTextField(suggestions);
|
|
model.setPrefWidth(300);
|
|
model.setPromptText("e.g. MyFreeCams:ModelName");
|
|
model.onActionHandler(e -> addModel(e));
|
|
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((e) -> addModel(e));
|
|
addModelBox.getChildren().addAll(modelLabel, model, addModelButton);
|
|
|
|
BorderPane root = new BorderPane();
|
|
root.setPadding(new Insets(5));
|
|
root.setTop(addModelBox);
|
|
root.setCenter(scrollPane);
|
|
setContent(root);
|
|
}
|
|
|
|
private void addModel(ActionEvent e) {
|
|
String[] parts = model.getText().trim().split(":");
|
|
if (parts.length != 2) {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
|
alert.setTitle("Wrong format");
|
|
alert.setHeaderText("Couldn't add model");
|
|
alert.setContentText("Use something like \"MyFreeCams:ModelName\"");
|
|
alert.showAndWait();
|
|
return;
|
|
}
|
|
|
|
String siteName = parts[0];
|
|
String modelName = parts[1];
|
|
for (Site site : sites) {
|
|
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
|
|
try {
|
|
Model m = site.createModel(modelName);
|
|
recorder.startRecording(m);
|
|
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
|
alert.setTitle("Error");
|
|
alert.setHeaderText("Couldn't add model");
|
|
alert.setContentText("The model " + modelName + " could not be added: " + e1.getLocalizedMessage());
|
|
alert.showAndWait();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
|
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)));
|
|
updateService.setOnSucceeded((event) -> {
|
|
List<JavaFxModel> models = updateService.getValue();
|
|
if(models == null) {
|
|
return;
|
|
}
|
|
|
|
for (JavaFxModel updatedModel : models) {
|
|
int index = observableModels.indexOf(updatedModel);
|
|
if (index == -1) {
|
|
observableModels.add(updatedModel);
|
|
} else {
|
|
// make sure to update the JavaFX online property, so that the table cell is updated
|
|
JavaFxModel oldModel = observableModels.get(index);
|
|
oldModel.setSuspended(updatedModel.isSuspended());
|
|
oldModel.getOnlineProperty().set(updatedModel.getOnlineProperty().get());
|
|
oldModel.getRecordingProperty().set(updatedModel.getRecordingProperty().get());
|
|
}
|
|
}
|
|
|
|
for (Iterator<JavaFxModel> iterator = observableModels.iterator(); iterator.hasNext();) {
|
|
Model model = iterator.next();
|
|
if (!models.contains(model)) {
|
|
iterator.remove();
|
|
}
|
|
}
|
|
});
|
|
updateService.setOnFailed((event) -> {
|
|
LOG.info("Couldn't get list of models from recorder", event.getSource().getException());
|
|
});
|
|
}
|
|
|
|
private ScheduledService<List<JavaFxModel>> createUpdateService() {
|
|
ScheduledService<List<JavaFxModel>> updateService = new ScheduledService<List<JavaFxModel>>() {
|
|
@Override
|
|
protected Task<List<JavaFxModel>> createTask() {
|
|
return new Task<List<JavaFxModel>>() {
|
|
@Override
|
|
public List<JavaFxModel> call() throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException, IOException {
|
|
LOG.trace("Updating recorded models");
|
|
List<Recording> recordings = recorder.getRecordings();
|
|
List<Model> onlineModels = recorder.getOnlineModels();
|
|
return recorder.getModelsRecording()
|
|
.stream()
|
|
.map(m -> new JavaFxModel(m))
|
|
.peek(fxm -> {
|
|
for (Recording recording : recordings) {
|
|
if(recording.getStatus() == Recording.STATUS.RECORDING &&
|
|
recording.getModelName().equals(fxm.getName()))
|
|
{
|
|
fxm.getRecordingProperty().set(true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (Model onlineModel : onlineModels) {
|
|
if(Objects.equals(onlineModel, fxm)) {
|
|
fxm.getOnlineProperty().set(true);
|
|
break;
|
|
}
|
|
}
|
|
})
|
|
.collect(Collectors.toList());
|
|
}
|
|
};
|
|
}
|
|
};
|
|
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
|
@Override
|
|
public Thread newThread(Runnable r) {
|
|
Thread t = new Thread(r);
|
|
t.setDaemon(true);
|
|
t.setName("RecordedModelsTab UpdateService");
|
|
return t;
|
|
}
|
|
});
|
|
updateService.setExecutor(executor);
|
|
return updateService;
|
|
}
|
|
|
|
@Override
|
|
public void selected() {
|
|
if (updateService != null) {
|
|
updateService.reset();
|
|
updateService.restart();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void deselected() {
|
|
if (updateService != null) {
|
|
updateService.cancel();
|
|
}
|
|
}
|
|
|
|
private ContextMenu createContextMenu() {
|
|
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
|
|
if(selectedModel == null) {
|
|
return null;
|
|
}
|
|
MenuItem stop = new MenuItem("Remove Model");
|
|
stop.setOnAction((e) -> stopAction());
|
|
|
|
MenuItem copyUrl = new MenuItem("Copy URL");
|
|
copyUrl.setOnAction((e) -> {
|
|
Model selected = selectedModel;
|
|
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
|
final ClipboardContent content = new ClipboardContent();
|
|
content.putString(selected.getUrl());
|
|
clipboard.setContent(content);
|
|
});
|
|
|
|
MenuItem pauseRecording = new MenuItem("Pause Recording");
|
|
pauseRecording.setOnAction((e) -> pauseRecording());
|
|
MenuItem resumeRecording = new MenuItem("Resume Recording");
|
|
resumeRecording.setOnAction((e) -> resumeRecording());
|
|
MenuItem openInBrowser = new MenuItem("Open in Browser");
|
|
openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModel.getUrl()));
|
|
MenuItem openInPlayer = new MenuItem("Open in Player");
|
|
openInPlayer.setOnAction((e) -> openInPlayer(selectedModel));
|
|
MenuItem switchStreamSource = new MenuItem("Switch resolution");
|
|
switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModel));
|
|
|
|
ContextMenu menu = new ContextMenu(stop);
|
|
menu.getItems().add(selectedModel.isSuspended() ? resumeRecording : pauseRecording);
|
|
menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource);
|
|
return menu;
|
|
}
|
|
|
|
private void openInPlayer(JavaFxModel selectedModel) {
|
|
table.setCursor(Cursor.WAIT);
|
|
new Thread(() -> {
|
|
Player.play(selectedModel);
|
|
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
|
|
}).start();
|
|
}
|
|
|
|
private void switchStreamSource(JavaFxModel fxModel) {
|
|
try {
|
|
if(!fxModel.isOnline()) {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
|
|
alert.setTitle("Switch resolution");
|
|
alert.setHeaderText("Couldn't switch stream resolution");
|
|
alert.setContentText("The resolution can only be changed, when the model is online");
|
|
alert.showAndWait();
|
|
return;
|
|
}
|
|
} catch (IOException | ExecutionException | InterruptedException e1) {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
|
|
alert.setTitle("Switch resolution");
|
|
alert.setHeaderText("Couldn't switch stream resolution");
|
|
alert.setContentText("An error occured while checking, if the model is online");
|
|
alert.showAndWait();
|
|
return;
|
|
}
|
|
|
|
Function<Model, Void> onSuccess = (m) -> {
|
|
try {
|
|
recorder.switchStreamSource(m);
|
|
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
|
|
LOG.error("Error while switching stream resolution", e);
|
|
showStreamSwitchErrorDialog(e);
|
|
}
|
|
return null;
|
|
};
|
|
Function<Throwable, Void> onFail = (t) -> {
|
|
LOG.error("Error while switching stream resolution", t);
|
|
showStreamSwitchErrorDialog(t);
|
|
return null;
|
|
};
|
|
StreamSourceSelectionDialog.show(fxModel.getDelegate(), onSuccess, onFail);
|
|
}
|
|
|
|
private void showStreamSwitchErrorDialog(Throwable throwable) {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
|
alert.setTitle("Error");
|
|
alert.setHeaderText("Couldn't switch stream resolution");
|
|
alert.setContentText("Error while switching stream resolution: " + throwable.getLocalizedMessage());
|
|
alert.showAndWait();
|
|
}
|
|
|
|
private void stopAction() {
|
|
Model selected = table.getSelectionModel().getSelectedItem().getDelegate();
|
|
if (selected != null) {
|
|
table.setCursor(Cursor.WAIT);
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
recorder.stopRecording(selected);
|
|
observableModels.remove(selected);
|
|
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
|
LOG.error("Couldn't stop recording", e1);
|
|
Platform.runLater(() -> {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
|
alert.setTitle("Error");
|
|
alert.setHeaderText("Couldn't stop recording");
|
|
alert.setContentText("Error while stopping the recording: " + e1.getLocalizedMessage());
|
|
alert.showAndWait();
|
|
});
|
|
} finally {
|
|
table.setCursor(Cursor.DEFAULT);
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
};
|
|
|
|
private void pauseRecording() {
|
|
JavaFxModel model = table.getSelectionModel().getSelectedItem();
|
|
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
|
|
if (delegate != null) {
|
|
table.setCursor(Cursor.WAIT);
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
recorder.suspendRecording(delegate);
|
|
Platform.runLater(() -> model.setSuspended(true));
|
|
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
|
LOG.error("Couldn't pause recording", e1);
|
|
Platform.runLater(() -> {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
|
alert.setTitle("Error");
|
|
alert.setHeaderText("Couldn't pause recording");
|
|
alert.setContentText("Error while pausing the recording: " + e1.getLocalizedMessage());
|
|
alert.showAndWait();
|
|
});
|
|
} finally {
|
|
table.setCursor(Cursor.DEFAULT);
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
};
|
|
|
|
private void resumeRecording() {
|
|
JavaFxModel model = table.getSelectionModel().getSelectedItem();
|
|
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
|
|
if (delegate != null) {
|
|
table.setCursor(Cursor.WAIT);
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
recorder.resumeRecording(delegate);
|
|
Platform.runLater(() -> model.setSuspended(false));
|
|
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
|
LOG.error("Couldn't resume recording", e1);
|
|
Platform.runLater(() -> {
|
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
|
alert.setTitle("Error");
|
|
alert.setHeaderText("Couldn't resume recording");
|
|
alert.setContentText("Error while resuming the recording: " + e1.getLocalizedMessage());
|
|
alert.showAndWait();
|
|
});
|
|
} finally {
|
|
table.setCursor(Cursor.DEFAULT);
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
};
|
|
}
|