diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d5899ef..ffea0876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +3.8.1 +======================== +* Fixed recent MFC error +* Added log file rotation +* Fixed a bug with the resolution slider + 3.8.0 ======================== * Server Settings are now editable in the web-interface diff --git a/client/pom.xml b/client/pom.xml index 96c8a191..76f36cf6 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 3.8.0 + 3.8.1 ../master diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 4143697c..9eb41adf 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -89,7 +89,6 @@ public class CamrecApplication extends Application { private Recorder recorder; private OnlineMonitor onlineMonitor; static HostServices hostServices; - private SettingsTab settingsTab; private BorderPane rootPane = new BorderPane(); private HBox statusBar = new HBox(); private Label statusLabel = new Label(); @@ -190,8 +189,7 @@ public class CamrecApplication extends Application { tabPane.getTabs().add(modelsTab); recordingsTab = new RecordingsTab("Recordings", recorder, config, sites); tabPane.getTabs().add(recordingsTab); - settingsTab = new SettingsTab(sites, recorder); - tabPane.getTabs().add(settingsTab); + tabPane.getTabs().add(new SettingsTab(sites, recorder)); tabPane.getTabs().add(new NewsTab()); tabPane.getTabs().add(new DonateTabFx()); tabPane.getTabs().add(new HelpTab()); @@ -207,6 +205,7 @@ public class CamrecApplication extends Application { primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/SearchBox.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/Popover.css"); + primaryStage.getScene().getStylesheets().add("/ctbrec/ui/settings/api/Preferences.css"); primaryStage.getScene().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue()); primaryStage.getScene().heightProperty() .addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue()); @@ -258,7 +257,6 @@ public class CamrecApplication extends Application { public void run() { modelsTab.saveState(); recordingsTab.saveState(); - settingsTab.saveConfig(); onlineMonitor.shutdown(); recorder.shutdown(); for (Site site : sites) { diff --git a/client/src/main/java/ctbrec/ui/Launcher.java b/client/src/main/java/ctbrec/ui/Launcher.java index d42d769b..0cd88767 100644 --- a/client/src/main/java/ctbrec/ui/Launcher.java +++ b/client/src/main/java/ctbrec/ui/Launcher.java @@ -7,7 +7,7 @@ import ctbrec.Java; public class Launcher { - private static final transient Logger LOG = LoggerFactory.getLogger(Launcher.class); + private static final Logger LOG = LoggerFactory.getLogger(Launcher.class); public static void main(String[] args) { int javaVersion = Java.version(); diff --git a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java index 5fc0ce2c..0f3820b8 100644 --- a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java @@ -46,9 +46,10 @@ public abstract class AbstractFileSelectionBox extends HBox { } }); Node browse = createBrowseButton(); - getChildren().addAll(fileInput, browse); - fileInput.disableProperty().bind(disableProperty()); browse.disableProperty().bind(disableProperty()); + fileInput.disableProperty().bind(disableProperty()); + fileInput.textProperty().bindBidirectional(fileProperty); + getChildren().addAll(fileInput, browse); HBox.setHgrow(fileInput, Priority.ALWAYS); disabledProperty().addListener((obs, oldV, newV) -> { @@ -70,10 +71,12 @@ public abstract class AbstractFileSelectionBox extends HBox { private ChangeListener textListener() { return (obs, o, n) -> { String input = fileInput.getText(); - if (StringUtil.isBlank(input) && allowEmptyValue) { - fileProperty.set(""); - hideValidationHints(); - return; + if (StringUtil.isBlank(input)) { + if (allowEmptyValue) { + fileProperty.set(""); + hideValidationHints(); + return; + } } else { File program = new File(input); setFile(program); @@ -122,6 +125,8 @@ public abstract class AbstractFileSelectionBox extends HBox { private Button createBrowseButton() { Button button = new Button("Select"); button.setOnAction(e -> choose()); + button.prefHeightProperty().bind(this.heightProperty()); + button.prefWidthProperty().set(70); return button; } diff --git a/client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java b/client/src/main/java/ctbrec/ui/controls/autocomplete/AutoFillTextField.java similarity index 66% rename from client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java rename to client/src/main/java/ctbrec/ui/controls/autocomplete/AutoFillTextField.java index ca772778..6b12b354 100644 --- a/client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java +++ b/client/src/main/java/ctbrec/ui/controls/autocomplete/AutoFillTextField.java @@ -1,7 +1,8 @@ -package ctbrec.ui.controls; +package ctbrec.ui.controls.autocomplete; -import javafx.collections.ObservableList; +import java.util.Optional; + import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.control.TextField; @@ -10,12 +11,12 @@ import javafx.scene.input.KeyEvent; public class AutoFillTextField extends TextField { - private ObservableList suggestions; private EventHandler handler; + private Suggester suggester; - public AutoFillTextField(ObservableList suggestions) { - this.suggestions = suggestions; - addEventHandler(KeyEvent.KEY_RELEASED, (evt) -> { + public AutoFillTextField(Suggester suggester) { + this.suggester = suggester; + addEventHandler(KeyEvent.KEY_RELEASED, evt -> { if (evt.getCode().isLetterKey() || evt.getCode().isDigitKey()) { autocomplete(false); } else if (evt.getCode() == KeyCode.ENTER) { @@ -38,16 +39,19 @@ public class AutoFillTextField extends TextField { if(oldtext.isEmpty()) { return; } - for (String sug : suggestions) { - boolean startsWith = sug.toLowerCase().startsWith(oldtext.toLowerCase()); - boolean textMatch = fulltextSearch && sug.toLowerCase().contains(oldtext.toLowerCase()); - if(startsWith || textMatch) { - setText(sug); - int pos = oldtext.length(); - positionCaret(pos); - selectRange(pos, sug.length()); - break; - } + + Optional match = null; + if(fulltextSearch) { + match = suggester.fulltext(oldtext); + } else { + match = suggester.startsWith(oldtext); + } + + if(match.isPresent()) { + setText(match.get()); + int pos = oldtext.length(); + positionCaret(pos); + selectRange(pos, match.get().length()); } } diff --git a/client/src/main/java/ctbrec/ui/controls/autocomplete/ObservableListSuggester.java b/client/src/main/java/ctbrec/ui/controls/autocomplete/ObservableListSuggester.java new file mode 100644 index 00000000..a4970b32 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/autocomplete/ObservableListSuggester.java @@ -0,0 +1,37 @@ +package ctbrec.ui.controls.autocomplete; + +import java.util.Optional; + +import javafx.collections.ObservableList; + +public class ObservableListSuggester implements Suggester { + + private ObservableList suggestions; + + public ObservableListSuggester(ObservableList suggestions) { + this.suggestions = suggestions; + } + + @Override + public Optional startsWith(String search) { + for (Object sug : suggestions) { + boolean startsWith = sug.toString().toLowerCase().startsWith(search.toLowerCase()); + if (startsWith) { + return Optional.of(sug.toString()); + } + } + return Optional.empty(); + } + + @Override + public Optional fulltext(String search) { + for (Object sug : suggestions) { + boolean startsWith = sug.toString().toLowerCase().contains(search.toLowerCase()); + if (startsWith) { + return Optional.of(sug.toString()); + } + } + return Optional.empty(); + } + +} diff --git a/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggester.java b/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggester.java new file mode 100644 index 00000000..f3e1e8c2 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggester.java @@ -0,0 +1,9 @@ +package ctbrec.ui.controls.autocomplete; + +import java.util.Optional; + +public interface Suggester { + + public Optional startsWith(String search); + public Optional fulltext(String search); +} \ No newline at end of file diff --git a/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggestion.java b/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggestion.java new file mode 100644 index 00000000..82378795 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggestion.java @@ -0,0 +1,5 @@ +package ctbrec.ui.controls.autocomplete; + +public class Suggestion { + +} diff --git a/client/src/main/java/ctbrec/ui/controls/range/RangeSlider.java b/client/src/main/java/ctbrec/ui/controls/range/RangeSlider.java index 78d98b21..b1c27951 100644 --- a/client/src/main/java/ctbrec/ui/controls/range/RangeSlider.java +++ b/client/src/main/java/ctbrec/ui/controls/range/RangeSlider.java @@ -1,7 +1,7 @@ package ctbrec.ui.controls.range; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Orientation; import javafx.scene.control.Control; import javafx.scene.control.Skin; @@ -11,16 +11,17 @@ public class RangeSlider extends Control { private static final String DEFAULT_STYLE_CLASS = "rangeslider"; private Range range; - private DoubleProperty low; - private DoubleProperty high; + private ObjectProperty low; + private ObjectProperty high; private boolean showTickMarks = false; private boolean showTickLabels = false; private Orientation orientation = Orientation.HORIZONTAL; + @SuppressWarnings({ "unchecked", "rawtypes" }) public RangeSlider(Range range) { this.range = range; - low = new SimpleDoubleProperty(getMinimum().doubleValue()); - high = new SimpleDoubleProperty(getMaximum().doubleValue()); + low = new SimpleObjectProperty(getMinimum()); + high = new SimpleObjectProperty(getMaximum()); getStyleClass().setAll(DEFAULT_STYLE_CLASS); } @@ -34,20 +35,20 @@ public class RangeSlider extends Control { return RangeSlider.class.getResource("rangeslider.css").toExternalForm(); } - public DoubleProperty getLow() { + public ObjectProperty getLow() { return low; } public void setLow(T newPosition) { - low.set(newPosition.doubleValue()); + low.set(newPosition); } - public DoubleProperty getHigh() { + public ObjectProperty getHigh() { return high; } public void setHigh(T newPosition) { - this.high.set(newPosition.doubleValue()); + this.high.set(newPosition); } public T getMinimum() { diff --git a/client/src/main/java/ctbrec/ui/controls/range/RangeSliderBehavior.java b/client/src/main/java/ctbrec/ui/controls/range/RangeSliderBehavior.java index f2fe2484..b2496f65 100644 --- a/client/src/main/java/ctbrec/ui/controls/range/RangeSliderBehavior.java +++ b/client/src/main/java/ctbrec/ui/controls/range/RangeSliderBehavior.java @@ -5,6 +5,9 @@ import java.util.List; import com.sun.javafx.scene.control.behavior.BehaviorBase; // NOSONAR import com.sun.javafx.scene.control.inputmap.InputMap; // NOSONAR +import javafx.scene.Node; +import javafx.scene.input.MouseEvent; + public class RangeSliderBehavior extends BehaviorBase> { private RangeSlider rangeSlider; @@ -12,6 +15,13 @@ public class RangeSliderBehavior extends BehaviorBase rangeSlider) { super(rangeSlider); this.rangeSlider = rangeSlider; + rangeSlider.addEventFilter(MouseEvent.MOUSE_CLICKED, this::sliderClicked); + } + + private void sliderClicked(MouseEvent me) { + Node source = (Node) me.getSource(); + double positionPercentage = me.getX() / source.getBoundsInParent().getWidth(); + moveClosestThumbTo(positionPercentage); } @Override @@ -25,7 +35,12 @@ public class RangeSliderBehavior extends BehaviorBase= high.doubleValue()) { + newPosition = getLow(); + } + rangeSlider.setLow(newPosition); } /** @@ -33,7 +48,33 @@ public class RangeSliderBehavior extends BehaviorBase extends BehaviorBase> { this.behavior = behavior; initTrack(); initThumbs(thumbRange); - registerChangeListener(control.getLow(), (obsVal) -> getSkinnable().requestLayout()); - registerChangeListener(control.getHigh(), (obsVal) -> getSkinnable().requestLayout()); + registerChangeListener(control.getLow(), obsVal -> getSkinnable().requestLayout()); + registerChangeListener(control.getHigh(), obsVal -> getSkinnable().requestLayout()); } private void initThumbs(ThumbRange t) { @@ -42,10 +42,8 @@ public class RangeSliderSkin extends SkinBase> { t.low.setOnMousePressed(me -> { preDragThumbPoint = t.low.localToParent(me.getX(), me.getY()); - preDragPos = (getSkinnable().getLow().doubleValue() - getSkinnable().getMinimum().doubleValue()) / (getMaxMinusMinNoZero()); + preDragPos = (getSkinnable().getLow().get().doubleValue() - getSkinnable().getMinimum().doubleValue()) / (getMaxMinusMinNoZero()); }); - - t.low.setOnMouseDragged(me -> { Point2D cur = t.low.localToParent(me.getX(), me.getY()); double dragPos = (isHorizontal()) ? cur.getX() - preDragThumbPoint.getX() : -(cur.getY() - preDragThumbPoint.getY()); @@ -54,17 +52,14 @@ public class RangeSliderSkin extends SkinBase> { t.high.setOnMousePressed(me -> { preDragThumbPoint = t.high.localToParent(me.getX(), me.getY()); - preDragPos = (getSkinnable().getHigh().doubleValue() - getSkinnable().getMinimum().doubleValue()) / (getMaxMinusMinNoZero()); + preDragPos = (getSkinnable().getHigh().get().doubleValue() - getSkinnable().getMinimum().doubleValue()) / (getMaxMinusMinNoZero()); }); - - t.high.setOnMouseDragged(me -> { boolean orientation = getSkinnable().getOrientation() == Orientation.HORIZONTAL; double trackLen = orientation ? track.getWidth() : track.getHeight(); Point2D cur = t.high.localToParent(me.getX(), me.getY()); double dragPos = getSkinnable().getOrientation() != Orientation.HORIZONTAL ? -(cur.getY() - preDragThumbPoint.getY()) : cur.getX() - preDragThumbPoint.getX(); behavior.highThumbDragged(preDragPos + dragPos / trackLen); - }); } @@ -165,8 +160,8 @@ public class RangeSliderSkin extends SkinBase> { private void positionThumbs() { RangeSlider s = getSkinnable(); - double lxl = trackStart + (trackLength * ((s.getLow().doubleValue() - s.getMinimum().doubleValue()) / (getMaxMinusMinNoZero())) - thumbWidth / 2D); - double lxh = trackStart + (trackLength * ((s.getHigh().doubleValue() - s.getMinimum().doubleValue()) / (getMaxMinusMinNoZero())) - thumbWidth / 2D); + double lxl = trackStart + (trackLength * ((s.getLow().get().doubleValue() - s.getMinimum().doubleValue()) / (getMaxMinusMinNoZero())) - thumbWidth / 2D); + double lxh = trackStart + (trackLength * ((s.getHigh().get().doubleValue() - s.getMinimum().doubleValue()) / (getMaxMinusMinNoZero())) - thumbWidth / 2D); double ly = lowThumbPos; if (thumbRange != null) { @@ -208,7 +203,6 @@ public class RangeSliderSkin extends SkinBase> { if (isHorizontal()) { if (showTickMarks) { double w = Math.max(140, tickLine.prefWidth(-1)); - System.err.println("computePrefWidth " + w); return w; } else { return 140; diff --git a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java index 80f88e62..6e945d46 100644 --- a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java @@ -50,9 +50,7 @@ import javafx.scene.control.ScrollPane; import javafx.scene.control.SelectionMode; import javafx.scene.control.Separator; import javafx.scene.control.TextField; -import javafx.scene.control.TitledPane; import javafx.scene.image.Image; -import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; @@ -61,8 +59,8 @@ import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.Window; -public class ActionSettingsPanel extends TitledPane { - private static final transient Logger LOG = LoggerFactory.getLogger(ActionSettingsPanel.class); +public class ActionSettingsPanel extends GridPane { + private static final Logger LOG = LoggerFactory.getLogger(ActionSettingsPanel.class); private ListView actionTable; private TextField name = new TextField(); @@ -80,11 +78,8 @@ public class ActionSettingsPanel extends TitledPane { private Recorder recorder; - public ActionSettingsPanel(SettingsTab settingsTab, Recorder recorder) { + public ActionSettingsPanel(Recorder recorder) { this.recorder = recorder; - setText("Events & Actions"); - setExpanded(true); - setCollapsible(false); createGui(); loadEventHandlers(); } @@ -94,15 +89,21 @@ public class ActionSettingsPanel extends TitledPane { } private void createGui() { - BorderPane mainLayout = new BorderPane(); - setContent(mainLayout); + setHgap(10); + setVgap(10); + setPadding(new Insets(20, 10, 10, 10)); + + Label headline = new Label("Events & Actions"); + headline.getStyleClass().add("settings-group-label"); + add(headline, 0, 0); actionTable = createActionTable(); ScrollPane scrollPane = new ScrollPane(actionTable); scrollPane.setFitToHeight(true); scrollPane.setFitToWidth(true); scrollPane.setStyle("-fx-background-color: -fx-background"); - mainLayout.setCenter(scrollPane); + add(scrollPane, 0, 1); + GridPane.setHgrow(scrollPane, Priority.ALWAYS); Button add = new Button("Add"); add.setOnAction(this::add); @@ -110,15 +111,10 @@ public class ActionSettingsPanel extends TitledPane { delete.setOnAction(this::delete); delete.setDisable(true); HBox buttons = new HBox(5, add, delete); - mainLayout.setBottom(buttons); - BorderPane.setMargin(buttons, new Insets(5, 0, 0, 0)); + buttons.setStyle("-fx-background-color: -fx-background"); // workaround so that the buttons don't shrink + add(buttons, 0, 2); - actionTable.getSelectionModel().getSelectedItems().addListener(new ListChangeListener() { - @Override - public void onChanged(Change change) { - delete.setDisable(change.getList().isEmpty()); - } - }); + actionTable.getSelectionModel().getSelectedItems().addListener((ListChangeListener) change -> delete.setDisable(change.getList().isEmpty())); } private void add(ActionEvent evt) { @@ -235,9 +231,7 @@ public class ActionSettingsPanel extends TitledPane { event.getItems().clear(); event.getItems().add(Event.Type.MODEL_STATUS_CHANGED); event.getItems().add(Event.Type.RECORDING_STATUS_CHANGED); - event.setOnAction(evt -> { - modelState.setVisible(event.getSelectionModel().getSelectedItem() == Event.Type.MODEL_STATUS_CHANGED); - }); + event.setOnAction(evt -> modelState.setVisible(event.getSelectionModel().getSelectedItem() == Event.Type.MODEL_STATUS_CHANGED)); event.getSelectionModel().select(Event.Type.MODEL_STATUS_CHANGED); layout.add(event, 1, row++); @@ -254,7 +248,7 @@ public class ActionSettingsPanel extends TitledPane { Label l = new Label("Models"); layout.add(l, 0, row); - modelSelectionPane = new ListSelectionPane(recorder.getModels(), Collections.emptyList()); + modelSelectionPane = new ListSelectionPane<>(recorder.getModels(), Collections.emptyList()); layout.add(modelSelectionPane, 1, row++); GridPane.setValignment(l, VPos.TOP); GridPane.setHgrow(modelSelectionPane, Priority.ALWAYS); @@ -289,7 +283,6 @@ public class ActionSettingsPanel extends TitledPane { private ListView createActionTable() { ListView view = new ListView<>(); view.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - view.setPrefSize(300, 200); return view; } diff --git a/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java b/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java index b91765d1..0725b779 100644 --- a/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java +++ b/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java @@ -1,6 +1,9 @@ package ctbrec.ui.settings; +import java.io.IOException; + import ctbrec.Config; +import ctbrec.ui.controls.Dialogs; import javafx.scene.control.Button; import javafx.scene.control.ColorPicker; import javafx.scene.control.Tooltip; @@ -14,41 +17,49 @@ public class ColorSettingsPane extends HBox { ColorPicker accentColor = new ColorPicker(); Button reset = new Button("Reset"); Pane foobar = new Pane(); + private Config config; - public ColorSettingsPane(SettingsTab settingsTab) { + public ColorSettingsPane(Config config) { super(5); + this.config = config; getChildren().add(baseColor); getChildren().add(accentColor); getChildren().add(reset); - baseColor.setValue(Color.web(Config.getInstance().getSettings().colorBase)); + baseColor.setValue(Color.web(config.getSettings().colorBase)); baseColor.setTooltip(new Tooltip("Base Color")); baseColor.setMaxSize(44, 25); - accentColor.setValue(Color.web(Config.getInstance().getSettings().colorAccent)); + accentColor.setValue(Color.web(config.getSettings().colorAccent)); accentColor.setTooltip(new Tooltip("Accent Color")); accentColor.setMaxSize(44, 25); + reset.setMinSize(60, 25); reset.setMaxSize(60, 25); baseColor.setOnAction(evt -> { - Config.getInstance().getSettings().colorBase = toWeb(baseColor.getValue()); - settingsTab.showRestartRequired(); - settingsTab.saveConfig(); + config.getSettings().colorBase = toWeb(baseColor.getValue()); + save(); }); accentColor.setOnAction(evt -> { - Config.getInstance().getSettings().colorAccent = toWeb(accentColor.getValue()); - settingsTab.showRestartRequired(); - settingsTab.saveConfig(); + config.getSettings().colorAccent = toWeb(accentColor.getValue()); + save(); }); reset.setOnAction(evt -> { baseColor.setValue(Color.WHITE); - Config.getInstance().getSettings().colorBase = toWeb(Color.WHITE); + config.getSettings().colorBase = toWeb(Color.WHITE); accentColor.setValue(Color.WHITE); - Config.getInstance().getSettings().colorAccent = toWeb(Color.WHITE); - settingsTab.showRestartRequired(); - settingsTab.saveConfig(); + config.getSettings().colorAccent = toWeb(Color.WHITE); + save(); }); } + private void save() { + try { + config.save(); + } catch (IOException e) { + Dialogs.showError(getScene(), "Save Settings", "Couldn't save color settings", e); + } + } + private String toWeb(Color value) { StringBuilder sb = new StringBuilder("#"); sb.append(toHex((int) (value.getRed() * 255))); diff --git a/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java b/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java new file mode 100644 index 00000000..b3bb8180 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java @@ -0,0 +1,285 @@ +package ctbrec.ui.settings; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Settings; +import ctbrec.StringUtil; +import ctbrec.ui.controls.DirectorySelectionBox; +import ctbrec.ui.controls.ProgramSelectionBox; +import ctbrec.ui.controls.range.DiscreteRange; +import ctbrec.ui.controls.range.RangeSlider; +import ctbrec.ui.settings.api.ExclusiveSelectionProperty; +import ctbrec.ui.settings.api.Preferences; +import ctbrec.ui.settings.api.PreferencesStorage; +import ctbrec.ui.settings.api.Setting; +import ctbrec.ui.settings.api.SimpleDirectoryProperty; +import ctbrec.ui.settings.api.SimpleFileProperty; +import ctbrec.ui.settings.api.SimpleRangeProperty; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ListProperty; +import javafx.beans.property.LongProperty; +import javafx.beans.property.Property; +import javafx.beans.property.StringProperty; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; +import javafx.util.converter.NumberStringConverter; + +public class CtbrecPreferencesStorage implements PreferencesStorage { + + private static final Logger LOG = LoggerFactory.getLogger(CtbrecPreferencesStorage.class); + public static final String PATTERN_NOT_A_DIGIT = "[^\\d]"; + public static final String COULDNT_SAVE_MSG = "Couldn't save config setting"; + + private Config config; + private Settings settings; + + public CtbrecPreferencesStorage(Config config) { + this.config = config; + this.settings = config.getSettings(); + } + + @Override + public void save(Preferences preferences) throws IOException { + throw new RuntimeException("not implemented"); + } + + @Override + public void load(Preferences preferences) { + throw new RuntimeException("not implemented"); + } + + @Override + public Node createGui(Setting setting) throws Exception { + Property prop = setting.getProperty(); + if (prop instanceof ExclusiveSelectionProperty) { + return createRadioGroup(setting); + } else if (prop instanceof SimpleRangeProperty) { + return createRangeSlider(setting); + } else if (prop instanceof SimpleDirectoryProperty) { + return createDirectorySelector(setting); + } else if (prop instanceof SimpleFileProperty) { + return createFileSelector(setting); + } else if (prop instanceof IntegerProperty) { + return createIntegerProperty(setting); + } else if (prop instanceof LongProperty) { + return createLongProperty(setting); + } else if (prop instanceof BooleanProperty) { + return createBooleanProperty(setting); + } else if (prop instanceof ListProperty) { + return createComboBox(setting); + } else if (prop instanceof StringProperty) { + return createStringProperty(setting); + } else { + return new Label("Unsupported Type for key " + setting.getKey() + ": " + setting.getProperty()); + } + } + + private Node createRadioGroup(Setting setting) { + ExclusiveSelectionProperty prop = (ExclusiveSelectionProperty) setting.getProperty(); + ToggleGroup toggleGroup = new ToggleGroup(); + RadioButton optionA = new RadioButton(prop.getOptionA()); + optionA.setSelected(prop.getValue()); + optionA.setToggleGroup(toggleGroup); + RadioButton optionB = new RadioButton(prop.getOptionB()); + optionB.setSelected(!optionA.isSelected()); + optionB.setToggleGroup(toggleGroup); + optionA.selectedProperty().bindBidirectional(prop); + prop.addListener((obs, oldV, newV) -> saveValue(() -> { + Field field = Settings.class.getField(setting.getKey()); + field.set(settings, newV); + config.save(); + })); + HBox row = new HBox(); + row.getChildren().addAll(optionA, optionB); + HBox.setMargin(optionA, new Insets(5)); + HBox.setMargin(optionB, new Insets(5)); + return row; + } + + @SuppressWarnings("unchecked") + private Node createRangeSlider(Setting setting) { + SimpleRangeProperty rangeProperty = (SimpleRangeProperty) setting.getProperty(); + DiscreteRange range = (DiscreteRange) rangeProperty.getRange(); + List labels = (List) range.getLabels(); + List values = range.getTicks(); + RangeSlider resolutionRange = new RangeSlider<>(rangeProperty.getRange()); + resolutionRange.setShowTickMarks(true); + resolutionRange.setShowTickLabels(true); + int lowValue = getRangeSliderValue(values, labels, Config.getInstance().getSettings().minimumResolution); + resolutionRange.setLow(lowValue >= 0 ? lowValue : values.get(0)); + int highValue = getRangeSliderValue(values, labels, Config.getInstance().getSettings().maximumResolution); + resolutionRange.setHigh(highValue >= 0 ? highValue : values.get(values.size() - 1)); + resolutionRange.getLow().addListener((obs, o, n) -> saveValue(() -> { + int newV = labels.get(n.intValue()); + Field field = Settings.class.getField(rangeProperty.getLowKey()); + field.set(settings, newV); + config.save(); + })); + resolutionRange.getHigh().addListener((obs, o, n) -> saveValue(() -> { + int newV = labels.get(n.intValue()); + Field field = Settings.class.getField(rangeProperty.getHighKey()); + field.set(settings, newV); + config.save(); + })); + return resolutionRange; + } + + private int getRangeSliderValue(List values, List labels, int value) { + for (int i = 0; i < labels.size(); i++) { + int label = labels.get(i).intValue(); + if(label == value) { + return values.get(i); + } + } + return -1; + } + + private Node createFileSelector(Setting setting) { + ProgramSelectionBox programSelector = new ProgramSelectionBox(""); + programSelector.fileProperty().addListener((obs, o, n) -> saveValue(() -> { + String path = n; + Field field = Settings.class.getField(setting.getKey()); + String oldValue = (String) field.get(settings); + if (!Objects.equals(path, oldValue)) { + field.set(settings, path); + config.save(); + } + })); + StringProperty property = (StringProperty) setting.getProperty(); + programSelector.fileProperty().bindBidirectional(property); + return programSelector; + } + + private Node createDirectorySelector(Setting setting) { + DirectorySelectionBox directorySelector = new DirectorySelectionBox(""); + directorySelector.prefWidth(400); + directorySelector.fileProperty().addListener((obs, o, n) -> saveValue(() -> { + String path = n; + Field field = Settings.class.getField(setting.getKey()); + String oldValue = (String) field.get(settings); + if (!Objects.equals(path, oldValue)) { + field.set(settings, path); + config.save(); + } + })); + StringProperty property = (StringProperty) setting.getProperty(); + directorySelector.fileProperty().bindBidirectional(property); + return directorySelector; + } + + private Node createStringProperty(Setting setting) { + TextField ctrl = new TextField(); + ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> { + Field field = Settings.class.getField(setting.getKey()); + field.set(settings, newV); + config.save(); + })); + StringProperty prop = (StringProperty) setting.getProperty(); + ctrl.textProperty().bindBidirectional(prop); + return ctrl; + } + + @SuppressWarnings("unchecked") + private Node createIntegerProperty(Setting setting) { + TextField ctrl = new TextField(); + ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> { + if (!newV.matches("\\d*")) { + ctrl.setText(newV.replaceAll(PATTERN_NOT_A_DIGIT, "")); + } + if (!ctrl.getText().isEmpty()) { + Field field = Settings.class.getField(setting.getKey()); + field.set(settings, Integer.parseInt(ctrl.getText())); + config.save(); + } + })); + Property prop = setting.getProperty(); + ctrl.textProperty().bindBidirectional(prop, new NumberStringConverter()); + return ctrl; + } + + @SuppressWarnings("unchecked") + private Node createLongProperty(Setting setting) { + TextField ctrl = new TextField(); + ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> { + if (!newV.matches("\\d*")) { + ctrl.setText(newV.replaceAll(PATTERN_NOT_A_DIGIT, "")); + } + if (!ctrl.getText().isEmpty()) { + long value = Long.parseLong(ctrl.getText()); + if (setting.getConverter() != null) { + value = (long) setting.getConverter().convertFrom(value); + } + Field field = Settings.class.getField(setting.getKey()); + field.set(settings, value); + config.save(); + } + })); + Property prop = setting.getProperty(); + ctrl.textProperty().bindBidirectional(prop, new NumberStringConverter()); + return ctrl; + } + + private Node createBooleanProperty(Setting setting) { + CheckBox ctrl = new CheckBox(); + ctrl.selectedProperty().addListener((obs, oldV, newV) -> saveValue(() -> { + Field field = Settings.class.getField(setting.getKey()); + field.set(settings, newV); + config.save(); + })); + BooleanProperty prop = (BooleanProperty) setting.getProperty(); + ctrl.selectedProperty().bindBidirectional(prop); + return ctrl; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Node createComboBox(Setting setting) throws NoSuchFieldException, IllegalAccessException { + ListProperty listProp = (ListProperty) setting.getProperty(); + ComboBox comboBox = new ComboBox(listProp); + Field field = Settings.class.getField(setting.getKey()); + Object value = field.get(settings); + if (StringUtil.isNotBlank(value.toString())) { + if (setting.getConverter() != null) { + comboBox.getSelectionModel().select(setting.getConverter().convertTo(value)); + } else { + comboBox.getSelectionModel().select(value); + } + } + comboBox.valueProperty().addListener((obs, oldV, newV) -> saveValue(() -> { + if (setting.getConverter() != null) { + field.set(settings, setting.getConverter().convertFrom(newV)); + } else { + field.set(settings, newV); + } + config.save(); + })); + return comboBox; + } + + private void saveValue(Exec exe) { + try { + exe.run(); + } catch (Exception e) { + LOG.error(COULDNT_SAVE_MSG, e); + } + } + + @FunctionalInterface + private interface Exec { + public void run() throws Exception; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/IgnoreList.java b/client/src/main/java/ctbrec/ui/settings/IgnoreList.java new file mode 100644 index 00000000..73111015 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/IgnoreList.java @@ -0,0 +1,160 @@ +package ctbrec.ui.settings; + +import static javafx.scene.control.ButtonType.*; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.ModelJsonAdapter; +import ctbrec.sites.Site; +import ctbrec.ui.AutosizeAlert; +import ctbrec.ui.controls.Dialogs; +import javafx.geometry.Insets; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.SelectionMode; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.stage.FileChooser; + +public class IgnoreList extends GridPane { + + private static final Logger LOG = LoggerFactory.getLogger(IgnoreList.class); + + private ListView ignoreListView; + + private List sites; + + public IgnoreList(List sites) { + this.sites = sites; + createGui(); + loadIgnoredModels(); + } + + private void createGui() { + setHgap(10); + setVgap(10); + setPadding(new Insets(20, 10, 10, 10)); + + Label headline = new Label("Ignore List"); + headline.getStyleClass().add("settings-group-label"); + add(headline, 0, 0); + + ignoreListView = new ListView<>(); + ignoreListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + ignoreListView.addEventHandler(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.DELETE) { + removeSelectedModels(); + } + }); + add(ignoreListView, 0, 1); + GridPane.setHgrow(ignoreListView, Priority.ALWAYS); + + Button remove = new Button("Remove"); + remove.setOnAction(evt -> removeSelectedModels()); + Button exportIgnoreList = new Button("Export"); + exportIgnoreList.setOnAction(e -> exportIgnoreList()); + Button importIgnoreList = new Button("Import"); + importIgnoreList.setOnAction(e -> importIgnoreList()); + HBox buttons = new HBox(10, remove, exportIgnoreList, importIgnoreList); + add(buttons, 0, 2); + buttons.setStyle("-fx-background-color: -fx-background"); // workaround so that the buttons don't shrink + } + + private void removeSelectedModels() { + List selectedModels = ignoreListView.getSelectionModel().getSelectedItems(); + if (selectedModels.isEmpty()) { + return; + } else { + Config.getInstance().getSettings().modelsIgnored.removeAll(selectedModels); + ignoreListView.getItems().removeAll(selectedModels); + LOG.debug(Config.getInstance().getSettings().modelsIgnored.toString()); + try { + Config.getInstance().save(); + } catch (IOException e) { + LOG.warn("Couldn't save config", e); + } + } + } + + private void loadIgnoredModels() { + List ignored = Config.getInstance().getSettings().modelsIgnored; + ignoreListView.getItems().clear(); + ignoreListView.getItems().addAll(ignored); + Collections.sort(ignoreListView.getItems()); + } + + public void refresh() { + loadIgnoredModels(); + } + + private void exportIgnoreList() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Export ignore list"); + chooser.setInitialFileName("ctbrec-ignorelist.json"); + File file = chooser.showSaveDialog(null); + if (file != null) { + Moshi moshi = new Moshi.Builder().add(Model.class, new ModelJsonAdapter(sites)).build(); + Type modelListType = Types.newParameterizedType(List.class, Model.class); + JsonAdapter> adapter = moshi.adapter(modelListType); + adapter = adapter.indent(" "); + try (FileOutputStream out = new FileOutputStream(file)) { + String json = adapter.toJson(Config.getInstance().getSettings().modelsIgnored); + out.write(json.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + Dialogs.showError(getScene(), "Couldn't export ignore list", e.getLocalizedMessage(), e); + } + } + } + + private void importIgnoreList() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Import ignore list"); + File file = chooser.showOpenDialog(null); + if (file != null) { + Moshi moshi = new Moshi.Builder().add(Model.class, new ModelJsonAdapter(sites)).build(); + Type modelListType = Types.newParameterizedType(List.class, Model.class); + JsonAdapter> adapter = moshi.adapter(modelListType); + try { + byte[] fileContent = Files.readAllBytes(file.toPath()); + List ignoredModels = adapter.fromJson(new String(fileContent, StandardCharsets.UTF_8)); + boolean confirmed = true; + if (!Config.getInstance().getSettings().modelsIgnored.isEmpty()) { + String msg = "This will replace the existing ignore list! Continue?"; + AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, msg, getScene(), YES, NO); + confirm.setTitle("Import ignore list"); + confirm.setHeaderText("Overwrite ignore list"); + confirm.showAndWait(); + confirmed = confirm.getResult() == ButtonType.YES; + } + if (confirmed) { + Config.getInstance().getSettings().modelsIgnored = ignoredModels; + refresh(); + } + } catch (IOException e) { + Dialogs.showError(getScene(), "Couldn't import ignore list", e.getLocalizedMessage(), e); + } + } + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/IgnoreListDialog.java b/client/src/main/java/ctbrec/ui/settings/IgnoreListDialog.java deleted file mode 100644 index 762743cc..00000000 --- a/client/src/main/java/ctbrec/ui/settings/IgnoreListDialog.java +++ /dev/null @@ -1,96 +0,0 @@ -package ctbrec.ui.settings; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ctbrec.Config; -import ctbrec.Model; -import ctbrec.ui.controls.Dialogs; -import javafx.geometry.Insets; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Dialog; -import javafx.scene.control.ListView; -import javafx.scene.control.SelectionMode; -import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.GridPane; -import javafx.stage.Modality; -import javafx.stage.Stage; - -public class IgnoreListDialog extends Dialog { - - private static final Logger LOG = LoggerFactory.getLogger(IgnoreListDialog.class); - - private Scene parent; - private ListView ignoreList; - - public IgnoreListDialog(Scene parent) { - this.parent = parent; - createGui(); - loadIgnoredModels(); - } - - private void createGui() { - setTitle("Ignore List"); - getDialogPane().getButtonTypes().addAll(ButtonType.OK); - initModality(Modality.APPLICATION_MODAL); - setResizable(true); - InputStream icon = Dialogs.class.getResourceAsStream("/icon.png"); - Stage stage = (Stage) getDialogPane().getScene().getWindow(); - stage.getIcons().add(new Image(icon)); - if (parent != null) { - stage.getScene().getStylesheets().addAll(parent.getStylesheets()); - } - - GridPane grid = new GridPane(); - grid.setHgap(10); - grid.setVgap(10); - grid.setPadding(new Insets(20, 150, 10, 10)); - - ignoreList = new ListView<>(); - ignoreList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - ignoreList.addEventHandler(KeyEvent.KEY_PRESSED, event -> { - if (event.getCode() == KeyCode.DELETE) { - removeSelectedModels(); - } - }); - grid.add(ignoreList, 0, 0); - - Button remove = new Button("Remove"); - remove.setOnAction(evt -> removeSelectedModels()); - grid.add(remove, 0, 1); - - getDialogPane().setContent(grid); - setResizable(true); - } - - private void removeSelectedModels() { - List selectedModels = ignoreList.getSelectionModel().getSelectedItems(); - if (selectedModels.isEmpty()) { - return; - } else { - Config.getInstance().getSettings().modelsIgnored.removeAll(selectedModels); - ignoreList.getItems().removeAll(selectedModels); - LOG.debug(Config.getInstance().getSettings().modelsIgnored.toString()); - try { - Config.getInstance().save(); - } catch (IOException e) { - LOG.warn("Couldn't save config", e); - } - } - } - - private void loadIgnoredModels() { - List ignored = Config.getInstance().getSettings().modelsIgnored; - ignoreList.getItems().addAll(ignored); - Collections.sort(ignoreList.getItems()); - } -} diff --git a/client/src/main/java/ctbrec/ui/settings/PlayerSettingsDialog.java b/client/src/main/java/ctbrec/ui/settings/PlayerSettingsDialog.java deleted file mode 100644 index 225d85cf..00000000 --- a/client/src/main/java/ctbrec/ui/settings/PlayerSettingsDialog.java +++ /dev/null @@ -1,101 +0,0 @@ -package ctbrec.ui.settings; - -import java.io.IOException; -import java.io.InputStream; - -import ctbrec.Config; -import ctbrec.Settings; -import ctbrec.ui.controls.Dialogs; -import javafx.application.Platform; -import javafx.geometry.Insets; -import javafx.scene.Scene; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Dialog; -import javafx.scene.control.Label; -import javafx.scene.control.TextField; -import javafx.scene.control.Tooltip; -import javafx.scene.image.Image; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.Priority; -import javafx.stage.Modality; -import javafx.stage.Stage; - -public class PlayerSettingsDialog extends Dialog { - - private Scene parent; - private Config config; - private Settings settings; - - private TextField playerParams; - private TextField maxResolution; - - public PlayerSettingsDialog(Scene parent, Config config) { - this.parent = parent; - this.config = config; - this.settings = config.getSettings(); - - initGui(); - } - - private void initGui() { - setTitle("Player Settings"); - setHeaderText("Player Settings"); - getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - initModality(Modality.APPLICATION_MODAL); - setResizable(true); - InputStream icon = Dialogs.class.getResourceAsStream("/icon.png"); - Stage stage = (Stage) getDialogPane().getScene().getWindow(); - stage.getIcons().add(new Image(icon)); - if (parent != null) { - stage.getScene().getStylesheets().addAll(parent.getStylesheets()); - } - - GridPane grid = new GridPane(); - grid.setHgap(10); - grid.setVgap(10); - grid.setPadding(new Insets(20, 150, 10, 10)); - - grid.add(new Label("Start parameters"), 0, 0); - playerParams = new TextField(settings.mediaPlayerParams); - grid.add(playerParams, 1, 0); - getDialogPane().setContent(grid); - GridPane.setFillWidth(playerParams, true); - GridPane.setHgrow(playerParams, Priority.ALWAYS); - - Label l = new Label("Maximum resolution (0 = unlimited)"); - grid.add(l, 0, 1); - maxResolution = new TextField(Integer.toString(settings.maximumResolutionPlayer)); - Tooltip tt = new Tooltip("video height, e.g. 720 or 1080"); - l.setTooltip(tt); - maxResolution.setTooltip(tt); - grid.add(maxResolution, 1, 1); - getDialogPane().setContent(grid); - GridPane.setFillWidth(maxResolution, true); - GridPane.setHgrow(maxResolution, Priority.ALWAYS); - - Platform.runLater(playerParams::requestFocus); - - setResultConverter(dialogButton -> { - try { - if (dialogButton == ButtonType.OK) { - saveSettings(); - } - return null; - } catch (IOException e) { - return e; - } - }); - } - - public void saveSettings() throws IOException { - settings.mediaPlayerParams = playerParams.getText(); - String res = maxResolution.getText(); - if (res.matches("\\d+")) { - int newRes = Integer.parseInt(maxResolution.getText()); - if (newRes != Config.getInstance().getSettings().maximumResolutionPlayer) { - settings.maximumResolutionPlayer = newRes; - } - } - config.save(); - } -} diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 99e13575..7d3a3c52 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -1,795 +1,319 @@ package ctbrec.ui.settings; import static ctbrec.Settings.DirectoryStructure.*; -import static javafx.scene.control.ButtonType.*; +import static ctbrec.Settings.ProxyType.*; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; -import com.squareup.moshi.Types; - import ctbrec.Config; import ctbrec.Hmac; -import ctbrec.Model; import ctbrec.Settings; import ctbrec.Settings.DirectoryStructure; -import ctbrec.StringUtil; -import ctbrec.io.ModelJsonAdapter; +import ctbrec.Settings.ProxyType; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; -import ctbrec.ui.AutosizeAlert; import ctbrec.ui.SiteUiFactory; -import ctbrec.ui.controls.Dialogs; -import ctbrec.ui.controls.DirectorySelectionBox; -import ctbrec.ui.controls.ProgramSelectionBox; import ctbrec.ui.controls.range.DiscreteRange; -import ctbrec.ui.controls.range.RangeSlider; -import ctbrec.ui.sites.ConfigUI; +import ctbrec.ui.settings.api.Category; +import ctbrec.ui.settings.api.ExclusiveSelectionProperty; +import ctbrec.ui.settings.api.GigabytesConverter; +import ctbrec.ui.settings.api.Group; +import ctbrec.ui.settings.api.Preferences; +import ctbrec.ui.settings.api.Setting; +import ctbrec.ui.settings.api.SimpleDirectoryProperty; +import ctbrec.ui.settings.api.SimpleFileProperty; +import ctbrec.ui.settings.api.SimpleRangeProperty; +import ctbrec.ui.settings.api.ValueConverter; import ctbrec.ui.tabs.TabSelectionListener; +import javafx.beans.binding.BooleanExpression; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleLongProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; -import javafx.geometry.HPos; import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.control.Accordion; -import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonType; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Label; -import javafx.scene.control.RadioButton; -import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; -import javafx.scene.control.TextField; import javafx.scene.control.TextInputDialog; -import javafx.scene.control.TitledPane; -import javafx.scene.control.ToggleGroup; -import javafx.scene.control.Tooltip; -import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javafx.scene.text.Font; -import javafx.stage.FileChooser; public class SettingsTab extends Tab implements TabSelectionListener { - public static final String PATTERN_NOT_A_DIGIT = "[^\\d]"; private static final Logger LOG = LoggerFactory.getLogger(SettingsTab.class); - private static final int ONE_GIB_IN_BYTES = 1024 * 1024 * 1024; - public static final int CHECKBOX_MARGIN = 6; - private DirectorySelectionBox recordingsDirectory; - private ProgramSelectionBox postProcessing; - private TextField server; - private TextField port; - private TextField servletContext; - private TextField onlineCheckIntervalInSecs; - private TextField leaveSpaceOnDevice; - private TextField minimumLengthInSecs; - private TextField ppThreads; - private TextField userAgent; - private TextField userAgentMobile; - private TextField ffmpegParameters; - private TextField fileExtension; - private CheckBox useAuthentication = new CheckBox(); - private CheckBox useTLS = new CheckBox(); - private CheckBox chooseStreamQuality = new CheckBox(); - private CheckBox multiplePlayers = new CheckBox(); - private CheckBox updateThumbnails = new CheckBox(); - private CheckBox livePreviews = new CheckBox(); - private CheckBox showPlayerStarting = new CheckBox(); - private CheckBox removeRecordingAfterPp = new CheckBox(); - private RadioButton recordLocal; - private ProxySettingsPane proxySettingsPane; - private TextField concurrentRecordings; - private ComboBox splitAfter; - private ComboBox directoryStructure; - private ComboBox startTab; - private RangeSlider resolutionRange; + private List sites; - private Label restartLabel; - private Accordion siteConfigAccordion = new Accordion(); private Recorder recorder; + private boolean initialized = false; + private Config config; + private Settings settings; + + private SimpleStringProperty httpUserAgent; + private SimpleStringProperty httpUserAgentMobile; + private SimpleIntegerProperty overviewUpdateIntervalInSecs; + private SimpleBooleanProperty updateThumbnails; + private SimpleBooleanProperty determineResolution; + private SimpleBooleanProperty chooseStreamQuality; + private SimpleBooleanProperty livePreviews; + private SimpleListProperty startTab; + private SimpleFileProperty mediaPlayer; + private SimpleStringProperty mediaPlayerParams; + private SimpleIntegerProperty maximumResolutionPlayer; + private SimpleBooleanProperty showPlayerStarting; + private SimpleBooleanProperty singlePlayer; + private SimpleListProperty proxyType; + private SimpleStringProperty proxyHost; + private SimpleStringProperty proxyPort; + private SimpleStringProperty proxyUser; + private SimpleStringProperty proxyPassword; + private SimpleDirectoryProperty recordingsDir; + private SimpleListProperty directoryStructure; + private SimpleListProperty splitAfter; + private SimpleRangeProperty resolutionRange; + private List labels = Arrays.asList(0, 240, 360, 480, 600, 720, 960, 1080, 1440, 2160, 4320, 8640); + private List values = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + private DiscreteRange rangeValues = new DiscreteRange<>(values, labels); + private SimpleIntegerProperty concurrentRecordings; + private SimpleIntegerProperty onlineCheckIntervalInSecs; + private SimpleLongProperty leaveSpaceOnDevice; + private SimpleIntegerProperty minimumLengthInSecs; + private SimpleStringProperty ffmpegParameters; + private SimpleStringProperty fileExtension; + private SimpleStringProperty server; + private SimpleIntegerProperty port; + private SimpleStringProperty path; + private SimpleBooleanProperty requireAuthentication; + private SimpleBooleanProperty transportLayerSecurity; + private ExclusiveSelectionProperty recordLocal; + private SimpleFileProperty postProcessing; + private SimpleIntegerProperty postProcessingThreads; + private SimpleBooleanProperty removeRecordingAfterPp; + private IgnoreList ignoreList; public SettingsTab(List sites, Recorder recorder) { this.sites = sites; this.recorder = recorder; setText("Settings"); - createGui(); setClosable(false); - setRecordingMode(recordLocal.isSelected()); + config = Config.getInstance(); + settings = config.getSettings(); + } + + private void initializeProperties() { + httpUserAgent = new SimpleStringProperty(null, "httpUserAgent", settings.httpUserAgent); + httpUserAgentMobile = new SimpleStringProperty(null, "httpUserAgentMobile", settings.httpUserAgentMobile); + overviewUpdateIntervalInSecs = new SimpleIntegerProperty(null, "overviewUpdateIntervalInSecs", settings.overviewUpdateIntervalInSecs); + updateThumbnails = new SimpleBooleanProperty(null, "updateThumbnails", settings.updateThumbnails); + determineResolution = new SimpleBooleanProperty(null, "determineResolution", settings.determineResolution); + chooseStreamQuality = new SimpleBooleanProperty(null, "chooseStreamQuality", settings.chooseStreamQuality); + livePreviews = new SimpleBooleanProperty(null, "livePreviews", settings.livePreviews); + startTab = new SimpleListProperty<>(null, "startTab", FXCollections.observableList(getTabNames())); + mediaPlayer = new SimpleFileProperty(null, "mediaPlayer", settings.mediaPlayer); + mediaPlayerParams = new SimpleStringProperty(null, "mediaPlayerParams", settings.mediaPlayerParams); + maximumResolutionPlayer = new SimpleIntegerProperty(null, "maximumResolutionPlayer", settings.maximumResolutionPlayer); + showPlayerStarting = new SimpleBooleanProperty(null, "showPlayerStarting", settings.showPlayerStarting); + singlePlayer = new SimpleBooleanProperty(null, "singlePlayer", settings.singlePlayer); + proxyType = new SimpleListProperty<>(null, "proxyType", FXCollections.observableList(List.of(DIRECT, HTTP, SOCKS4, SOCKS5))); + proxyHost = new SimpleStringProperty(null, "proxyHost", settings.proxyHost); + proxyPort = new SimpleStringProperty(null, "proxyPort", settings.proxyPort); + proxyUser = new SimpleStringProperty(null, "proxyUser", settings.proxyUser); + proxyPassword = new SimpleStringProperty(null, "proxyPassword", settings.proxyPassword); + recordingsDir = new SimpleDirectoryProperty(null, "recordingsDir", settings.recordingsDir); + directoryStructure = new SimpleListProperty<>(null, "recordingsDirStructure", FXCollections.observableList(List.of(FLAT, ONE_PER_MODEL, ONE_PER_RECORDING))); + splitAfter = new SimpleListProperty<>(null, "splitRecordings", FXCollections.observableList(getSplitOptions())); + resolutionRange = new SimpleRangeProperty<>(rangeValues, "minimumResolution", "maximumResolution", settings.minimumResolution, settings.maximumResolution); + concurrentRecordings = new SimpleIntegerProperty(null, "concurrentRecordings", settings.concurrentRecordings); + onlineCheckIntervalInSecs = new SimpleIntegerProperty(null, "onlineCheckIntervalInSecs", settings.onlineCheckIntervalInSecs); + leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes)); + minimumLengthInSecs = new SimpleIntegerProperty(null, "minimumLengthInSeconds", settings.minimumLengthInSeconds); + ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs); + fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix); + server = new SimpleStringProperty(null, "httpServer", settings.httpServer); + port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort); + path = new SimpleStringProperty(null, "servletContext", settings.servletContext); + requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication); + requireAuthentication.addListener(this::requireAuthenticationChanged); + transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity); + recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote"); + postProcessing = new SimpleFileProperty(null, "postProcessing", settings.postProcessing); + postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads); + removeRecordingAfterPp = new SimpleBooleanProperty(null, "removeRecordingAfterPostProcessing", settings.removeRecordingAfterPostProcessing); } private void createGui() { - // set up main layout, 2 columns with VBoxes 50/50 - GridPane mainLayout = createGridLayout(); - mainLayout.setHgap(15); - mainLayout.setVgap(15); - mainLayout.setPadding(new Insets(15)); - ColumnConstraints cc = new ColumnConstraints(); - cc.setPercentWidth(50); - mainLayout.getColumnConstraints().setAll(cc, cc); - ScrollPane scrollPane = new ScrollPane(mainLayout); - setContent(scrollPane); - GridPane.setFillHeight(scrollPane, true); - GridPane.setFillWidth(scrollPane, true); - GridPane.setHgrow(scrollPane, Priority.ALWAYS); - GridPane.setVgrow(scrollPane, Priority.ALWAYS); - VBox leftSide = new VBox(15); - leftSide.setFillWidth(true); - VBox rightSide = new VBox(15); - rightSide.setFillWidth(true); - GridPane.setHgrow(leftSide, Priority.ALWAYS); - GridPane.setHgrow(rightSide, Priority.ALWAYS); - GridPane.setFillWidth(leftSide, true); - GridPane.setFillWidth(rightSide, true); - mainLayout.add(leftSide, 0, 1); - mainLayout.add(rightSide, 1, 1); - mainLayout.prefWidthProperty().bind(scrollPane.widthProperty()); + ignoreList = new IgnoreList(sites); + List siteCategories = new ArrayList<>(); + for (Site site : sites) { + siteCategories.add(Category.of(site.getName(), SiteUiFactory.getUi(site).getConfigUI().createConfigPanel())); + } - // restart info label - restartLabel = new Label("A restart is required to apply the changes you made!"); - restartLabel.setVisible(false); - restartLabel.setFont(Font.font(24)); - restartLabel.setTextFill(Color.RED); - mainLayout.add(restartLabel, 0, 0); - GridPane.setColumnSpan(restartLabel, 2); - GridPane.setHalignment(restartLabel, HPos.CENTER); + Preferences prefs = Preferences.of(new CtbrecPreferencesStorage(config), + Category.of("General", + Group.of("General", + Setting.of("User-Agent", httpUserAgent), + Setting.of("User-Agent mobile", httpUserAgentMobile), + Setting.of("Update overview interval (seconds)", overviewUpdateIntervalInSecs, "Update the thumbnail overviews every x seconds"), + Setting.of("Update thumbnails", updateThumbnails, "The overviews will still be updated, but the thumbnails won't be changed. This is useful for less powerful systems."), + Setting.of("Display stream resolution in overview", determineResolution), + Setting.of("Manually select stream quality", chooseStreamQuality, "Opens a dialog to select the video resolution before recording"), + Setting.of("Enable live previews (experimental)", livePreviews), + Setting.of("Start Tab", startTab), + Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance())) + ), + Group.of("Player", + Setting.of("Player", mediaPlayer), + Setting.of("Start parameters", mediaPlayerParams), + Setting.of("Maximum resolution (0 = unlimited)", maximumResolutionPlayer, "video height, e.g. 720 or 1080"), + Setting.of("Show \"Player Starting\" Message", showPlayerStarting), + Setting.of("Start only one player at a time", singlePlayer) + ) + ), + Category.of("Recorder", + Group.of("Settings", + Setting.of("Recordings Directory", recordingsDir), + Setting.of("Directory Structure", directoryStructure), + Setting.of("Split recordings after (minutes)", splitAfter).converter(SplitAfterOption.converter()), + Setting.of("Restrict Resolution", resolutionRange, "Only record streams with resolution within the given range"), + Setting.of("Concurrent Recordings (0 = unlimited)", concurrentRecordings), + Setting.of("Check online state every (seconds)", onlineCheckIntervalInSecs, "Check every x seconds, if a model came online"), + Setting.of("Leave space on device (GiB)", leaveSpaceOnDevice, "Stop recording, if the free space on the device gets below this threshold").converter(new GigabytesConverter()), + Setting.of("FFmpeg parameters", ffmpegParameters, "FFmpeg parameters to use when merging stream segments"), + Setting.of("File Extension", fileExtension, "File extension to use for recordings") + ), + Group.of("Location", + Setting.of("Record Location", recordLocal), + Setting.of("Server", server), + Setting.of("Port", port), + Setting.of("Path", path, "Leave empty, if you didn't change the servletContext in the server config"), + Setting.of("Require authentication", requireAuthentication), + Setting.of("Use Secure Communication (TLS)", transportLayerSecurity) + ) + ), + Category.of("Post-Processing", + Group.of("Post-Processing", + Setting.of("Post-Processing", postProcessing), + Setting.of("Threads", postProcessingThreads), + Setting.of("Delete recordings shorter than (secs)", minimumLengthInSecs, "Delete recordings, which are shorter than x seconds. 0 to disable"), + Setting.of("Remove recording after post-processing", removeRecordingAfterPp) + ) + ), + Category.of("Events & Actions", new ActionSettingsPanel(recorder)), + Category.of("Ignore List", ignoreList), + Category.of("Sites", siteCategories.toArray(new Category[0])), + Category.of("Proxy", + Group.of("Proxy", + Setting.of("Type", proxyType), + Setting.of("Host", proxyHost), + Setting.of("Port", proxyPort), + Setting.of("Username", proxyUser), + Setting.of("Password", proxyPassword) + ) + ) + ); + setContent(prefs.getView()); + prefs.expandTree(); - // left side - leftSide.getChildren().add(createGeneralPanel()); - leftSide.getChildren().add(createRecorderPanel()); - leftSide.getChildren().add(createRecordLocationPanel()); - //right side - rightSide.getChildren().add(siteConfigAccordion); - ActionSettingsPanel actions = new ActionSettingsPanel(this, recorder); - rightSide.getChildren().add(actions); - proxySettingsPane = new ProxySettingsPane(this); - rightSide.getChildren().add(createIgnoreListPanel()); - rightSide.getChildren().add(proxySettingsPane); - for (int i = 0; i < sites.size(); i++) { - Site site = sites.get(i); - ConfigUI siteConfig = SiteUiFactory.getUi(site).getConfigUI(); - if(siteConfig != null) { - TitledPane pane = new TitledPane(site.getName(), siteConfig.createConfigPanel()); - siteConfigAccordion.getPanes().add(pane); - } + prefs.getSetting("httpServer").ifPresent(s -> bindEnabledProperty(s, recordLocal)); + prefs.getSetting("httpPort").ifPresent(s -> bindEnabledProperty(s, recordLocal)); + prefs.getSetting("servletContext").ifPresent(s -> bindEnabledProperty(s, recordLocal)); + prefs.getSetting("requireAuthentication").ifPresent(s -> bindEnabledProperty(s, recordLocal)); + prefs.getSetting("transportLayerSecurity").ifPresent(s -> bindEnabledProperty(s, recordLocal)); + prefs.getSetting("recordingsDir").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("splitRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("minimumResolution").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("recordingsDirStructure").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("onlineCheckIntervalInSecs").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("minimumSpaceLeftInBytes").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("postProcessing").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("postProcessingThreads").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("removeRecordingAfterPostProcessing").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("minimumLengthInSeconds").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); + } + + private void bindEnabledProperty(Setting s, BooleanExpression bindTo) { + try { + s.getGui().disableProperty().bind(bindTo); + } catch (Exception e) { + LOG.error("Couldn't bind disableProperty of {}", s.getName(), e); } } - private Node createRecordLocationPanel() { - GridPane layout = createGridLayout(); - Label l = new Label("Record Location"); - int row = 0; - layout.add(l, 0, row); - ToggleGroup recordLocationToggleGroup = new ToggleGroup(); - recordLocal = new RadioButton("Local"); - RadioButton recordRemote = new RadioButton("Remote"); - recordLocal.setToggleGroup(recordLocationToggleGroup); - recordRemote.setToggleGroup(recordLocationToggleGroup); - recordLocal.setSelected(Config.getInstance().getSettings().localRecording); - recordRemote.setSelected(!recordLocal.isSelected()); - layout.add(recordLocal, 1, row); - layout.add(recordRemote, 2, row++); - recordLocationToggleGroup.selectedToggleProperty().addListener(e -> { - Config.getInstance().getSettings().localRecording = recordLocal.isSelected(); - setRecordingMode(recordLocal.isSelected()); - showRestartRequired(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(0, 0, CHECKBOX_MARGIN, 0)); - GridPane.setMargin(recordLocal, new Insets(0, 0, CHECKBOX_MARGIN, 0)); - GridPane.setMargin(recordRemote, new Insets(0, 0, CHECKBOX_MARGIN, 0)); - - layout.add(new Label("Server"), 0, row); - server = new TextField(Config.getInstance().getSettings().httpServer); - server.textProperty().addListener((ob, o, n) -> { - if(!server.getText().isEmpty()) { - Config.getInstance().getSettings().httpServer = server.getText(); - saveConfig(); - } - }); - GridPane.setFillWidth(server, true); - GridPane.setHgrow(server, Priority.ALWAYS); - GridPane.setColumnSpan(server, 2); - layout.add(server, 1, row++); - - layout.add(new Label("Port"), 0, row); - port = new TextField(Integer.toString(Config.getInstance().getSettings().httpPort)); - port.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - port.setText(newValue.replaceAll(PATTERN_NOT_A_DIGIT, "")); - } - if(!port.getText().isEmpty()) { - Config.getInstance().getSettings().httpPort = Integer.parseInt(port.getText()); - saveConfig(); - } - }); - GridPane.setFillWidth(port, true); - GridPane.setHgrow(port, Priority.ALWAYS); - GridPane.setColumnSpan(port, 2); - layout.add(port, 1, row++); - - layout.add(new Label("Path"), 0, row); - servletContext = new TextField(Config.getInstance().getSettings().servletContext); - servletContext.setPromptText("e.g. /ctbrec"); - servletContext.setTooltip(new Tooltip("Leave empty, if you didn't change the servletContext in the server config")); - servletContext.textProperty().addListener((observable, oldValue, newValue) -> { - Config.getInstance().getSettings().servletContext = servletContext.getText(); - saveConfig(); - }); - GridPane.setFillWidth(servletContext, true); - GridPane.setHgrow(servletContext, Priority.ALWAYS); - GridPane.setColumnSpan(servletContext, 2); - layout.add(servletContext, 1, row++); - - l = new Label("Require authentication"); - layout.add(l, 0, row); - useAuthentication.setSelected(Config.getInstance().getSettings().requireAuthentication); - useAuthentication.setOnAction(e -> { - Config.getInstance().getSettings().requireAuthentication = useAuthentication.isSelected(); - if(useAuthentication.isSelected()) { - byte[] key = Config.getInstance().getSettings().key; - if(key == null) { - key = Hmac.generateKey(); - Config.getInstance().getSettings().key = key; - saveConfig(); - } - TextInputDialog keyDialog = new TextInputDialog(); - keyDialog.setResizable(true); - keyDialog.setTitle("Server Authentication"); - keyDialog.setHeaderText("A key has been generated"); - keyDialog.setContentText("Add this setting to your server's config.json:\n"); - keyDialog.getEditor().setText("\"key\": " + Arrays.toString(key)); - keyDialog.getEditor().setEditable(false); - keyDialog.setWidth(800); - keyDialog.setHeight(200); - keyDialog.show(); - } - }); - GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0)); - GridPane.setMargin(useAuthentication, new Insets(4, 0, 0, 0)); - layout.add(useAuthentication, 1, row++); - - l = new Label("Use Secure Communication (TLS)"); - layout.add(l, 0, row); - useTLS.setSelected(Config.getInstance().getSettings().transportLayerSecurity); - useTLS.setOnAction(e -> { - Config.getInstance().getSettings().transportLayerSecurity = useTLS.isSelected(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0)); - GridPane.setMargin(useTLS, new Insets(4, 0, 0, 0)); - layout.add(useTLS, 1, row); - - TitledPane recordLocation = new TitledPane("Record Location", layout); - recordLocation.setCollapsible(false); - return recordLocation; + private List getTabNames() { + return getTabPane().getTabs().stream() + .map(Tab::getText) + .collect(Collectors.toList()); } - private Node createRecorderPanel() { - int row = 0; - GridPane layout = createGridLayout(); - layout.getColumnConstraints().add(new ColumnConstraints(270)); - layout.getColumnConstraints().add(new ColumnConstraints(100, 400, Double.MAX_VALUE, Priority.ALWAYS, HPos.LEFT, true)); - layout.getColumnConstraints().add(new ColumnConstraints(80)); - layout.getColumnConstraints().add(new ColumnConstraints(40)); - - layout.add(new Label("Recordings Directory"), 0, row); - recordingsDirectory = new DirectorySelectionBox(Config.getInstance().getSettings().recordingsDir); - recordingsDirectory.prefWidth(400); - recordingsDirectory.fileProperty().addListener((obs, o, n) -> { - String path = n; - if(!Objects.equals(path, Config.getInstance().getSettings().recordingsDir)) { - Config.getInstance().getSettings().recordingsDir = path; - saveConfig(); - } - }); - GridPane.setFillWidth(recordingsDirectory, true); - GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS); - GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - GridPane.setColumnSpan(recordingsDirectory, 3); - layout.add(recordingsDirectory, 1, row++); - - layout.add(new Label("Directory Structure"), 0, row); - List options = new ArrayList<>(); - options.add(FLAT); - options.add(ONE_PER_MODEL); - options.add(ONE_PER_RECORDING); - directoryStructure = new ComboBox<>(FXCollections.observableList(options)); - directoryStructure.setValue(Config.getInstance().getSettings().recordingsDirStructure); - directoryStructure.setOnAction(evt -> { - Config.getInstance().getSettings().recordingsDirStructure = directoryStructure.getValue(); - saveConfig(); - }); - GridPane.setMargin(directoryStructure, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(directoryStructure, 1, row++); - recordingsDirectory.prefWidthProperty().bind(directoryStructure.widthProperty()); - - Label l = new Label("Split recordings after (minutes)"); - layout.add(l, 0, row); + private List getSplitOptions() { List splitOptions = new ArrayList<>(); splitOptions.add(new SplitAfterOption("disabled", 0)); - if(Config.isDevMode()) { - splitOptions.add(new SplitAfterOption( "1 min", 1 * 60)); - splitOptions.add(new SplitAfterOption( "3 min", 3 * 60)); + if (Config.isDevMode()) { + splitOptions.add(new SplitAfterOption("1 min", 1 * 60)); + splitOptions.add(new SplitAfterOption("3 min", 3 * 60)); } - splitOptions.add(new SplitAfterOption( "5 min", 5 * 60)); + splitOptions.add(new SplitAfterOption("5 min", 5 * 60)); splitOptions.add(new SplitAfterOption("10 min", 10 * 60)); splitOptions.add(new SplitAfterOption("15 min", 15 * 60)); splitOptions.add(new SplitAfterOption("20 min", 20 * 60)); splitOptions.add(new SplitAfterOption("30 min", 30 * 60)); splitOptions.add(new SplitAfterOption("60 min", 60 * 60)); - splitAfter = new ComboBox<>(FXCollections.observableList(splitOptions)); - layout.add(splitAfter, 1, row++); - setSplitAfterValue(); - splitAfter.setOnAction(e -> { - Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue(); - saveConfig(); - }); - splitAfter.prefWidthProperty().bind(directoryStructure.widthProperty()); - GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - - l = new Label("Restrict Resolution"); - Tooltip tt = new Tooltip("Only record streams with resolution within the given range"); - l.setTooltip(tt); - layout.add(l, 0, row); - List labels = Arrays.asList(0, 240, 360, 480, 600, 720, 960, 1080, 1440, 2160, 4320, 8640); - List values = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); - DiscreteRange rangeValues = new DiscreteRange<>(values, labels); - resolutionRange = new RangeSlider<>(rangeValues); - resolutionRange.prefWidthProperty().bind(directoryStructure.widthProperty()); - resolutionRange.setShowTickMarks(true); - resolutionRange.setShowTickLabels(true); - resolutionRange.setLow(labels.indexOf(Config.getInstance().getSettings().minimumResolution)); - resolutionRange.setHigh(labels.indexOf(Config.getInstance().getSettings().maximumResolution)); - layout.add(resolutionRange, 1, row++); - GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setColumnSpan(resolutionRange, 3); - GridPane.setMargin(resolutionRange, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - resolutionRange.getLow().addListener((obs, oldV, newV) -> { - Config.getInstance().getSettings().minimumResolution = labels.get(newV.intValue()); - saveConfig(); - }); - resolutionRange.getHigh().addListener((obs, oldV, newV) -> { - Config.getInstance().getSettings().maximumResolution = labels.get(newV.intValue()); - saveConfig(); - }); - - - l = new Label("Concurrent Recordings (0 = unlimited)"); - layout.add(l, 0, row); - concurrentRecordings = new TextField(Integer.toString(Config.getInstance().getSettings().concurrentRecordings)); - concurrentRecordings.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - concurrentRecordings.setText(newValue.replaceAll(PATTERN_NOT_A_DIGIT, "")); - } - if (!concurrentRecordings.getText().isEmpty()) { - int newConcurrentRecordings = Integer.parseInt(concurrentRecordings.getText()); - if (newConcurrentRecordings != Config.getInstance().getSettings().concurrentRecordings) { - Config.getInstance().getSettings().concurrentRecordings = newConcurrentRecordings; - saveConfig(); - } - } - }); - concurrentRecordings.prefWidthProperty().bind(directoryStructure.widthProperty()); - layout.add(concurrentRecordings, 1, row++); - GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setColumnSpan(concurrentRecordings, 3); - GridPane.setMargin(concurrentRecordings, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - - tt = new Tooltip("Check every x seconds, if a model came online"); - l = new Label("Check online state every (seconds)"); - l.setTooltip(tt); - layout.add(l, 0, row); - onlineCheckIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().onlineCheckIntervalInSecs)); - onlineCheckIntervalInSecs.setTooltip(tt); - onlineCheckIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - onlineCheckIntervalInSecs.setText(newValue.replaceAll(PATTERN_NOT_A_DIGIT, "")); - } - if(!onlineCheckIntervalInSecs.getText().isEmpty()) { - Config.getInstance().getSettings().onlineCheckIntervalInSecs = Integer.parseInt(onlineCheckIntervalInSecs.getText()); - saveConfig(); - } - }); - GridPane.setColumnSpan(onlineCheckIntervalInSecs, 3); - GridPane.setMargin(onlineCheckIntervalInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(onlineCheckIntervalInSecs, 1, row++); - - tt = new Tooltip("Stop recording, if the free space on the device gets below this threshold"); - l = new Label("Leave space on device (GiB)"); - l.setTooltip(tt); - layout.add(l, 0, row); - long minimumSpaceLeftInBytes = Config.getInstance().getSettings().minimumSpaceLeftInBytes; - int minimumSpaceLeftInGiB = (int) (minimumSpaceLeftInBytes / ONE_GIB_IN_BYTES); - leaveSpaceOnDevice = new TextField(Integer.toString(minimumSpaceLeftInGiB)); - leaveSpaceOnDevice.setTooltip(tt); - leaveSpaceOnDevice.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - leaveSpaceOnDevice.setText(newValue.replaceAll(PATTERN_NOT_A_DIGIT, "")); - } - if(!leaveSpaceOnDevice.getText().isEmpty()) { - long spaceLeftInGiB = Long.parseLong(leaveSpaceOnDevice.getText()); - Config.getInstance().getSettings().minimumSpaceLeftInBytes = spaceLeftInGiB * ONE_GIB_IN_BYTES; - saveConfig(); - } - }); - GridPane.setColumnSpan(leaveSpaceOnDevice, 3); - GridPane.setMargin(leaveSpaceOnDevice, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(leaveSpaceOnDevice, 1, row++); - - tt = new Tooltip("Delete recordings, which are shorter than x seconds. 0 to disable."); - l = new Label("Delete recordings shorter than (secs)"); - l.setTooltip(tt); - layout.add(l, 0, row); - int minimumLengthInSeconds = Config.getInstance().getSettings().minimumLengthInSeconds; - minimumLengthInSecs = new TextField(Integer.toString(minimumLengthInSeconds)); - minimumLengthInSecs.setTooltip(tt); - minimumLengthInSecs.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - minimumLengthInSecs.setText(newValue.replaceAll(PATTERN_NOT_A_DIGIT, "")); - } - if(!minimumLengthInSecs.getText().isEmpty()) { - int minimumLength = Integer.parseInt(minimumLengthInSecs.getText()); - Config.getInstance().getSettings().minimumLengthInSeconds = minimumLength; - saveConfig(); - } - }); - GridPane.setColumnSpan(minimumLengthInSecs, 3); - GridPane.setMargin(minimumLengthInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(minimumLengthInSecs, 1, row++); - - tt = new Tooltip("FFmpeg parameters to use when merging stream segments"); - l = new Label("FFmpeg parameters"); - l.setTooltip(tt); - layout.add(l, 0, row); - String ffmpegParams = Config.getInstance().getSettings().ffmpegMergedDownloadArgs; - ffmpegParameters = new TextField(ffmpegParams); - ffmpegParameters.setTooltip(tt); - ffmpegParameters.textProperty().addListener((observable, oldValue, newValue) -> { - if(!ffmpegParameters.getText().isEmpty()) { - Config.getInstance().getSettings().ffmpegMergedDownloadArgs = ffmpegParameters.getText(); - saveConfig(); - } - }); - GridPane.setColumnSpan(ffmpegParameters, 3); - GridPane.setMargin(ffmpegParameters, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(ffmpegParameters, 1, row++); - - tt = new Tooltip("File extension to use for recordings"); - l = new Label("File Extension"); - l.setTooltip(tt); - layout.add(l, 0, row); - String fileSuffix = Config.getInstance().getSettings().ffmpegFileSuffix; - fileExtension = new TextField(fileSuffix); - fileExtension.setTooltip(tt); - fileExtension.textProperty().addListener((observable, oldValue, newValue) -> { - if(!fileExtension.getText().isEmpty()) { - Config.getInstance().getSettings().ffmpegFileSuffix = fileExtension.getText(); - saveConfig(); - } - }); - GridPane.setColumnSpan(fileExtension, 3); - GridPane.setMargin(fileExtension, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(fileExtension, 1, row++); - - layout.add(new Label("Post-Processing"), 0, row); - postProcessing = new ProgramSelectionBox(Config.getInstance().getSettings().postProcessing); - postProcessing.allowEmptyValue(); - postProcessing.fileProperty().addListener((obs, o, n) -> { - String path = n; - if(!Objects.equals(path, Config.getInstance().getSettings().postProcessing)) { - Config.getInstance().getSettings().postProcessing = path; - saveConfig(); - } - }); - GridPane.setFillWidth(postProcessing, true); - GridPane.setHgrow(postProcessing, Priority.ALWAYS); - GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(postProcessing, 1, row); - - l = new Label("PP-Threads"); - layout.add(l, 2, row); - GridPane.setMargin(l, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - GridPane.setHalignment(l, HPos.RIGHT); - ppThreads = new TextField(Integer.toString(Config.getInstance().getSettings().postProcessingThreads)); - ppThreads.prefWidth(40); - ppThreads.minWidth(40); - ppThreads.maxWidth(40); - ppThreads.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - ppThreads.setText(newValue.replaceAll(PATTERN_NOT_A_DIGIT, "")); - } - if (!ppThreads.getText().isEmpty()) { - int newPpThreads = Integer.parseInt(ppThreads.getText()); - if (newPpThreads != Config.getInstance().getSettings().postProcessingThreads) { - Config.getInstance().getSettings().postProcessingThreads = newPpThreads; - saveConfig(); - showRestartRequired(); - } - } - }); - layout.add(ppThreads, 3, row++); - - l = new Label("Remove recording after post-processing"); - layout.add(l, 0, row); - removeRecordingAfterPp.setSelected(Config.getInstance().getSettings().removeRecordingAfterPostProcessing); - removeRecordingAfterPp.setOnAction(e -> { - Config.getInstance().getSettings().removeRecordingAfterPostProcessing = removeRecordingAfterPp.isSelected(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0)); - GridPane.setMargin(removeRecordingAfterPp, new Insets(4, 0, 0, CHECKBOX_MARGIN)); - layout.add(removeRecordingAfterPp, 1, row); - - TitledPane locations = new TitledPane("Recorder", layout); - locations.setCollapsible(false); - return locations; + return splitOptions; } - private Node createIgnoreListPanel() { - GridPane layout = createGridLayout(); - Button editIgnoreList = new Button("Edit"); - editIgnoreList.setOnAction(e -> new IgnoreListDialog(editIgnoreList.getScene()).showAndWait()); - layout.add(editIgnoreList, 0, 0); - Button exportIgnoreList = new Button("Export"); - exportIgnoreList.setOnAction(e -> exportIgnoreList()); - layout.add(exportIgnoreList, 1, 0); - Button importIgnoreList = new Button("Import"); - importIgnoreList.setOnAction(e -> importIgnoreList()); - layout.add(importIgnoreList, 2, 0); - TitledPane ignoreList = new TitledPane("Ignore List", layout); - ignoreList.setCollapsible(false); - return ignoreList; - } - - private void exportIgnoreList() { - FileChooser chooser = new FileChooser(); - chooser.setTitle("Export ignore list"); - chooser.setInitialFileName("ctbrec-ignorelist.json"); - File file = chooser.showSaveDialog(null); - if (file != null) { - Moshi moshi = new Moshi.Builder().add(Model.class, new ModelJsonAdapter(sites)).build(); - Type modelListType = Types.newParameterizedType(List.class, Model.class); - JsonAdapter> adapter = moshi.adapter(modelListType); - adapter = adapter.indent(" "); - try (FileOutputStream out = new FileOutputStream(file)) { - String json = adapter.toJson(Config.getInstance().getSettings().modelsIgnored); - out.write(json.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - Dialogs.showError(getTabPane().getScene(), "Couldn't export ignore list", e.getLocalizedMessage(), e); + private void requireAuthenticationChanged(ObservableValue obs, Boolean oldV, Boolean newV) { // NOSONAR + boolean requiresAuthentication = newV; + Config.getInstance().getSettings().requireAuthentication = requiresAuthentication; + if (requiresAuthentication) { + byte[] key = Config.getInstance().getSettings().key; + if (key == null) { + key = Hmac.generateKey(); + Config.getInstance().getSettings().key = key; + saveConfig(); } + TextInputDialog keyDialog = new TextInputDialog(); + keyDialog.setResizable(true); + keyDialog.setTitle("Server Authentication"); + keyDialog.setHeaderText("A key has been generated"); + keyDialog.setContentText("Add this setting to your server's config.json:\n"); + keyDialog.getEditor().setText("\"key\": " + Arrays.toString(key)); + keyDialog.getEditor().setEditable(false); + keyDialog.setWidth(800); + keyDialog.setHeight(200); + keyDialog.show(); } } - private void importIgnoreList() { - FileChooser chooser = new FileChooser(); - chooser.setTitle("Import ignore list"); - File file = chooser.showOpenDialog(null); - if (file != null) { - Moshi moshi = new Moshi.Builder().add(Model.class, new ModelJsonAdapter(sites)).build(); - Type modelListType = Types.newParameterizedType(List.class, Model.class); - JsonAdapter> adapter = moshi.adapter(modelListType); - try { - byte[] fileContent = Files.readAllBytes(file.toPath()); - List ignoredModels = adapter.fromJson(new String(fileContent, StandardCharsets.UTF_8)); - boolean confirmed = true; - if (!Config.getInstance().getSettings().modelsIgnored.isEmpty()) { - String msg = "This will replace the existing ignore list! Continue?"; - AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, msg, getTabPane().getScene(), YES, NO); - confirm.setTitle("Import ignore list"); - confirm.setHeaderText("Overwrite ignore list"); - confirm.showAndWait(); - confirmed = confirm.getResult() == ButtonType.YES; - } - if (confirmed) { - Config.getInstance().getSettings().modelsIgnored = ignoredModels; - } - } catch (IOException e) { - Dialogs.showError(getTabPane().getScene(), "Couldn't import ignore list", e.getLocalizedMessage(), e); - } + public void saveConfig() { + try { + Config.getInstance().save(); + } catch (IOException e) { + LOG.error("Couldn't save config", e); } } - private Node createGeneralPanel() { - Settings settings = Config.getInstance().getSettings(); - GridPane layout = createGridLayout(); - int row = 0; - - layout.add(new Label("Player"), 0, row); - ProgramSelectionBox mediaPlayer = new ProgramSelectionBox(settings.mediaPlayer); - mediaPlayer.fileProperty().addListener((obs, o, n) -> { - String path = n; - if (!Objects.equals(path, settings.mediaPlayer)) { - settings.mediaPlayer = path; - saveConfig(); - } - }); - GridPane.setFillWidth(mediaPlayer, true); - GridPane.setHgrow(mediaPlayer, Priority.ALWAYS); - GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - GridPane.setColumnSpan(mediaPlayer, 2); - layout.add(mediaPlayer, 1, row); - Button mediaPlayerParamsButton = new Button("⚙"); - mediaPlayerParamsButton.setOnAction(e -> { - PlayerSettingsDialog dialog = new PlayerSettingsDialog(getTabPane().getScene(), Config.getInstance()); - Optional exception = dialog.showAndWait(); - if (exception.isPresent()) { - Dialogs.showError("Saving player parameters", "Player parameters couldn't be saved", exception.get()); - } - }); - layout.add(mediaPlayerParamsButton, 3, row++); - - Label l = new Label("Allow multiple players"); - layout.add(l, 0, row); - multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer); - multiplePlayers.setOnAction(e -> { - Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - GridPane.setHgrow(multiplePlayers, Priority.ALWAYS); - layout.add(multiplePlayers, 1, row); - - l = new Label("Show \"Player Starting\" Message"); - layout.add(l, 2, row); - showPlayerStarting.setSelected(Config.getInstance().getSettings().showPlayerStarting); - showPlayerStarting.setOnAction(e -> { - Config.getInstance().getSettings().showPlayerStarting = showPlayerStarting.isSelected(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(showPlayerStarting, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(showPlayerStarting, 3, row++); - - l = new Label("Display stream resolution in overview"); - layout.add(l, 0, row); - CheckBox loadResolution = new CheckBox(); - loadResolution.setSelected(Config.getInstance().getSettings().determineResolution); - loadResolution.setOnAction(e -> { - Config.getInstance().getSettings().determineResolution = loadResolution.isSelected(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(loadResolution, 1, row); - - l = new Label("Manually select stream quality"); - layout.add(l, 2, row); - chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality); - chooseStreamQuality.setOnAction(e -> { - Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(chooseStreamQuality, 3, row++); - - l = new Label("Enable live previews (experimental)"); - layout.add(l, 0, row); - livePreviews.setSelected(Config.getInstance().getSettings().livePreviews); - livePreviews.setOnAction(e -> { - Config.getInstance().getSettings().livePreviews = livePreviews.isSelected(); - saveConfig(); - showRestartRequired(); - }); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(livePreviews, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(livePreviews, 1, row); - - Tooltip tt = new Tooltip("The overviews will still be updated, but the thumbnails won't be changed. This is useful for less powerful systems."); - l = new Label("Update thumbnails"); - l.setTooltip(tt); - layout.add(l, 2, row); - updateThumbnails.setTooltip(tt); - updateThumbnails.setSelected(Config.getInstance().getSettings().updateThumbnails); - updateThumbnails.setOnAction(e -> { - Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(updateThumbnails, 3, row++); - - tt = new Tooltip("Update the thumbnail overviews every x seconds"); - l = new Label("Update overview interval (seconds)"); - l.setTooltip(tt); - layout.add(l, 0, row); - TextField overviewUpdateIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().overviewUpdateIntervalInSecs)); - overviewUpdateIntervalInSecs.setTooltip(tt); - overviewUpdateIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - overviewUpdateIntervalInSecs.setText(newValue.replaceAll(PATTERN_NOT_A_DIGIT, "")); - } - if(!overviewUpdateIntervalInSecs.getText().isEmpty()) { - Config.getInstance().getSettings().overviewUpdateIntervalInSecs = Integer.parseInt(overviewUpdateIntervalInSecs.getText()); - saveConfig(); - showRestartRequired(); - } - }); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(overviewUpdateIntervalInSecs, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(overviewUpdateIntervalInSecs, 1, row++); - - l = new Label("Start Tab"); - layout.add(l, 0, row); - startTab = new ComboBox<>(); - startTab.setOnAction(e -> { - Config.getInstance().getSettings().startTab = startTab.getSelectionModel().getSelectedItem(); - saveConfig(); - }); - layout.add(startTab, 1, row++); - GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setMargin(startTab, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - overviewUpdateIntervalInSecs.maxWidthProperty().bind(startTab.widthProperty()); - - l = new Label("Colors (Base / Accent)"); - layout.add(l, 0, row); - ColorSettingsPane colorSettingsPane = new ColorSettingsPane(this); - layout.add(colorSettingsPane, 1, row++); - GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setMargin(colorSettingsPane, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - - layout.add(new Label("User-Agent"), 0, row); - userAgent = new TextField(Config.getInstance().getSettings().httpUserAgent); - userAgent.textProperty().addListener((observable, oldValue, newValue) -> { - if(!userAgent.getText().isEmpty()) { - Config.getInstance().getSettings().httpUserAgent = userAgent.getText(); - saveConfig(); - } - }); - GridPane.setColumnSpan(userAgent, 3); - GridPane.setMargin(userAgent, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(userAgent, 1, row++); - - layout.add(new Label("User-Agent mobile"), 0, row); - userAgentMobile = new TextField(Config.getInstance().getSettings().httpUserAgentMobile); - userAgentMobile.textProperty().addListener((observable, oldValue, newValue) -> { - if(!userAgentMobile.getText().isEmpty()) { - Config.getInstance().getSettings().httpUserAgentMobile = userAgentMobile.getText(); - saveConfig(); - } - }); - GridPane.setColumnSpan(userAgentMobile, 3); - GridPane.setMargin(userAgentMobile, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(userAgentMobile, 1, row); - - TitledPane general = new TitledPane("General", layout); - general.setCollapsible(false); - return general; - } - - private void setSplitAfterValue() { - int value = Config.getInstance().getSettings().splitRecordings; - for (SplitAfterOption option : splitAfter.getItems()) { - if(option.getValue() == value) { - splitAfter.getSelectionModel().select(option); - } + @Override + public void selected() { + if (!initialized) { + initializeProperties(); + createGui(); + initialized = true; } + ignoreList.refresh(); } - void showRestartRequired() { - restartLabel.setVisible(true); + @Override + public void deselected() { + saveConfig(); } public static GridPane createGridLayout() { @@ -800,51 +324,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { return layout; } - private void setRecordingMode(boolean local) { - server.setDisable(local); - port.setDisable(local); - servletContext.setDisable(local); - useAuthentication.setDisable(local); - useTLS.setDisable(local); - recordingsDirectory.setDisable(!local); - splitAfter.setDisable(!local); - resolutionRange.setDisable(!local); - directoryStructure.setDisable(!local); - onlineCheckIntervalInSecs.setDisable(!local); - leaveSpaceOnDevice.setDisable(!local); - postProcessing.setDisable(!local); - minimumLengthInSecs.setDisable(!local); - concurrentRecordings.setDisable(!local); - ppThreads.setDisable(!local); - } - - @Override - public void selected() { - if(startTab.getItems().isEmpty()) { - for(Tab tab : getTabPane().getTabs()) { - startTab.getItems().add(tab.getText()); - } - } - String startTabName = Config.getInstance().getSettings().startTab; - if(StringUtil.isNotBlank(startTabName)) { - startTab.getSelectionModel().select(startTabName); - } - } - - @Override - public void deselected() { - saveConfig(); - } - - public void saveConfig() { - if(proxySettingsPane != null) { - proxySettingsPane.saveConfig(); - } - try { - Config.getInstance().save(); - } catch (IOException e) { - LOG.error("Couldn't save config", e); - } + void showRestartRequired() { + // TODO restartLabel.setVisible(true); } public static class SplitAfterOption { @@ -865,5 +346,39 @@ public class SettingsTab extends Tab implements TabSelectionListener { public String toString() { return label; } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + value; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SplitAfterOption other = (SplitAfterOption) obj; + return value == other.value; + } + + public static ValueConverter converter() { + return new ValueConverter() { + @Override + public Integer convertFrom(Object splitAfterOption) { + return ((SplitAfterOption) splitAfterOption).getValue(); + } + + @Override + public SplitAfterOption convertTo(Object integer) { + return new SplitAfterOption(integer.toString(), (Integer) integer); + } + }; + } } } diff --git a/client/src/main/java/ctbrec/ui/settings/api/Category.java b/client/src/main/java/ctbrec/ui/settings/api/Category.java new file mode 100644 index 00000000..df43a3b7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Category.java @@ -0,0 +1,153 @@ +package ctbrec.ui.settings.api; + +import static java.util.Optional.*; + +import java.util.function.Supplier; + +import ctbrec.StringUtil; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.Pane; + +public class Category { + + protected String name; + protected Group[] groups; + protected Category[] subCategories; + protected Node gui; + + private Category(String name, Group... groups) { + this.name = name; + this.groups = groups; + } + + public Category(String name, Category[] subCategories) { + this.name = name; + this.subCategories = subCategories; + groups = new Group[0]; + } + + public Category(String name, Node gui) { + this.name = name; + this.gui = gui; + groups = new Group[0]; + } + + public static Category of(String name, Setting... settings) { + return new Category(name, Group.of(settings)); + } + + public static Category of(String name, Group... groups) { + return new Category(name, groups); + } + + public static Category of(String name, Category... subCategories) { + return new Category(name, subCategories); + } + + public static Category of(String name, Node gui) { + return new Category(name, gui); + } + + String getName() { + return name; + } + + Group[] getGroups() { + return groups; + } + + Category[] getSubCategories() { + return subCategories; + } + + boolean hasGroups() { + return groups != null && groups.length > 0 && !groups[0].isDefault(); + } + + boolean hasSubCategories() { + return subCategories != null && subCategories.length > 0; + } + + Node getGuiOrElse(Supplier guiFactory) { + if (gui == null) { + gui = guiFactory.get(); + } + return gui; + } + + @Override + public String toString() { + return name; + } + + public boolean contains(String filter) { + if (StringUtil.isBlank(filter)) { + return true; + } + String q = filter.toLowerCase().trim(); + if(hasGroups() || hasSubCategories()) { + return name.toLowerCase().contains(q) + | groupsContains(q) + | subCategoriesContains(q); + } else { + return name.toLowerCase().contains(q) + | guiContains(q); + } + + } + + private boolean subCategoriesContains(String filter) { + boolean contains = false; + if (subCategories != null) { + for (Category category : subCategories) { + if (category.contains(filter)) { + contains = true; + } + } + } + return contains; + } + + private boolean groupsContains(String filter) { + boolean contains = false; + if (groups != null) { + for (Group group : groups) { + if (group.contains(filter)) { + contains = true; + } + } + } + return contains; + } + + private boolean guiContains(String filter) { + if (gui != null) { + return nodeContains(gui, filter); + } + return false; + } + + private boolean nodeContains(Node node, String filter) { + boolean contains = false; + if (node instanceof Pane) { + Pane pane = (Pane) node; + for (Node child : pane.getChildren()) { + contains |= nodeContains(child, filter); + } + } + + if (node instanceof Label) { + Label lbl = (Label) node; + contains |= lbl.getText().toLowerCase().contains(filter); + contains |= ofNullable(lbl.getTooltip()).map(Tooltip::getText).orElse("").toLowerCase().contains(filter); + } + if (node instanceof Control) { + contains |= ofNullable(((Control) node).getTooltip()).map(Tooltip::getText).orElse("").toLowerCase().contains(filter); + } + contains |= node.toString().toLowerCase().contains(filter); + return contains; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/ExclusiveSelectionProperty.java b/client/src/main/java/ctbrec/ui/settings/api/ExclusiveSelectionProperty.java new file mode 100644 index 00000000..fd367819 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/ExclusiveSelectionProperty.java @@ -0,0 +1,23 @@ +package ctbrec.ui.settings.api; + +import javafx.beans.property.SimpleBooleanProperty; + +public class ExclusiveSelectionProperty extends SimpleBooleanProperty { + + private String optionA; + private String optionB; + + public ExclusiveSelectionProperty(Object bean, String name, boolean value, String optionA, String optionB) { + super(bean, name, value); + this.optionA = optionA; + this.optionB = optionB; + } + + public String getOptionA() { + return optionA; + } + + public String getOptionB() { + return optionB; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/GigabytesConverter.java b/client/src/main/java/ctbrec/ui/settings/api/GigabytesConverter.java new file mode 100644 index 00000000..f007e20d --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/GigabytesConverter.java @@ -0,0 +1,19 @@ +package ctbrec.ui.settings.api; + +public class GigabytesConverter implements ValueConverter { + + private static final int ONE_GIB_IN_BYTES = 1024 * 1024 * 1024; + + @Override + public Object convertTo(Object a) { + long input = (long) a; + return input / ONE_GIB_IN_BYTES; + } + + @Override + public Object convertFrom(Object b) { + long spaceLeftInGiB = (long) b; + return spaceLeftInGiB * ONE_GIB_IN_BYTES; + } + +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/Group.java b/client/src/main/java/ctbrec/ui/settings/api/Group.java new file mode 100644 index 00000000..d3af7040 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Group.java @@ -0,0 +1,51 @@ +package ctbrec.ui.settings.api; + +import java.util.Objects; + +public class Group { + + public static final String DEFAULT = "default"; + private String name; + private Setting[] settings; + + private Group(String name, Setting...settings) { + this.name = name; + this.settings = settings; + } + + public static Group of(Setting...settings) { + return new Group(DEFAULT, settings); + } + + public static Group of(String name, Setting...settings) { + return new Group(name, settings); + } + + String getName() { + return name; + } + + Setting[] getSettings() { + return settings; + } + + boolean isDefault() { + return Objects.equals(name, DEFAULT); + } + + public boolean contains(String filter) { + return name.toLowerCase().contains(filter) | settingsContain(filter); + } + + private boolean settingsContain(String filter) { + boolean contains = false; + if (settings != null) { + for (Setting setting : settings) { + if (setting.contains(filter)) { + contains = true; + } + } + } + return contains; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/HighlightingSupport.java b/client/src/main/java/ctbrec/ui/settings/api/HighlightingSupport.java new file mode 100644 index 00000000..ab14d273 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/HighlightingSupport.java @@ -0,0 +1,89 @@ +package ctbrec.ui.settings.api; + +import static java.util.Optional.*; + +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.TextInputControl; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.Pane; + +class HighlightingSupport { + + private static final String CSS_HIGHLIGHT_CLASS = "setting-highlighted"; + + private HighlightingSupport() {} + + static void highlightMatches(Category cat, String filter) { + Node node = cat.getGuiOrElse(Label::new); + highlightMatchess(node, filter); + if(cat.hasSubCategories()) { + for (Category sub : cat.getSubCategories()) { + highlightMatches(sub, filter); + } + } + } + + static void highlightMatchess(Node node, String filter) { + boolean contains = false; + if (node instanceof Pane) { + Pane pane = (Pane) node; + for (Node child : pane.getChildren()) { + highlightMatchess(child, filter); + } + } + + if (node instanceof Label) { + Label lbl = (Label) node; + contains |= lbl.getText().toLowerCase().contains(filter); + contains |= ofNullable(lbl.getTooltip()).map(Tooltip::getText).orElse("").toLowerCase().contains(filter); + contains |= labelControlContains(lbl, filter); + + if (contains) { + if (!node.getStyleClass().contains(CSS_HIGHLIGHT_CLASS)) { + node.getStyleClass().add(CSS_HIGHLIGHT_CLASS); + } + } else { + node.getStyleClass().remove(CSS_HIGHLIGHT_CLASS); + } + } + } + + private static boolean labelControlContains(Label lbl, String filter) { + boolean contains = false; + if (lbl.labelForProperty().get() != null) { + Node labeledNode = lbl.labelForProperty().get(); + contains |= labeledNode.toString().toLowerCase().contains(filter); + if (labeledNode instanceof Control) { + contains |= ofNullable(((Control) labeledNode).getTooltip()).map(Tooltip::getText).orElse("").toLowerCase().contains(filter); + } + if (labeledNode instanceof TextInputControl) { + contains |= ofNullable(((TextInputControl) labeledNode).getText()).orElse("").toLowerCase().contains(filter); + } + } + return contains; + } + + static void removeHighlights(Category cat) { + Node node = cat.getGuiOrElse(Label::new); + removeHighlights(node); + if(cat.hasSubCategories()) { + for (Category sub : cat.getSubCategories()) { + removeHighlights(sub); + } + } + } + + static void removeHighlights(Node gui) { + if (gui != null) { + if (gui instanceof Pane) { + Pane p = (Pane) gui; + for (Node n : p.getChildren()) { + removeHighlights(n); + } + } + gui.getStyleClass().remove(CSS_HIGHLIGHT_CLASS); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/Preferences.css b/client/src/main/java/ctbrec/ui/settings/api/Preferences.css new file mode 100644 index 00000000..e3d88a47 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Preferences.css @@ -0,0 +1,8 @@ +.settings-group-label { + -fx-font-size: 1.6em; +} + +.setting-highlighted { + -fx-border-color: -fx-accent; + -fx-border-width: 3; +} \ No newline at end of file diff --git a/client/src/main/java/ctbrec/ui/settings/api/Preferences.java b/client/src/main/java/ctbrec/ui/settings/api/Preferences.java new file mode 100644 index 00000000..9b096406 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Preferences.java @@ -0,0 +1,240 @@ +package ctbrec.ui.settings.api; + +import static java.util.Optional.*; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.ui.controls.SearchBox; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.geometry.VPos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +public class Preferences { + + private static final Logger LOG = LoggerFactory.getLogger(Preferences.class); + + private Category[] categories; + + private TreeView categoryTree; + + private Preferences(PreferencesStorage preferencesStorage, Category...categories) { + this.categories = categories; + for (Category category : categories) { + assignPreferencesStorage(category, preferencesStorage); + } + } + + private void assignPreferencesStorage(Category cat, PreferencesStorage preferencesStorage) { + if(cat.hasSubCategories()) { + for (Category sub : cat.getSubCategories()) { + assignPreferencesStorage(sub, preferencesStorage); + } + } else { + for (Group group : cat.getGroups()) { + for (Setting setting : group.getSettings()) { + setting.setPreferencesStorage(preferencesStorage); + } + } + } + } + + public static Preferences of(PreferencesStorage preferencesStorage, Category... categories) { + return new Preferences(preferencesStorage, categories); + } + + public void save() { + throw new RuntimeException("save not implemented"); + } + + Category[] getCategories() { + return categories; + } + + public Node getView() { + SearchBox search = new SearchBox(true); + search.textProperty().addListener(this::filterTree); + TreeItem categoryTreeItems = createCategoryTree(categories, new TreeItem<>(), null); + categoryTree = new TreeView<>(categoryTreeItems); + categoryTree.showRootProperty().set(false); + VBox leftSide = new VBox(search, categoryTree); + VBox.setVgrow(categoryTree, Priority.ALWAYS); + VBox.setMargin(search, new Insets(2)); + VBox.setMargin(categoryTree, new Insets(2)); + + BorderPane main = new BorderPane(); + main.setLeft(leftSide); + main.setCenter(new Label("Center")); + BorderPane.setMargin(leftSide, new Insets(2)); + + categoryTree.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> { + if (newV != null) { + Category cat = newV.getValue(); + Node gui = cat.getGuiOrElse(() -> createGui(cat)); + BorderPane.setMargin(gui, new Insets(10)); + main.setCenter(gui); + } + }); + categoryTree.getSelectionModel().select(0); + return main; + } + + private void filterTree(ObservableValue obs, String oldV, String newV) { + String q = ofNullable(newV).orElse("").toLowerCase().trim(); + TreeItem filteredCategoryTree = createCategoryTree(categories, new TreeItem<>(), q); + categoryTree.setRoot(filteredCategoryTree); + expandAll(categoryTree.getRoot()); + + TreeItem parent = categoryTree.getRoot(); + while (!parent.getChildren().isEmpty()) { + parent = parent.getChildren().get(0); + categoryTree.getSelectionModel().select(parent); + } + + for (Category category : categories) { + if (q.length() > 2) { + HighlightingSupport.highlightMatches(category, q); + } else { + HighlightingSupport.removeHighlights(category); + } + } + } + + private void expandAll(TreeItem treeItem) { + treeItem.setExpanded(true); + for (TreeItem child : treeItem.getChildren()) { + expandAll(child); + } + } + + private Node createGui(Category cat) { + try { + if (cat.hasSubCategories()) { + return new Label(cat.getName()); + } else if(cat.hasGroups()) { + return createPaneWithGroups(cat); + } else { + return createGrid(cat.getGroups()[0].getSettings()); + } + } catch(Exception e) { + LOG.error("Error creating the GUI", e); + return new Label(e.getLocalizedMessage()); + } + } + + private Node createPaneWithGroups(Category cat) throws Exception { + VBox pane = new VBox(); + for (Group grp : cat.getGroups()) { + Label groupLabel = new Label(grp.getName()); + groupLabel.getStyleClass().add("settings-group-label"); + VBox.setMargin(groupLabel, new Insets(20, 0, 10, 20)); + pane.getChildren().add(groupLabel); + Node parameterGrid = createGrid(grp.getSettings()); + pane.getChildren().add(parameterGrid); + VBox.setMargin(parameterGrid, new Insets(0, 0, 0, 40)); + } + return pane; + } + + private Node createGrid(Setting[] settings) throws Exception { + GridPane pane = new GridPane(); + int row = 0; + for (Setting setting : settings) { + Node node = setting.getGui(); + Label label = new Label(setting.getName()); + label.setMinHeight(34); + label.labelForProperty().set(node); + label.setTooltip(new Tooltip(setting.getName())); + pane.addRow(row++, label, node); + GridPane.setVgrow(label, Priority.ALWAYS); + GridPane.setValignment(label, VPos.CENTER); + GridPane.setMargin(node, new Insets(5, 0, 5, 10)); + GridPane.setValignment(node, VPos.CENTER); + GridPane.setHgrow(node, Priority.ALWAYS); + } + return pane; + } + + /** + * Creates a tree of the given categories. Filters out categories, which don't match the filter + * @param filter may be null + */ + private TreeItem createCategoryTree(Category[] categories, TreeItem parent, String filter) { + for (Category category : categories) { + TreeItem child = new TreeItem<>(category); + if (category.hasSubCategories()) { + createCategoryTree(category.getSubCategories(), child, filter); + if (!child.getChildren().isEmpty()) { + parent.getChildren().add(child); + } + } else if(category.contains(filter)) { + parent.getChildren().add(child); + } + } + return parent; + } + + public void expandTree() { + expandAll(categoryTree.getRoot()); + } + + public void traverse(Consumer visitor) { + for (Category category : categories) { + visit(category, visitor); + } + } + + private void visit(Category cat, Consumer visitor) { + if (cat.hasGroups()) { + for (Group group : cat.getGroups()) { + for (Setting setting : group.getSettings()) { + visitor.accept(setting); + } + } + } + if (cat.hasSubCategories()) { + for (Category subcat : cat.getSubCategories()) { + visit(subcat, visitor); + } + } + } + + public Optional getSetting(String key) { + SettingSearchVisitor search = new SettingSearchVisitor(key); + traverse(search); + return search.getResult(); + } + + private class SettingSearchVisitor implements Consumer { + Optional result = Optional.empty(); + private String key; + + public SettingSearchVisitor(String key) { + this.key = key; + } + + @Override + public void accept(Setting s) { + if (Objects.equals(key, ofNullable(s.getKey()).orElse(""))) { + result = Optional.of(s); + } + } + + public Optional getResult() { + return result; + } + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/PreferencesStorage.java b/client/src/main/java/ctbrec/ui/settings/api/PreferencesStorage.java new file mode 100644 index 00000000..c380eaa1 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/PreferencesStorage.java @@ -0,0 +1,13 @@ +package ctbrec.ui.settings.api; + +import java.io.IOException; + +import javafx.scene.Node; + +public interface PreferencesStorage { + + void save(Preferences preferences) throws IOException; + void load(Preferences preferences); + + Node createGui(Setting setting) throws Exception; +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/Setting.java b/client/src/main/java/ctbrec/ui/settings/api/Setting.java new file mode 100644 index 00000000..98acb0a6 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Setting.java @@ -0,0 +1,110 @@ +package ctbrec.ui.settings.api; + +import static java.util.Optional.*; + +import ctbrec.StringUtil; +import javafx.beans.property.Property; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Tooltip; + +public class Setting { + + private String name; + private String tooltip; + private Property property; + private Node gui; + private PreferencesStorage preferencesStorage; + private boolean needsRestart = false; + private ValueConverter converter; + + protected Setting(String name, Property property) { + this.name = name; + this.property = property; + } + + protected Setting(String name, Node gui) { + this.name = name; + this.gui = gui; + } + + public static Setting of(String name, Property property) { + return new Setting(name, property); + } + + public static Setting of(String name, Property property, String tooltip) { + Setting setting = new Setting(name, property); + setting.tooltip = tooltip; + return setting; + } + + public static Setting of(String name, Node gui) { + Setting setting = new Setting(name, gui); + return setting; + } + + public String getName() { + return name; + } + + public String getKey() { + if (getProperty() == null) { + return ""; + } else { + String key = getProperty().getName(); + if (StringUtil.isBlank(key)) { + throw new IllegalStateException("Name for property of setting [" + name + "] is null"); + } + return key; + } + } + + public String getTooltip() { + return tooltip; + } + + public Setting needsRestart() { + needsRestart = true; + return this; + } + + public boolean doesNeedRestart() { + return needsRestart; + } + + @SuppressWarnings("rawtypes") + public Property getProperty() { + return property; + } + + public Node getGui() throws Exception { + if (gui == null) { + gui = preferencesStorage.createGui(this); + if (gui instanceof Control && StringUtil.isNotBlank(tooltip)) { + Control control = (Control) gui; + control.setTooltip(new Tooltip(tooltip)); + } + } + return gui; + } + + public void setPreferencesStorage(PreferencesStorage preferencesStorage) { + this.preferencesStorage = preferencesStorage; + } + + public boolean contains(String filter) { + boolean contains = name.toLowerCase().contains(filter) + || ofNullable(tooltip).orElse("").toLowerCase().contains(filter) + || ofNullable(property).map(Property::getValue).map(Object::toString).orElse("").toLowerCase().contains(filter); + return contains; + } + + public Setting converter(ValueConverter converter) { + this.converter = converter; + return this; + } + + public ValueConverter getConverter() { + return converter; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/SimpleDirectoryProperty.java b/client/src/main/java/ctbrec/ui/settings/api/SimpleDirectoryProperty.java new file mode 100644 index 00000000..0fd61c3b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/SimpleDirectoryProperty.java @@ -0,0 +1,11 @@ +package ctbrec.ui.settings.api; + +import javafx.beans.property.SimpleStringProperty; + +public class SimpleDirectoryProperty extends SimpleStringProperty { + + public SimpleDirectoryProperty(Object bean, String name, String initialValue) { + super(bean, name, initialValue); + } + +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/SimpleFileProperty.java b/client/src/main/java/ctbrec/ui/settings/api/SimpleFileProperty.java new file mode 100644 index 00000000..837ffd3e --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/SimpleFileProperty.java @@ -0,0 +1,11 @@ +package ctbrec.ui.settings.api; + +import javafx.beans.property.SimpleStringProperty; + +public class SimpleFileProperty extends SimpleStringProperty { + + public SimpleFileProperty(Object bean, String name, String initialValue) { + super(bean, name, initialValue); + } + +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/SimpleRangeProperty.java b/client/src/main/java/ctbrec/ui/settings/api/SimpleRangeProperty.java new file mode 100644 index 00000000..238243eb --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/SimpleRangeProperty.java @@ -0,0 +1,43 @@ +package ctbrec.ui.settings.api; + +import ctbrec.ui.controls.range.Range; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +public class SimpleRangeProperty extends SimpleObjectProperty { + + private Range range; + private SimpleObjectProperty lowProperty; + private SimpleObjectProperty highProperty; + private String lowKey; + private String highKey; + + public SimpleRangeProperty(Range range, String lowKey, String highKey, T low, T high) { + super(null, lowKey); + this.range = range; + this.lowKey = lowKey; + this.highKey = highKey; + lowProperty = new SimpleObjectProperty<>(low); + highProperty = new SimpleObjectProperty<>(high); + } + + public Range getRange() { + return range; + } + + public ObjectProperty lowProperty() { + return lowProperty; + } + + public ObjectProperty highProperty() { + return highProperty; + } + + public String getLowKey() { + return lowKey; + } + + public String getHighKey() { + return highKey; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/ValueConverter.java b/client/src/main/java/ctbrec/ui/settings/api/ValueConverter.java new file mode 100644 index 00000000..86764ffc --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/ValueConverter.java @@ -0,0 +1,7 @@ +package ctbrec.ui.settings.api; + +public interface ValueConverter { + + Object convertTo(Object a); + Object convertFrom(Object b); +} diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java index 8270d55b..389ee915 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java @@ -29,11 +29,9 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane layout = SettingsTab.createGridLayout(); Settings settings = Config.getInstance().getSettings(); - Label l = new Label("Active"); - layout.add(l, 0, row); CheckBox enabled = new CheckBox(); enabled.setSelected(!settings.disabledSites.contains(myFreeCams.getName())); - enabled.setOnAction((e) -> { + enabled.setOnAction(e -> { if(enabled.isSelected()) { settings.disabledSites.remove(myFreeCams.getName()); } else { @@ -42,10 +40,13 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { save(); }); GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + Label l = new Label("Active"); + l.labelForProperty().set(enabled); + layout.add(l, 0, row); layout.add(enabled, 1, row++); - layout.add(new Label("MyFreeCams User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().mfcUsername); + username.setId("mfcUsername"); username.setPrefWidth(300); username.textProperty().addListener((ob, o, n) -> { if(!n.equals(Config.getInstance().getSettings().mfcUsername)) { @@ -57,9 +58,11 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); + l = new Label("MyFreeCams User"); + l.labelForProperty().set(username); + layout.add(l, 0, row); layout.add(username, 1, row++); - layout.add(new Label("MyFreeCams Password"), 0, row); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().mfcPassword); password.textProperty().addListener((ob, o, n) -> { @@ -72,13 +75,15 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); + l = new Label("MyFreeCams Password"); + l.labelForProperty().set(password); + layout.add(l, 0, row); layout.add(password, 1, row++); Button createAccount = new Button("Create new Account"); createAccount.setOnAction(e -> DesktopIntegration.open(myFreeCams.getAffiliateLink())); layout.add(createAccount, 1, row++); - layout.add(new Label("MyFreeCams Base URL"), 0, row); TextField baseUrl = new TextField(); baseUrl.setText(Config.getInstance().getSettings().mfcBaseUrl); baseUrl.textProperty().addListener((ob, o, n) -> { @@ -88,6 +93,9 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setFillWidth(baseUrl, true); GridPane.setHgrow(baseUrl, Priority.ALWAYS); GridPane.setColumnSpan(baseUrl, 2); + l = new Label("MyFreeCams Base URL"); + l.labelForProperty().set(baseUrl); + layout.add(l, 0, row); layout.add(baseUrl, 1, row); GridPane.setColumnSpan(createAccount, 2); diff --git a/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java index 5ce7396d..48faf307 100644 --- a/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java @@ -39,10 +39,11 @@ import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.ResumeAction; import ctbrec.ui.action.StopRecordingAction; -import ctbrec.ui.controls.AutoFillTextField; 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 javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringPropertyBase; @@ -254,7 +255,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { modelLabel.setPadding(new Insets(5, 0, 0, 0)); ObservableList suggestions = FXCollections.observableArrayList(); sites.forEach(site -> suggestions.add(site.getClass().getSimpleName())); - model = new AutoFillTextField(suggestions); + model = new AutoFillTextField(new ObservableListSuggester(suggestions)); model.setPrefWidth(600); model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/"); model.onActionHandler(this::addModel); diff --git a/client/src/main/resources/logback.xml b/client/src/main/resources/logback.xml index b49b6ff7..4d7ed7bb 100644 --- a/client/src/main/resources/logback.xml +++ b/client/src/main/resources/logback.xml @@ -9,17 +9,24 @@ - - + ctbrec.log - false - - DEBUG - + true + + DEBUG + - %date %level [%thread] %logger{10} [%file:%line] %msg%n - + %date %level [%thread] %logger{10} [%file:%line] %msg%n + + ctbrec.%i.log + 1 + 3 + + + 5MB + diff --git a/common/pom.xml b/common/pom.xml index fa2962d0..ac6c1fdd 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 3.8.0 + 3.8.1 ../master diff --git a/common/src/main/java/ctbrec/Java.java b/common/src/main/java/ctbrec/Java.java index 3d6d2ec1..b62603de 100644 --- a/common/src/main/java/ctbrec/Java.java +++ b/common/src/main/java/ctbrec/Java.java @@ -17,6 +17,12 @@ public class Java { return 11; case "12": return 12; + case "13": + return 13; + case "14": + return 14; + case "15": + return 15; default: return 0; } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 2c2d69f3..e62e54d6 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -70,7 +70,7 @@ public class Settings { public boolean livePreviews = false; public boolean localRecording = true; public int minimumResolution = 0; - public int maximumResolution = 8160; + public int maximumResolution = 8640; public int maximumResolutionPlayer = 0; public String mediaPlayer = "/usr/bin/mpv"; public String mediaPlayerParams = ""; diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java index 8f554f9b..ffb4d07b 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java @@ -108,7 +108,7 @@ public class BongaCams extends AbstractSite { @Override public HttpClient getHttpClient() { - if(httpClient == null) { + if (httpClient == null) { httpClient = new BongaCamsHttpClient(); } return httpClient; @@ -116,7 +116,7 @@ public class BongaCams extends AbstractSite { @Override public void shutdown() { - if(httpClient != null) { + if (httpClient != null) { httpClient.shutdown(); } } diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4.java b/common/src/main/java/ctbrec/sites/cam4/Cam4.java index 0c169cdf..f099bfcd 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4.java @@ -72,7 +72,7 @@ public class Cam4 extends AbstractSite { @Override public HttpClient getHttpClient() { - if(httpClient == null) { + if (httpClient == null) { httpClient = new Cam4HttpClient(); } return httpClient; @@ -80,7 +80,9 @@ public class Cam4 extends AbstractSite { @Override public void shutdown() { - getHttpClient().shutdown(); + if (httpClient != null) { + httpClient.shutdown(); + } } @Override diff --git a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java index f52897d6..05f85bc1 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -90,7 +90,7 @@ public class Camsoda extends AbstractSite { @Override public HttpClient getHttpClient() { - if(httpClient == null) { + if (httpClient == null) { httpClient = new CamsodaHttpClient(); } return httpClient; @@ -103,7 +103,7 @@ public class Camsoda extends AbstractSite { @Override public void shutdown() { - if(httpClient != null) { + if (httpClient != null) { httpClient.shutdown(); } } diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index be291b39..8b2f087a 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -89,7 +89,7 @@ public class Chaturbate extends AbstractSite { @Override public HttpClient getHttpClient() { - if(httpClient == null) { + if (httpClient == null) { httpClient = new ChaturbateHttpClient(); } return httpClient; @@ -97,7 +97,9 @@ public class Chaturbate extends AbstractSite { @Override public void shutdown() { - getHttpClient().shutdown(); + if (httpClient != null) { + httpClient.shutdown(); + } } @Override diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java index fbf73ab8..140a7e3e 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java @@ -73,7 +73,7 @@ public class Fc2Live extends AbstractSite { @Override public HttpClient getHttpClient() { - if(httpClient == null) { + if (httpClient == null) { httpClient = new Fc2HttpClient(); } return httpClient; @@ -85,7 +85,7 @@ public class Fc2Live extends AbstractSite { @Override public void shutdown() { - if(httpClient != null) { + if (httpClient != null) { httpClient.shutdown(); } } diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java index 8346a1f6..dd4e2925 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java @@ -89,7 +89,7 @@ public class MyFreeCams extends AbstractSite { @Override public MyFreeCamsHttpClient getHttpClient() { - if(httpClient == null) { + if (httpClient == null) { httpClient = new MyFreeCamsHttpClient(); } return httpClient; @@ -97,7 +97,9 @@ public class MyFreeCams extends AbstractSite { @Override public void shutdown() { - httpClient.shutdown(); + if (httpClient != null) { + httpClient.shutdown(); + } } @Override diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index b78816fa..ade01494 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -455,17 +455,19 @@ public class MyFreeCamsClient { } private Message parseMessage(StringBuilder msgBuffer) throws UnsupportedEncodingException { - if (msgBuffer.length() < 4) { + int packetLengthBytes = 6; + if (msgBuffer.length() < packetLengthBytes) { // packet size not transmitted completely return null; } else { try { - int packetLength = Integer.parseInt(msgBuffer.substring(0, 4)); - if (packetLength > msgBuffer.length() - 4) { + int packetLength = Integer.parseInt(msgBuffer.substring(0, packetLengthBytes)); + if (packetLength > msgBuffer.length() - packetLengthBytes) { // packet not complete return null; } else { - msgBuffer.delete(0, 4); + LOG.trace("<-- {}", msgBuffer); + msgBuffer.delete(0, packetLengthBytes); StringBuilder rawMessage = new StringBuilder(msgBuffer.substring(0, packetLength)); int type = parseNextInt(rawMessage); int sender = parseNextInt(rawMessage); diff --git a/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java b/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java index 07974847..7919ef23 100644 --- a/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java +++ b/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java @@ -87,7 +87,7 @@ public class Stripchat extends AbstractSite { @Override public HttpClient getHttpClient() { - if(httpClient == null) { + if (httpClient == null) { httpClient = new StripchatHttpClient(); } return httpClient; @@ -100,7 +100,7 @@ public class Stripchat extends AbstractSite { @Override public void shutdown() { - if(httpClient != null) { + if (httpClient != null) { httpClient.shutdown(); } } diff --git a/master/pom.xml b/master/pom.xml index 037cc805..ebbb31be 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -6,7 +6,7 @@ ctbrec master pom - 3.8.0 + 3.8.1 ../common diff --git a/server/pom.xml b/server/pom.xml index 19e99d3f..26cf509e 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 3.8.0 + 3.8.1 ../master diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml index 4285a070..d9bf0230 100644 --- a/server/src/main/resources/logback.xml +++ b/server/src/main/resources/logback.xml @@ -9,17 +9,24 @@ - - + server.log - false - - DEBUG - + true + + DEBUG + - %date %level [%thread] %logger{10} [%file:%line] %msg%n - + %date %level [%thread] %logger{10} [%file:%line] %msg%n + + server.%i.log + 1 + 3 + + + 5MB +