package ctbrec.ui; import static ctbrec.Settings.DirectoryStructure.*; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Hmac; import ctbrec.Settings; import ctbrec.Settings.DirectoryStructure; import ctbrec.sites.ConfigUI; import ctbrec.sites.Site; import javafx.beans.value.ChangeListener; 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; import javafx.scene.control.Button; 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.Border; import javafx.scene.layout.BorderStroke; import javafx.scene.layout.BorderStrokeStyle; import javafx.scene.layout.BorderWidths; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.CornerRadii; 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.DirectoryChooser; import javafx.stage.FileChooser;; public class SettingsTab extends Tab implements TabSelectionListener { private static final transient Logger LOG = LoggerFactory.getLogger(SettingsTab.class); public static final int CHECKBOX_MARGIN = 6; private TextField recordingsDirectory; private Button recordingsDirectoryButton; private Button postProcessingDirectoryButton; private TextField mediaPlayer; private TextField postProcessing; private TextField server; private TextField port; private CheckBox loadResolution; private CheckBox secureCommunication = new CheckBox(); private CheckBox chooseStreamQuality = new CheckBox(); private CheckBox multiplePlayers = new CheckBox(); private RadioButton recordLocal; private RadioButton recordRemote; private ToggleGroup recordLocation; private ProxySettingsPane proxySettingsPane; private ComboBox maxResolution; private ComboBox splitAfter; private ComboBox directoryStructure; private List sites; private Label restartLabel; private Accordion credentialsAccordion = new Accordion(); public SettingsTab(List sites) { this.sites = sites; 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); setContent(new ScrollPane(mainLayout)); VBox leftSide = new VBox(15); VBox rightSide = new VBox(15); 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); // restart info label restartLabel = new Label("A restart is required to apply 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(createLocationsPanel()); leftSide.getChildren().add(createRecordLocationPanel()); proxySettingsPane = new ProxySettingsPane(this); leftSide.getChildren().add(proxySettingsPane); //right side rightSide.getChildren().add(createSiteSelectionPanel()); rightSide.getChildren().add(credentialsAccordion); 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()); credentialsAccordion.getPanes().add(pane); } } credentialsAccordion.setExpandedPane(credentialsAccordion.getPanes().get(0)); } private Node createSiteSelectionPanel() { Settings settings = Config.getInstance().getSettings(); GridPane layout = createGridLayout(); int row = 0; for (Site site : sites) { Label l = new Label(site.getName()); layout.add(l, 0, row); CheckBox enabled = new CheckBox(); enabled.setSelected(!settings.disabledSites.contains(site.getName())); enabled.setOnAction((e) -> { if(enabled.isSelected()) { settings.disabledSites.remove(site.getName()); } else { settings.disabledSites.add(site.getName()); } showRestartRequired(); }); GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(enabled, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(enabled, 1, row++); } TitledPane siteSelection = new TitledPane("Enabled Sites", layout); siteSelection.setCollapsible(false); return siteSelection; } private Node createRecordLocationPanel() { GridPane layout = createGridLayout(); Label l = new Label("Record Location"); layout.add(l, 0, 0); recordLocation = new ToggleGroup(); recordLocal = new RadioButton("Local"); recordRemote = new RadioButton("Remote"); recordLocal.setToggleGroup(recordLocation); recordRemote.setToggleGroup(recordLocation); recordLocal.setSelected(Config.getInstance().getSettings().localRecording); recordRemote.setSelected(!recordLocal.isSelected()); layout.add(recordLocal, 1, 0); layout.add(recordRemote, 2, 0); recordLocation.selectedToggleProperty().addListener((e) -> { Config.getInstance().getSettings().localRecording = recordLocal.isSelected(); setRecordingMode(recordLocal.isSelected()); showRestartRequired(); }); 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, 1); server = new TextField(Config.getInstance().getSettings().httpServer); server.focusedProperty().addListener((e) -> { if(!server.getText().isEmpty()) { Config.getInstance().getSettings().httpServer = server.getText(); } }); GridPane.setFillWidth(server, true); GridPane.setHgrow(server, Priority.ALWAYS); GridPane.setColumnSpan(server, 2); layout.add(server, 1, 1); layout.add(new Label("Port"), 0, 2); port = new TextField(Integer.toString(Config.getInstance().getSettings().httpPort)); port.focusedProperty().addListener((e) -> { if(!port.getText().isEmpty()) { try { Config.getInstance().getSettings().httpPort = Integer.parseInt(port.getText()); port.setBorder(Border.EMPTY); port.setTooltip(null); } catch (NumberFormatException e1) { port.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); port.setTooltip(new Tooltip("Port has to be a number in the range 1 - 65536")); } } }); GridPane.setFillWidth(port, true); GridPane.setHgrow(port, Priority.ALWAYS); GridPane.setColumnSpan(port, 2); layout.add(port, 1, 2); l = new Label("Require authentication"); layout.add(l, 0, 3); secureCommunication.setSelected(Config.getInstance().getSettings().requireAuthentication); secureCommunication.setOnAction((e) -> { Config.getInstance().getSettings().requireAuthentication = secureCommunication.isSelected(); if(secureCommunication.isSelected()) { byte[] key = Config.getInstance().getSettings().key; if(key == null) { key = Hmac.generateKey(); Config.getInstance().getSettings().key = key; } 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(secureCommunication, new Insets(4, 0, 0, 0)); layout.add(secureCommunication, 1, 3); TitledPane recordLocation = new TitledPane("Record Location", layout); recordLocation.setCollapsible(false); return recordLocation; } private Node createLocationsPanel() { int row = 0; GridPane layout = createGridLayout(); layout.add(new Label("Recordings Directory"), 0, row); recordingsDirectory = new TextField(Config.getInstance().getSettings().recordingsDir); recordingsDirectory.focusedProperty().addListener(createRecordingsDirectoryFocusListener()); recordingsDirectory.setPrefWidth(400); GridPane.setFillWidth(recordingsDirectory, true); GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS); GridPane.setColumnSpan(recordingsDirectory, 2); GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(recordingsDirectory, 1, row); recordingsDirectoryButton = createRecordingsBrowseButton(); layout.add(recordingsDirectoryButton, 3, 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()); GridPane.setColumnSpan(directoryStructure, 2); GridPane.setMargin(directoryStructure, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(directoryStructure, 1, row++); layout.add(new Label("Post-Processing"), 0, row); postProcessing = new TextField(Config.getInstance().getSettings().postProcessing); postProcessing.focusedProperty().addListener(createPostProcessingFocusListener()); GridPane.setFillWidth(postProcessing, true); GridPane.setHgrow(postProcessing, Priority.ALWAYS); GridPane.setColumnSpan(postProcessing, 2); GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(postProcessing, 1, row); postProcessingDirectoryButton = createPostProcessingBrowseButton(); layout.add(postProcessingDirectoryButton, 3, row++); layout.add(new Label("Player"), 0, row); mediaPlayer = new TextField(Config.getInstance().getSettings().mediaPlayer); mediaPlayer.focusedProperty().addListener(createMpvFocusListener()); GridPane.setFillWidth(mediaPlayer, true); GridPane.setHgrow(mediaPlayer, Priority.ALWAYS); GridPane.setColumnSpan(mediaPlayer, 2); GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(mediaPlayer, 1, row); layout.add(createMpvBrowseButton(), 3, row++); TitledPane locations = new TitledPane("Locations", layout); locations.setCollapsible(false); return locations; } private Node createGeneralPanel() { GridPane layout = createGridLayout(); int row = 0; Label l = new Label("Display stream resolution in overview"); layout.add(l, 0, row); loadResolution = new CheckBox(); loadResolution.setSelected(Config.getInstance().getSettings().determineResolution); loadResolution.setOnAction((e) -> { Config.getInstance().getSettings().determineResolution = loadResolution.isSelected(); if(!loadResolution.isSelected()) { ThumbOverviewTab.queue.clear(); } }); //GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(loadResolution, 1, row++); 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()); 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("Manually select stream quality"); layout.add(l, 0, row); chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality); chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected()); GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(chooseStreamQuality, 1, row++); l = new Label("Maximum resolution (0 = unlimited)"); layout.add(l, 0, row); List resolutionOptions = new ArrayList<>(); resolutionOptions.add(1080); resolutionOptions.add(720); resolutionOptions.add(600); resolutionOptions.add(480); resolutionOptions.add(0); maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions)); setMaxResolutionValue(); maxResolution.setOnAction((e) -> Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem()); layout.add(maxResolution, 1, row++); GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(maxResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); l = new Label("Split recordings after (minutes)"); layout.add(l, 0, row); List options = new ArrayList<>(); options.add(new SplitAfterOption("disabled", 0)); options.add(new SplitAfterOption("10 min", 10 * 60)); options.add(new SplitAfterOption("15 min", 15 * 60)); options.add(new SplitAfterOption("20 min", 20 * 60)); options.add(new SplitAfterOption("30 min", 30 * 60)); options.add(new SplitAfterOption("60 min", 60 * 60)); splitAfter = new ComboBox<>(FXCollections.observableList(options)); layout.add(splitAfter, 1, row++); setSplitAfterValue(); splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue()); GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); maxResolution.prefWidthProperty().bind(splitAfter.widthProperty()); 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); } } } private void setMaxResolutionValue() { int value = Config.getInstance().getSettings().maximumResolution; for (Integer option : maxResolution.getItems()) { if(option == value) { maxResolution.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); secureCommunication.setDisable(local); recordingsDirectory.setDisable(!local); recordingsDirectoryButton.setDisable(!local); splitAfter.setDisable(!local); maxResolution.setDisable(!local); postProcessing.setDisable(!local); postProcessingDirectoryButton.setDisable(!local); directoryStructure.setDisable(!local); } private ChangeListener createRecordingsDirectoryFocusListener() { return new ChangeListener() { @Override public void changed(ObservableValue arg0, Boolean oldPropertyValue, Boolean newPropertyValue) { if (newPropertyValue) { recordingsDirectory.setBorder(Border.EMPTY); recordingsDirectory.setTooltip(null); } else { String input = recordingsDirectory.getText(); File newDir = new File(input); setRecordingsDir(newDir); } } }; } private ChangeListener createMpvFocusListener() { return new ChangeListener() { @Override public void changed(ObservableValue arg0, Boolean oldPropertyValue, Boolean newPropertyValue) { if (newPropertyValue) { mediaPlayer.setBorder(Border.EMPTY); mediaPlayer.setTooltip(null); } else { String input = mediaPlayer.getText(); File program = new File(input); setMpv(program); } } }; } private ChangeListener createPostProcessingFocusListener() { return new ChangeListener() { @Override public void changed(ObservableValue arg0, Boolean oldPropertyValue, Boolean newPropertyValue) { if (newPropertyValue) { postProcessing.setBorder(Border.EMPTY); postProcessing.setTooltip(null); } else { String input = postProcessing.getText(); if(!input.trim().isEmpty()) { File program = new File(input); setPostProcessing(program); } else { Config.getInstance().getSettings().postProcessing = ""; } } } }; } private void setMpv(File program) { String msg = validateProgram(program); if (msg != null) { mediaPlayer.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); mediaPlayer.setTooltip(new Tooltip(msg)); } else { Config.getInstance().getSettings().mediaPlayer = mediaPlayer.getText(); } } private void setPostProcessing(File program) { String msg = validateProgram(program); if (msg != null) { postProcessing.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); postProcessing.setTooltip(new Tooltip(msg)); } else { Config.getInstance().getSettings().postProcessing = postProcessing.getText(); } } private String validateProgram(File program) { if (program == null || !program.exists()) { return "File does not exist"; } else if (!program.isFile() || !program.canExecute()) { return "This is not an executable application"; } return null; } private Button createRecordingsBrowseButton() { Button button = new Button("Select"); button.setOnAction((e) -> { DirectoryChooser chooser = new DirectoryChooser(); File currentDir = new File(Config.getInstance().getSettings().recordingsDir); if (currentDir.exists() && currentDir.isDirectory()) { chooser.setInitialDirectory(currentDir); } File selectedDir = chooser.showDialog(null); if(selectedDir != null) { setRecordingsDir(selectedDir); } }); return button; } private Node createMpvBrowseButton() { Button button = new Button("Select"); button.setOnAction((e) -> { FileChooser chooser = new FileChooser(); File program = chooser.showOpenDialog(null); if(program != null) { try { mediaPlayer.setText(program.getCanonicalPath()); } catch (IOException e1) { LOG.error("Couldn't determine path", e1); Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); alert.setTitle("Whoopsie"); alert.setContentText("Couldn't determine path"); alert.showAndWait(); } setMpv(program); } }); return button; } private Button createPostProcessingBrowseButton() { Button button = new Button("Select"); button.setOnAction((e) -> { FileChooser chooser = new FileChooser(); File program = chooser.showOpenDialog(null); if(program != null) { try { postProcessing.setText(program.getCanonicalPath()); } catch (IOException e1) { LOG.error("Couldn't determine path", e1); Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); alert.setTitle("Whoopsie"); alert.setContentText("Couldn't determine path"); alert.showAndWait(); } setPostProcessing(program); } }); return button; } private void setRecordingsDir(File dir) { if (dir != null && dir.isDirectory()) { try { String path = dir.getCanonicalPath(); Config.getInstance().getSettings().recordingsDir = path; recordingsDirectory.setText(path); } catch (IOException e1) { LOG.error("Couldn't determine directory path", e1); Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); alert.setTitle("Whoopsie"); alert.setContentText("Couldn't determine directory path"); alert.showAndWait(); } } else { recordingsDirectory.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); if (!dir.isDirectory()) { recordingsDirectory.setTooltip(new Tooltip("This is not a directory")); } if (!dir.exists()) { recordingsDirectory.setTooltip(new Tooltip("Directory does not exist")); } } } @Override public void selected() { } @Override public void deselected() { saveConfig(); } public void saveConfig() { proxySettingsPane.saveConfig(); } 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; } } }