From 31721ed7c93bec8b441d98b6a10c7aebfba145f0 Mon Sep 17 00:00:00 2001 From: Jafea7 <73450040+Jafea7@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:47:21 +1000 Subject: [PATCH] Add buttons - Show PAC file, backup config --- .../main/java/ctbrec/ui/controls/Dialogs.java | 80 +++++++++++++ .../java/ctbrec/ui/settings/SettingsTab.java | 112 +++++++++++++++++- 2 files changed, 186 insertions(+), 6 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/Dialogs.java b/client/src/main/java/ctbrec/ui/controls/Dialogs.java index 548b6586..3b965097 100644 --- a/client/src/main/java/ctbrec/ui/controls/Dialogs.java +++ b/client/src/main/java/ctbrec/ui/controls/Dialogs.java @@ -7,24 +7,34 @@ import java.io.StringWriter; import java.util.Collection; import java.util.Objects; import java.util.Optional; +import javafx.animation.FadeTransition; +import javafx.animation.PauseTransition; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; +import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import javafx.stage.Modality; import javafx.stage.Stage; +import javafx.util.Duration; +import lombok.extern.slf4j.Slf4j; import java.io.InputStream; import java.util.Optional; import static javafx.scene.control.ButtonType.*; +@Slf4j public class Dialogs { private Dialogs() {} @@ -43,6 +53,76 @@ public class Dialogs { } } + public static void showToast(Scene parent, String message) { + if (parent == null) { + throw new IllegalArgumentException("Scene cannot be null for showing toast"); + } + + Runnable r = () -> { + Label toast = new Label(message); + toast.setStyle("-fx-background-color: #323232; -fx-text-fill: white; -fx-padding: 10px; -fx-border-radius: 5px; -fx-background-radius: 5px;"); + toast.setOpacity(0); + toast.setMaxWidth(Double.MAX_VALUE); + toast.setAlignment(Pos.CENTER); + + Node root = parent.getRoot(); + + if (root instanceof StackPane) { + StackPane parentPane = (StackPane) root; + parentPane.getChildren().add(toast); + StackPane.setAlignment(toast, Pos.BOTTOM_CENTER); + StackPane.setMargin(toast, new Insets(0, 0, 40, 0)); + } else if (root instanceof BorderPane) { + BorderPane parentPane = (BorderPane) root; + parentPane.setBottom(toast); + BorderPane.setAlignment(toast, Pos.BOTTOM_CENTER); + BorderPane.setMargin(toast, new Insets(0, 0, 40, 0)); + } else if (root instanceof Pane) { + Pane parentPane = (Pane) root; + parentPane.getChildren().add(toast); + toast.setLayoutX((parentPane.getWidth() - toast.getWidth()) / 2); + toast.setLayoutY(parentPane.getHeight() - 40 - toast.getHeight()); + } else { + log.error("Unsupported root node type for toast: " + root.getClass().getSimpleName()); + return; + } + + FadeTransition fadeIn = new FadeTransition(Duration.millis(500), toast); + fadeIn.setFromValue(0); + fadeIn.setToValue(1); + + FadeTransition fadeOut = new FadeTransition(Duration.millis(500), toast); + fadeOut.setFromValue(1); + fadeOut.setToValue(0); + + fadeIn.setOnFinished(e -> { + PauseTransition pause = new PauseTransition(Duration.seconds(3)); + pause.setOnFinished(ev -> { + fadeOut.play(); + }); + pause.play(); + }); + + fadeOut.setOnFinished(e -> { + if (root instanceof StackPane) { + ((StackPane) root).getChildren().remove(toast); + } else if (root instanceof BorderPane) { + ((BorderPane) root).setBottom(null); + } else if (root instanceof Pane) { + ((Pane) root).getChildren().remove(toast); + } + }); + + fadeIn.play(); + }; + + if (Platform.isFxApplicationThread()) { + r.run(); + } else { + Platform.runLater(r); + } + } + public static void showError(Scene parent, String header, String text) { Runnable r = () -> { AutosizeAlert alert = new AutosizeAlert(Alert.AlertType.ERROR, parent); diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 91dd91b4..b2a3f8c4 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -1,7 +1,6 @@ package ctbrec.ui.settings; -import lombok.extern.slf4j.Slf4j; import ctbrec.Config; import ctbrec.GlobalThreadPool; import ctbrec.Hmac; @@ -12,6 +11,7 @@ import ctbrec.docs.DocServer; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.ui.DesktopIntegration; +import ctbrec.ui.controls.Dialogs; import ctbrec.ui.SiteUI; import ctbrec.ui.SiteUiFactory; import ctbrec.ui.controls.range.DiscreteRange; @@ -23,6 +23,7 @@ import ctbrec.ui.tabs.TabSelectionListener; import javafx.animation.FadeTransition; import javafx.animation.PauseTransition; import javafx.animation.Transition; +import javafx.application.Platform; import javafx.beans.binding.BooleanExpression; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; @@ -30,18 +31,33 @@ import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; +/* import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; */ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Tab; import javafx.scene.control.TextInputDialog; +import javafx.scene.control.Tooltip; import javafx.scene.layout.*; import javafx.scene.paint.Color; import javafx.util.Duration; +import javafx.geometry.Insets; +import javafx.scene.control.Label; + import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.awt.Desktop; +import java.io.*; +import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.zip.*; import static ctbrec.Settings.DirectoryStructure.*; import static ctbrec.Settings.ProxyType.*; @@ -340,7 +356,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { Setting.of("Port", proxyPort).needsRestart(), Setting.of("Username", proxyUser).needsRestart(), Setting.of("Password", proxyPassword).needsRestart(), - Setting.of("PAC URL", pacUrl, "URL to your Proxy Auto-Config (PAC) file (e.g. http://example.com/pac.js or file:///G:/path/to/pac.js)").needsRestart())), + Setting.of("PAC URL", pacUrl, "URL to your Proxy Auto-Config (PAC) file (e.g. http://example.com/pac.js or file:///G:/path/to/pac.js)").needsRestart(), + Setting.of("", createPacButton()))), Category.of("Advanced / Devtools", Group.of("Networking", Setting.of("Playlist request timeout (ms)", playlistRequestTimeout, "Timeout in ms for playlist requests"), @@ -363,11 +380,13 @@ public class SettingsTab extends Tab implements TabSelectionListener { "Use hlsdl to record the live streams. Some features might not work correctly."), Setting.of("hlsdl executable", hlsdlExecutable, "Path to the hlsdl executable"), Setting.of("Log hlsdl output", loghlsdlOutput, "Log hlsdl output to files in the system's temp directory")), - Group.of("Miscelaneous", + Group.of("Miscellaneous", Setting.of("Config file saving delay (ms)", configSavingDelayMs, - "Wait specified number of milliseconds before actually writing config to disk"))), - Category.of("Help/Cfg/Log", (new HelpTab()).getContent()), - Category.of("Donate", (new DonateTabFx()).getContent())); + "Wait specified number of milliseconds before actually writing config to disk")), + Group.of("Backup", + Setting.of("Backup Config", createBackupConfigButton()))), + Category.of("Help/Cfg/Log", (new HelpTab()).getContent()), + Category.of("Donate", (new DonateTabFx()).getContent())); Region preferencesView = prefs.getView(); prefs.onRestartRequired(this::showRestartRequired); storage.setPreferences(prefs); @@ -447,6 +466,87 @@ public class SettingsTab extends Tab implements TabSelectionListener { return postProcessingHelpButton; } + private Button createPacButton() { + Button openPacButton = new Button("View PAC File"); + openPacButton.setOnAction(e -> { + String url = pacUrl.get(); + if (url == null || url.trim().isEmpty()) { + Dialogs.showToast(this.getTabPane().getScene(), "Invalid PAC URL: Please enter a valid PAC URL in the settings."); + return; + } + new Thread(() -> { + try { + URI uri = new URI(url); + if (!uri.getScheme().equalsIgnoreCase("http") && !uri.getScheme().equalsIgnoreCase("https") && !uri.getScheme().equalsIgnoreCase("file")) { + Platform.runLater(() -> Dialogs.showToast(this.getTabPane().getScene(), "Invalid PAC URL: Must use http, https, or file scheme.")); + return; + } + log.info("Opening PAC file in browser: {}", url); + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(uri); + } else { + log.info("Desktop.browse not supported, trying xdg-open"); + ProcessBuilder pb = new ProcessBuilder("xdg-open", url); + pb.start(); + } + } catch (URISyntaxException ex) { + log.error("Invalid PAC URL format: {}", url, ex); + Platform.runLater(() -> Dialogs.showToast(this.getTabPane().getScene(), "Invalid PAC URL: " + ex.getMessage())); + } catch (IOException ex) { + log.error("Failed to open PAC file in browser: {}", url, ex); + Platform.runLater(() -> Dialogs.showToast(this.getTabPane().getScene(), "Failed to open PAC file: " + ex.getMessage())); + } + }, "PAC-Browser-Thread").start(); + }); + openPacButton.disableProperty().bind(pacUrl.isNull().or(pacUrl.isEqualTo(""))); + return openPacButton; + } + + private Button createBackupConfigButton() { + var button = new Button("Backup Config"); + button.setTooltip(new Tooltip("Excludes recordings, cache, and backup configs.")); + button.setOnAction(e -> { + Path configDir = Config.getInstance().getConfigDir().toPath(); + Path rootDir = configDir.getParent(); + String backupName = "config_backup_" + System.currentTimeMillis() + ".zip"; + Path backupZip = Paths.get(System.getProperty("user.dir")).resolve(backupName); + try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(backupZip))) { + Files.walk(rootDir) + .filter(path -> !path.toString().contains("_backup_")) + .filter(path -> !path.toString().contains("cache")) + .filter(path -> !path.toString().contains("recordings")) + .forEach(path -> { + Path relativePath = rootDir.relativize(path); + try { + if (Files.isDirectory(path)) { + String dirEntry = relativePath.toString() + "/"; + if (!dirEntry.equals("./")) { + ZipEntry entry = new ZipEntry(dirEntry); + zos.putNextEntry(entry); + zos.closeEntry(); + } + } else { + ZipEntry entry = new ZipEntry(relativePath.toString()); + zos.putNextEntry(entry); + try (InputStream is = Files.newInputStream(path)) { + is.transferTo(zos); + } + zos.closeEntry(); + } + } catch (IOException ex) { + log.error("Error adding path to backup: " + path, ex); + } + }); + log.info("Config backup created: " + backupZip); + Dialogs.showToast(this.getTabPane().getScene(), "Backup finished: " + backupZip.getFileName()); + } catch (IOException ex) { + log.error("Failed to create config backup", ex); + Dialogs.showToast(this.getTabPane().getScene(), "Backup failed!"); + } + }); + return button; + } + private Button createVariablePlayGroundButton() { var button = new Button("Variable Playground"); button.setOnAction(e -> variablePlayGroundDialogFactory.openDialog(this.getTabPane().getScene(), config, recorder));