package ctbrec.ui.settings; import static ctbrec.Settings.DirectoryStructure.*; 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.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; 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.DirectoryStructure; import ctbrec.StringUtil; import ctbrec.io.ModelJsonAdapter; import ctbrec.recorder.Recorder; import ctbrec.sites.ConfigUI; import ctbrec.sites.Site; import ctbrec.ui.AutosizeAlert; import ctbrec.ui.SiteUiFactory; import ctbrec.ui.TabSelectionListener; import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.DirectorySelectionBox; import ctbrec.ui.controls.ProgramSelectionBox; 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 { private 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 onlineCheckIntervalInSecs; private TextField leaveSpaceOnDevice; private TextField minimumLengthInSecs; 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 RadioButton recordLocal; private ProxySettingsPane proxySettingsPane; private TextField maxResolution; private TextField concurrentRecordings; private ComboBox splitAfter; private ComboBox directoryStructure; private ComboBox startTab; private List sites; private Label restartLabel; private Accordion siteConfigAccordion = new Accordion(); private Recorder recorder; public SettingsTab(List sites, Recorder recorder) { this.sites = sites; this.recorder = recorder; setText("Settings"); createGui(); setClosable(false); setRecordingMode(recordLocal.isSelected()); } 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()); // 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); // 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); } } } 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); TextField 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 Node createRecorderPanel() { int row = 0; GridPane layout = createGridLayout(); 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)); 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); 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)); } 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("Maximum resolution (0 = unlimited)"); Tooltip tt = new Tooltip("video height, e.g. 720 or 1080\n!Caution: If the resolution is unknown, ctbrec will not record the stream!"); l.setTooltip(tt); layout.add(l, 0, row); maxResolution = new TextField(Integer.toString(Config.getInstance().getSettings().maximumResolution)); maxResolution.setTooltip(tt); maxResolution.textProperty().addListener((observable, oldValue, newValue) -> { if (!newValue.matches("\\d*")) { maxResolution.setText(newValue.replaceAll(PATTERN_NOT_A_DIGIT, "")); } if (!maxResolution.getText().isEmpty()) { int newRes = Integer.parseInt(maxResolution.getText()); if (newRes != Config.getInstance().getSettings().maximumResolution) { Config.getInstance().getSettings().maximumResolution = newRes; saveConfig(); } } }); maxResolution.prefWidthProperty().bind(directoryStructure.widthProperty()); layout.add(maxResolution, 1, row++); GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); 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.setMargin(concurrentRecordings, new Insets(0, 0, 0, CHECKBOX_MARGIN)); 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++); 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.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.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.setMargin(minimumLengthInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(minimumLengthInSecs, 1, row); TitledPane locations = new TitledPane("Recorder", layout); locations.setCollapsible(false); return locations; } 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 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); } } } private Node createGeneralPanel() { GridPane layout = createGridLayout(); int row = 0; layout.add(new Label("Player"), 0, row); ProgramSelectionBox mediaPlayer = new ProgramSelectionBox(Config.getInstance().getSettings().mediaPlayer); mediaPlayer.fileProperty().addListener((obs, o, n) -> { String path = n; if (!Objects.equals(path, Config.getInstance().getSettings().mediaPlayer)) { Config.getInstance().getSettings().mediaPlayer = path; saveConfig(); } }); GridPane.setFillWidth(mediaPlayer, true); GridPane.setHgrow(mediaPlayer, Priority.ALWAYS); GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(mediaPlayer, 1, 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)); layout.add(multiplePlayers, 1, row++); l = new Label("Show \"Player Starting\" Message"); layout.add(l, 0, 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, 1, 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, 0, 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, 1, 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, 0, 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, 1, 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)); 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); } } } void showRestartRequired() { restartLabel.setVisible(true); } public static GridPane createGridLayout() { GridPane layout = new GridPane(); layout.setPadding(new Insets(10)); layout.setHgap(5); layout.setVgap(5); return layout; } private void setRecordingMode(boolean local) { server.setDisable(local); port.setDisable(local); useAuthentication.setDisable(local); useTLS.setDisable(local); recordingsDirectory.setDisable(!local); splitAfter.setDisable(!local); maxResolution.setDisable(!local); directoryStructure.setDisable(!local); onlineCheckIntervalInSecs.setDisable(!local); leaveSpaceOnDevice.setDisable(!local); postProcessing.setDisable(!local); minimumLengthInSecs.setDisable(!local); concurrentRecordings.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); } } public static class SplitAfterOption { private String label; private int value; public SplitAfterOption(String label, int value) { super(); this.label = label; this.value = value; } public int getValue() { return value; } @Override public String toString() { return label; } } }