From 11d7bfcdca3b324aadfadf251ba8fac324a31402 Mon Sep 17 00:00:00 2001 From: Jafea7 <73450040+Jafea7@users.noreply.github.com> Date: Sun, 28 Sep 2025 14:00:36 +1000 Subject: [PATCH] Add Backup Config button --- .../main/java/ctbrec/ui/controls/Dialogs.java | 80 +++++++++++++ .../java/ctbrec/ui/settings/SettingsTab.java | 108 +++++++++++++++++- 2 files changed, 186 insertions(+), 2 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..8504adba 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() {} @@ -35,6 +45,76 @@ public class Dialogs { Dialogs.scene = scene; } + 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(String header, String text, Throwable t) { if (Objects.nonNull(t)) { Dialogs.showError(scene, header, text, t); diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 90578bfb..7639df0a 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -10,6 +10,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; @@ -32,16 +33,26 @@ 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.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.*; @@ -228,6 +239,18 @@ public class SettingsTab extends Tab implements TabSelectionListener { .ifPresent(configPanel -> siteCategories.add(Category.of(site.getName(), configPanel))); } + Button openPacButton = new Button("View PAC File"); + openPacButton.setOnAction(e -> { + String url = pacUrl.get(); + if (url != null && !url.isEmpty()) { + try { + Desktop.getDesktop().browse(new URI(url)); + } catch (Exception ex) { + log.error("Could not open PAC file in browser", ex); + } + } + }); + var storage = new CtbrecPreferencesStorage(config); var prefs = Preferences.of(storage, Category.of("General", @@ -323,7 +346,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("", openPacButton))), Category.of("Advanced / Devtools", Group.of("Networking", Setting.of("Playlist request timeout (ms)", playlistRequestTimeout, "Timeout in ms for playlist requests")), @@ -335,7 +359,9 @@ public class SettingsTab extends Tab implements TabSelectionListener { Setting.of("Use hlsdl (if possible)", useHlsdl, "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"))), + Setting.of("Log hlsdl output", loghlsdlOutput, "Log hlsdl output to files in the system's temp directory")), + 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(); @@ -415,6 +441,51 @@ public class SettingsTab extends Tab implements TabSelectionListener { return postProcessingHelpButton; } + 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)); @@ -600,6 +671,39 @@ public class SettingsTab extends Tab implements TabSelectionListener { } } + private void showToast(String message) { + 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); + + StackPane parent = (StackPane) getContent(); + toast.setMaxWidth(Double.MAX_VALUE); + toast.setAlignment(Pos.CENTER); + + parent.getChildren().add(toast); + StackPane.setAlignment(toast, Pos.BOTTOM_CENTER); + StackPane.setMargin(toast, new Insets(0, 0, 40, 0)); + + 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 -> parent.getChildren().remove(toast)); + + fadeIn.play(); + } + + public record SplitBiggerThanOption(String label, @Getter long value) { @Override