From 97715aecc5460fb94e8d07a144ae298bcb9aa397 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Mon, 21 Dec 2020 18:53:34 +0100 Subject: [PATCH] Improve UI features for time limited recordings --- .../src/main/java/ctbrec/ui/JavaFxModel.java | 15 +++- .../src/main/java/ctbrec/ui/UnicodeEmoji.java | 8 ++ .../ui/action/RemoveTimeLimitAction.java | 41 +++++++++ .../ctbrec/ui/tabs/RecordedModelsTab.java | 89 ++++++++++++++----- .../src/main/java/ctbrec/AbstractModel.java | 5 ++ common/src/main/java/ctbrec/Model.java | 1 + 6 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/UnicodeEmoji.java create mode 100644 client/src/main/java/ctbrec/ui/action/RemoveTimeLimitAction.java diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index 3902f842..f2ad24c8 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -22,13 +22,15 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; 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 */ public class JavaFxModel implements Model { - private transient BooleanProperty onlineProperty = new SimpleBooleanProperty(); - private transient BooleanProperty recordingProperty = new SimpleBooleanProperty(); + private transient StringProperty onlineProperty = new SimpleStringProperty(); + private transient StringProperty recordingProperty = new SimpleStringProperty(); private transient BooleanProperty pausedProperty = new SimpleBooleanProperty(); private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty(); private transient SimpleObjectProperty lastSeenProperty = new SimpleObjectProperty<>(); @@ -103,11 +105,11 @@ public class JavaFxModel implements Model { return delegate.toString(); } - public BooleanProperty getOnlineProperty() { + public StringProperty getOnlineProperty() { return onlineProperty; } - public BooleanProperty getRecordingProperty() { + public StringProperty getRecordingProperty() { return recordingProperty; } @@ -312,4 +314,9 @@ public class JavaFxModel implements Model { public boolean exists() throws IOException { return delegate.exists(); } + + @Override + public boolean isRecordingTimeLimited() { + return delegate.isRecordingTimeLimited(); + } } diff --git a/client/src/main/java/ctbrec/ui/UnicodeEmoji.java b/client/src/main/java/ctbrec/ui/UnicodeEmoji.java new file mode 100644 index 00000000..7c981df9 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/UnicodeEmoji.java @@ -0,0 +1,8 @@ +package ctbrec.ui; + +public class UnicodeEmoji { + + public static final String HEAVY_CHECK_MARK = "✔"; + public static final String CLOCK = "🕒"; + +} diff --git a/client/src/main/java/ctbrec/ui/action/RemoveTimeLimitAction.java b/client/src/main/java/ctbrec/ui/action/RemoveTimeLimitAction.java new file mode 100644 index 00000000..fd3e6460 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/RemoveTimeLimitAction.java @@ -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 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)); + } +} diff --git a/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java index 90eba303..044063d3 100644 --- a/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java @@ -2,6 +2,7 @@ package ctbrec.ui.tabs; import static ctbrec.Recording.State.*; import static ctbrec.SubsequentAction.*; +import static ctbrec.ui.UnicodeEmoji.*; import java.io.IOException; import java.security.InvalidKeyException; @@ -9,6 +10,9 @@ import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.time.LocalDate; 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.Collections; import java.util.Iterator; @@ -44,6 +48,7 @@ import ctbrec.ui.action.FollowAction; import ctbrec.ui.action.OpenRecordingsDir; import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PlayAction; +import ctbrec.ui.action.RemoveTimeLimitAction; import ctbrec.ui.action.ResumeAction; import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.action.ToggleRecordingAction; @@ -107,6 +112,8 @@ import javafx.util.converter.NumberStringConverter; public class RecordedModelsTab extends Tab implements TabSelectionListener { private static final Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class); + private static final String STYLE_ALIGN_CENTER = "-fx-alignment: CENTER;"; + private ReentrantLock lock = new ReentrantLock(); private ScheduledService> updateService; private Recorder recorder; @@ -168,28 +175,29 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } TableColumn name = new TableColumn<>("Model"); name.setPrefWidth(200); - name.setCellValueFactory(new PropertyValueFactory("displayName")); + name.setCellValueFactory(new PropertyValueFactory<>("displayName")); name.setCellFactory(new ClickableCellFactory<>()); name.setEditable(false); name.setId("name"); TableColumn url = new TableColumn<>("URL"); - url.setCellValueFactory(new PropertyValueFactory("url")); + url.setCellValueFactory(new PropertyValueFactory<>("url")); url.setCellFactory(new ClickableCellFactory<>()); url.setPrefWidth(400); url.setEditable(false); url.setId("url"); - TableColumn online = new TableColumn<>("Online"); + TableColumn online = new TableColumn<>("Online"); online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty()); - online.setCellFactory(CheckBoxTableCell.forTableColumn(online)); online.setPrefWidth(100); online.setEditable(false); online.setId("online"); - TableColumn recording = new TableColumn<>("Recording"); + online.setStyle(STYLE_ALIGN_CENTER); + TableColumn recording = new TableColumn<>("Recording"); recording.setCellValueFactory(cdf -> cdf.getValue().getRecordingProperty()); - recording.setCellFactory(CheckBoxTableCell.forTableColumn(recording)); + recording.setCellFactory(tc -> new RecordingCell()); recording.setPrefWidth(100); recording.setEditable(false); recording.setId("recording"); + recording.setStyle(STYLE_ALIGN_CENTER); TableColumn paused = new TableColumn<>("Paused"); paused.setCellValueFactory(cdf -> cdf.getValue().getPausedProperty()); paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused)); @@ -207,13 +215,13 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { priority.setId("priority"); TableColumn lastSeen = new TableColumn<>("last seen"); lastSeen.setCellValueFactory(cdf -> cdf.getValue().lastSeenProperty()); - lastSeen.setCellFactory(new DateTimeCellFactory()); + lastSeen.setCellFactory(new DateTimeCellFactory<>()); lastSeen.setPrefWidth(150); lastSeen.setEditable(false); lastSeen.setId("lastSeen"); TableColumn lastRecorded = new TableColumn<>("last recorded"); lastRecorded.setCellValueFactory(cdf -> cdf.getValue().lastRecordedProperty()); - lastRecorded.setCellFactory(new DateTimeCellFactory()); + lastRecorded.setCellFactory(new DateTimeCellFactory<>()); lastRecorded.setPrefWidth(150); lastRecorded.setEditable(false); lastRecorded.setId("lastRecorded"); @@ -581,17 +589,21 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { return recorder.getModels() .stream() .map(JavaFxModel::new) - .peek(fxm -> { + .peek(fxm -> { // NOSONAR for (Recording recording : recordings) { 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; } } for (Model onlineModel : onlineModels) { if(Objects.equals(onlineModel, fxm)) { - fxm.getOnlineProperty().set(true); + fxm.getOnlineProperty().set(HEAVY_CHECK_MARK); break; } } @@ -649,6 +661,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { resumeRecording.setOnAction(e -> resumeRecording(selectedModels)); MenuItem stopRecordingAt = new MenuItem("Stop Recording at Date"); 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"); openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl())); MenuItem openInPlayer = new MenuItem("Open in Player"); @@ -669,6 +683,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { if (selectedModels.size() == 1) { menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording); menu.getItems().add(stopRecordingAt); + if (selectedModels.get(0).isRecordingTimeLimited()) { + menu.getItems().add(removeTimeLimit); + } } else { menu.getItems().addAll(resumeRecording, pauseRecording); } @@ -687,13 +704,13 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private void setStopDate(JavaFxModel model) { DatePicker datePicker = new DatePicker(); - GridPane grid = new GridPane(); - grid.setHgap(10); - grid.setVgap(10); - grid.setPadding(new Insets(20, 150, 10, 10)); - grid.add(new Label("Stop at"), 0, 0); - grid.add(datePicker, 1, 0); - grid.add(new Label("And then"), 0, 1); + GridPane gridPane = new GridPane(); + gridPane.setHgap(10); + gridPane.setVgap(10); + gridPane.setPadding(new Insets(20, 150, 10, 10)); + gridPane.add(new Label("Stop at"), 0, 0); + gridPane.add(datePicker, 1, 0); + gridPane.add(new Label("And then"), 0, 1); ToggleGroup toggleGroup = new ToggleGroup(); RadioButton pauseButton = new RadioButton("pause recording"); pauseButton.setSelected(model.getRecordUntilSubsequentAction() == PAUSE); @@ -705,18 +722,19 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { row.getChildren().addAll(pauseButton, removeButton); HBox.setMargin(pauseButton, new Insets(5)); HBox.setMargin(removeButton, new Insets(5)); - grid.add(row, 1, 1); - if (model.getRecordUntil().toEpochMilli() != Model.RECORD_INDEFINITELY) { + gridPane.add(row, 1, 1); + if (model.isRecordingTimeLimited()) { LocalDate localDate = LocalDate.ofInstant(model.getRecordUntil(), ZoneId.systemDefault()); 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) { SubsequentAction action = pauseButton.isSelected() ? PAUSE : REMOVE; LOG.info("Stop at {} and {}", datePicker.getValue(), action); Instant stopAt = Instant.from(datePicker.getValue().atStartOfDay().atZone(ZoneId.systemDefault())); model.setRecordUntil(stopAt); model.setRecordUntilSubsequentAction(action); + table.refresh(); try { recorder.stopRecordingAt(model.getDelegate()); } 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 selectedModels) { for (JavaFxModel fxModel : selectedModels) { Model modelToIgnore = fxModel.getDelegate(); @@ -737,7 +761,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private void follow(ObservableList selectedModels) { - new FollowAction(getTabPane(), new ArrayList(selectedModels)).execute(); + new FollowAction(getTabPane(), new ArrayList<>(selectedModels)).execute(); } private void notes(ObservableList selectedModels) { @@ -920,4 +944,25 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { return tableCell; } } + + private class RecordingCell extends TableCell { + @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); + } + } + } } diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index 8a3dac36..8c16d499 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -237,6 +237,11 @@ public abstract class AbstractModel implements Model { this.lastRecorded = lastRecorded; } + @Override + public boolean isRecordingTimeLimited() { + return !getRecordUntil().equals(Instant.ofEpochMilli(RECORD_INDEFINITELY)); + } + @Override public Instant getRecordUntil() { return Optional.ofNullable(recordUntil).orElse(Instant.ofEpochMilli(RECORD_INDEFINITELY)); diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index 15b94c7f..1a75a170 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -130,6 +130,7 @@ public interface Model extends Comparable, Serializable { public HttpHeaderFactory getHttpHeaderFactory(); + public boolean isRecordingTimeLimited(); public Instant getRecordUntil(); public void setRecordUntil(Instant instant);