Improve UI features for time limited recordings

This commit is contained in:
0xb00bface 2020-12-21 18:53:34 +01:00
parent 8fe48f91b7
commit 97715aecc5
6 changed files with 133 additions and 26 deletions

View File

@ -22,13 +22,15 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/** /**
* Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly * Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly
*/ */
public class JavaFxModel implements Model { public class JavaFxModel implements Model {
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty(); private transient StringProperty onlineProperty = new SimpleStringProperty();
private transient BooleanProperty recordingProperty = new SimpleBooleanProperty(); private transient StringProperty recordingProperty = new SimpleStringProperty();
private transient BooleanProperty pausedProperty = new SimpleBooleanProperty(); private transient BooleanProperty pausedProperty = new SimpleBooleanProperty();
private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty(); private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty();
private transient SimpleObjectProperty<Instant> lastSeenProperty = new SimpleObjectProperty<>(); private transient SimpleObjectProperty<Instant> lastSeenProperty = new SimpleObjectProperty<>();
@ -103,11 +105,11 @@ public class JavaFxModel implements Model {
return delegate.toString(); return delegate.toString();
} }
public BooleanProperty getOnlineProperty() { public StringProperty getOnlineProperty() {
return onlineProperty; return onlineProperty;
} }
public BooleanProperty getRecordingProperty() { public StringProperty getRecordingProperty() {
return recordingProperty; return recordingProperty;
} }
@ -312,4 +314,9 @@ public class JavaFxModel implements Model {
public boolean exists() throws IOException { public boolean exists() throws IOException {
return delegate.exists(); return delegate.exists();
} }
@Override
public boolean isRecordingTimeLimited() {
return delegate.isRecordingTimeLimited();
}
} }

View File

@ -0,0 +1,8 @@
package ctbrec.ui;
public class UnicodeEmoji {
public static final String HEAVY_CHECK_MARK = "";
public static final String CLOCK = "🕒";
}

View File

@ -0,0 +1,41 @@
package ctbrec.ui.action;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.scene.Cursor;
import javafx.scene.Node;
public class RemoveTimeLimitAction {
private Model selectedModel;
private Node source;
private Recorder recorder;
public RemoveTimeLimitAction(Node source, Model selectedModel, Recorder recorder) {
this.source = source;
this.selectedModel = selectedModel;
this.recorder = recorder;
}
public CompletableFuture<Boolean> execute() {
source.setCursor(Cursor.WAIT);
Instant unlimited = Instant.ofEpochMilli(Model.RECORD_INDEFINITELY);
return CompletableFuture.supplyAsync(() -> {
try {
selectedModel.setRecordUntil(unlimited);
recorder.stopRecordingAt(selectedModel);
return true;
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Dialogs.showError(source.getScene(), "Error", "Couln't remove stop date", e);
return false;
}
}).whenComplete((r,e) -> source.setCursor(Cursor.DEFAULT));
}
}

View File

@ -2,6 +2,7 @@ package ctbrec.ui.tabs;
import static ctbrec.Recording.State.*; import static ctbrec.Recording.State.*;
import static ctbrec.SubsequentAction.*; import static ctbrec.SubsequentAction.*;
import static ctbrec.ui.UnicodeEmoji.*;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
@ -9,6 +10,9 @@ import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
@ -44,6 +48,7 @@ import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.OpenRecordingsDir; import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.RemoveTimeLimitAction;
import ctbrec.ui.action.ResumeAction; import ctbrec.ui.action.ResumeAction;
import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.action.ToggleRecordingAction; import ctbrec.ui.action.ToggleRecordingAction;
@ -107,6 +112,8 @@ import javafx.util.converter.NumberStringConverter;
public class RecordedModelsTab extends Tab implements TabSelectionListener { public class RecordedModelsTab extends Tab 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 ReentrantLock lock = new ReentrantLock(); private ReentrantLock lock = new ReentrantLock();
private ScheduledService<List<JavaFxModel>> updateService; private ScheduledService<List<JavaFxModel>> updateService;
private Recorder recorder; private Recorder recorder;
@ -168,28 +175,29 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} }
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model"); TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
name.setPrefWidth(200); name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("displayName")); name.setCellValueFactory(new PropertyValueFactory<>("displayName"));
name.setCellFactory(new ClickableCellFactory<>()); name.setCellFactory(new ClickableCellFactory<>());
name.setEditable(false); name.setEditable(false);
name.setId("name"); name.setId("name");
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL"); TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
url.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("url")); url.setCellValueFactory(new PropertyValueFactory<>("url"));
url.setCellFactory(new ClickableCellFactory<>()); url.setCellFactory(new ClickableCellFactory<>());
url.setPrefWidth(400); url.setPrefWidth(400);
url.setEditable(false); url.setEditable(false);
url.setId("url"); url.setId("url");
TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online"); TableColumn<JavaFxModel, String> online = new TableColumn<>("Online");
online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty()); online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty());
online.setCellFactory(CheckBoxTableCell.forTableColumn(online));
online.setPrefWidth(100); online.setPrefWidth(100);
online.setEditable(false); online.setEditable(false);
online.setId("online"); online.setId("online");
TableColumn<JavaFxModel, Boolean> recording = new TableColumn<>("Recording"); online.setStyle(STYLE_ALIGN_CENTER);
TableColumn<JavaFxModel, String> recording = new TableColumn<>("Recording");
recording.setCellValueFactory(cdf -> cdf.getValue().getRecordingProperty()); recording.setCellValueFactory(cdf -> cdf.getValue().getRecordingProperty());
recording.setCellFactory(CheckBoxTableCell.forTableColumn(recording)); recording.setCellFactory(tc -> new RecordingCell());
recording.setPrefWidth(100); recording.setPrefWidth(100);
recording.setEditable(false); recording.setEditable(false);
recording.setId("recording"); recording.setId("recording");
recording.setStyle(STYLE_ALIGN_CENTER);
TableColumn<JavaFxModel, Boolean> paused = new TableColumn<>("Paused"); TableColumn<JavaFxModel, Boolean> paused = new TableColumn<>("Paused");
paused.setCellValueFactory(cdf -> cdf.getValue().getPausedProperty()); paused.setCellValueFactory(cdf -> cdf.getValue().getPausedProperty());
paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused)); paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused));
@ -207,13 +215,13 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
priority.setId("priority"); priority.setId("priority");
TableColumn<JavaFxModel, Instant> lastSeen = new TableColumn<>("last seen"); TableColumn<JavaFxModel, Instant> lastSeen = new TableColumn<>("last seen");
lastSeen.setCellValueFactory(cdf -> cdf.getValue().lastSeenProperty()); lastSeen.setCellValueFactory(cdf -> cdf.getValue().lastSeenProperty());
lastSeen.setCellFactory(new DateTimeCellFactory<JavaFxModel>()); lastSeen.setCellFactory(new DateTimeCellFactory<>());
lastSeen.setPrefWidth(150); lastSeen.setPrefWidth(150);
lastSeen.setEditable(false); lastSeen.setEditable(false);
lastSeen.setId("lastSeen"); lastSeen.setId("lastSeen");
TableColumn<JavaFxModel, Instant> lastRecorded = new TableColumn<>("last recorded"); TableColumn<JavaFxModel, Instant> lastRecorded = new TableColumn<>("last recorded");
lastRecorded.setCellValueFactory(cdf -> cdf.getValue().lastRecordedProperty()); lastRecorded.setCellValueFactory(cdf -> cdf.getValue().lastRecordedProperty());
lastRecorded.setCellFactory(new DateTimeCellFactory<JavaFxModel>()); lastRecorded.setCellFactory(new DateTimeCellFactory<>());
lastRecorded.setPrefWidth(150); lastRecorded.setPrefWidth(150);
lastRecorded.setEditable(false); lastRecorded.setEditable(false);
lastRecorded.setId("lastRecorded"); lastRecorded.setId("lastRecorded");
@ -581,17 +589,21 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
return recorder.getModels() return recorder.getModels()
.stream() .stream()
.map(JavaFxModel::new) .map(JavaFxModel::new)
.peek(fxm -> { .peek(fxm -> { // NOSONAR
for (Recording recording : recordings) { for (Recording recording : recordings) {
if(recording.getStatus() == RECORDING && Objects.equals(recording.getModel(), fxm)){ if(recording.getStatus() == RECORDING && Objects.equals(recording.getModel(), fxm)){
fxm.getRecordingProperty().set(true); String recordingValue = HEAVY_CHECK_MARK;
if(!Objects.equals(recording.getModel().getRecordUntil(), Instant.ofEpochMilli(Model.RECORD_INDEFINITELY))) {
recordingValue += ' ' + CLOCK;
}
fxm.getRecordingProperty().set(recordingValue);
break; break;
} }
} }
for (Model onlineModel : onlineModels) { for (Model onlineModel : onlineModels) {
if(Objects.equals(onlineModel, fxm)) { if(Objects.equals(onlineModel, fxm)) {
fxm.getOnlineProperty().set(true); fxm.getOnlineProperty().set(HEAVY_CHECK_MARK);
break; break;
} }
} }
@ -649,6 +661,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
resumeRecording.setOnAction(e -> resumeRecording(selectedModels)); resumeRecording.setOnAction(e -> resumeRecording(selectedModels));
MenuItem stopRecordingAt = new MenuItem("Stop Recording at Date"); MenuItem stopRecordingAt = new MenuItem("Stop Recording at Date");
stopRecordingAt.setOnAction(e -> setStopDate(selectedModels.get(0))); stopRecordingAt.setOnAction(e -> setStopDate(selectedModels.get(0)));
MenuItem removeTimeLimit = new MenuItem("Remove Time Limit");
removeTimeLimit.setOnAction(e -> removeTimeLimit(selectedModels.get(0)));
MenuItem openInBrowser = new MenuItem("Open in Browser"); MenuItem openInBrowser = new MenuItem("Open in Browser");
openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl())); openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
MenuItem openInPlayer = new MenuItem("Open in Player"); MenuItem openInPlayer = new MenuItem("Open in Player");
@ -669,6 +683,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
if (selectedModels.size() == 1) { if (selectedModels.size() == 1) {
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording); menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
menu.getItems().add(stopRecordingAt); menu.getItems().add(stopRecordingAt);
if (selectedModels.get(0).isRecordingTimeLimited()) {
menu.getItems().add(removeTimeLimit);
}
} else { } else {
menu.getItems().addAll(resumeRecording, pauseRecording); menu.getItems().addAll(resumeRecording, pauseRecording);
} }
@ -687,13 +704,13 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void setStopDate(JavaFxModel model) { private void setStopDate(JavaFxModel model) {
DatePicker datePicker = new DatePicker(); DatePicker datePicker = new DatePicker();
GridPane grid = new GridPane(); GridPane gridPane = new GridPane();
grid.setHgap(10); gridPane.setHgap(10);
grid.setVgap(10); gridPane.setVgap(10);
grid.setPadding(new Insets(20, 150, 10, 10)); gridPane.setPadding(new Insets(20, 150, 10, 10));
grid.add(new Label("Stop at"), 0, 0); gridPane.add(new Label("Stop at"), 0, 0);
grid.add(datePicker, 1, 0); gridPane.add(datePicker, 1, 0);
grid.add(new Label("And then"), 0, 1); gridPane.add(new Label("And then"), 0, 1);
ToggleGroup toggleGroup = new ToggleGroup(); ToggleGroup toggleGroup = new ToggleGroup();
RadioButton pauseButton = new RadioButton("pause recording"); RadioButton pauseButton = new RadioButton("pause recording");
pauseButton.setSelected(model.getRecordUntilSubsequentAction() == PAUSE); pauseButton.setSelected(model.getRecordUntilSubsequentAction() == PAUSE);
@ -705,18 +722,19 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
row.getChildren().addAll(pauseButton, removeButton); row.getChildren().addAll(pauseButton, removeButton);
HBox.setMargin(pauseButton, new Insets(5)); HBox.setMargin(pauseButton, new Insets(5));
HBox.setMargin(removeButton, new Insets(5)); HBox.setMargin(removeButton, new Insets(5));
grid.add(row, 1, 1); gridPane.add(row, 1, 1);
if (model.getRecordUntil().toEpochMilli() != Model.RECORD_INDEFINITELY) { if (model.isRecordingTimeLimited()) {
LocalDate localDate = LocalDate.ofInstant(model.getRecordUntil(), ZoneId.systemDefault()); LocalDate localDate = LocalDate.ofInstant(model.getRecordUntil(), ZoneId.systemDefault());
datePicker.setValue(localDate); datePicker.setValue(localDate);
} }
boolean userClickedOk = Dialogs.showCustomInput(getTabPane().getScene(), "Stop Recording at", grid); boolean userClickedOk = Dialogs.showCustomInput(getTabPane().getScene(), "Stop Recording at", gridPane);
if (userClickedOk) { if (userClickedOk) {
SubsequentAction action = pauseButton.isSelected() ? PAUSE : REMOVE; SubsequentAction action = pauseButton.isSelected() ? PAUSE : REMOVE;
LOG.info("Stop at {} and {}", datePicker.getValue(), action); LOG.info("Stop at {} and {}", datePicker.getValue(), action);
Instant stopAt = Instant.from(datePicker.getValue().atStartOfDay().atZone(ZoneId.systemDefault())); Instant stopAt = Instant.from(datePicker.getValue().atStartOfDay().atZone(ZoneId.systemDefault()));
model.setRecordUntil(stopAt); model.setRecordUntil(stopAt);
model.setRecordUntilSubsequentAction(action); model.setRecordUntilSubsequentAction(action);
table.refresh();
try { try {
recorder.stopRecordingAt(model.getDelegate()); recorder.stopRecordingAt(model.getDelegate());
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) { } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
@ -725,6 +743,12 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} }
} }
private void removeTimeLimit(JavaFxModel selectedModel) {
new RemoveTimeLimitAction(table, selectedModel.getDelegate(), recorder) //
.execute() //
.whenComplete((result, exception) -> table.refresh());
}
private void ignore(ObservableList<JavaFxModel> selectedModels) { private void ignore(ObservableList<JavaFxModel> selectedModels) {
for (JavaFxModel fxModel : selectedModels) { for (JavaFxModel fxModel : selectedModels) {
Model modelToIgnore = fxModel.getDelegate(); Model modelToIgnore = fxModel.getDelegate();
@ -737,7 +761,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} }
private void follow(ObservableList<JavaFxModel> selectedModels) { private void follow(ObservableList<JavaFxModel> selectedModels) {
new FollowAction(getTabPane(), new ArrayList<JavaFxModel>(selectedModels)).execute(); new FollowAction(getTabPane(), new ArrayList<>(selectedModels)).execute();
} }
private void notes(ObservableList<JavaFxModel> selectedModels) { private void notes(ObservableList<JavaFxModel> selectedModels) {
@ -920,4 +944,25 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
return tableCell; return tableCell;
} }
} }
private class RecordingCell extends TableCell<JavaFxModel, String> {
@Override
protected void updateItem(String value, boolean empty) {
super.updateItem(value, empty);
if (value == null) {
setTooltip(null);
setText(null);
} else {
Model m = getTableView().getItems().get(getTableRow().getIndex());
if (m.isRecordingTimeLimited()) {
Tooltip tooltip = new Tooltip();
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
ZonedDateTime zonedDateTime = m.getRecordUntil().atZone(ZoneId.systemDefault());
tooltip.setText("Recording until " + dtf.format(zonedDateTime));
setTooltip(tooltip);
}
setText(value);
}
}
}
} }

View File

@ -237,6 +237,11 @@ public abstract class AbstractModel implements Model {
this.lastRecorded = lastRecorded; this.lastRecorded = lastRecorded;
} }
@Override
public boolean isRecordingTimeLimited() {
return !getRecordUntil().equals(Instant.ofEpochMilli(RECORD_INDEFINITELY));
}
@Override @Override
public Instant getRecordUntil() { public Instant getRecordUntil() {
return Optional.ofNullable(recordUntil).orElse(Instant.ofEpochMilli(RECORD_INDEFINITELY)); return Optional.ofNullable(recordUntil).orElse(Instant.ofEpochMilli(RECORD_INDEFINITELY));

View File

@ -130,6 +130,7 @@ public interface Model extends Comparable<Model>, Serializable {
public HttpHeaderFactory getHttpHeaderFactory(); public HttpHeaderFactory getHttpHeaderFactory();
public boolean isRecordingTimeLimited();
public Instant getRecordUntil(); public Instant getRecordUntil();
public void setRecordUntil(Instant instant); public void setRecordUntil(Instant instant);