Add buttons - Show PAC file, backup config

This commit is contained in:
Jafea7 2025-09-30 17:47:21 +10:00
parent 4cff4ba57f
commit 31721ed7c9
2 changed files with 186 additions and 6 deletions

View File

@ -7,24 +7,34 @@ import java.io.StringWriter;
import java.util.Collection; import java.util.Collection;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import javafx.animation.FadeTransition;
import javafx.animation.PauseTransition;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import java.io.InputStream; import java.io.InputStream;
import java.util.Optional; import java.util.Optional;
import static javafx.scene.control.ButtonType.*; import static javafx.scene.control.ButtonType.*;
@Slf4j
public class Dialogs { public class Dialogs {
private 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) { public static void showError(Scene parent, String header, String text) {
Runnable r = () -> { Runnable r = () -> {
AutosizeAlert alert = new AutosizeAlert(Alert.AlertType.ERROR, parent); AutosizeAlert alert = new AutosizeAlert(Alert.AlertType.ERROR, parent);

View File

@ -1,7 +1,6 @@
package ctbrec.ui.settings; package ctbrec.ui.settings;
import lombok.extern.slf4j.Slf4j;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.GlobalThreadPool; import ctbrec.GlobalThreadPool;
import ctbrec.Hmac; import ctbrec.Hmac;
@ -12,6 +11,7 @@ import ctbrec.docs.DocServer;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.DesktopIntegration; import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.SiteUI; import ctbrec.ui.SiteUI;
import ctbrec.ui.SiteUiFactory; import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.controls.range.DiscreteRange; import ctbrec.ui.controls.range.DiscreteRange;
@ -23,6 +23,7 @@ import ctbrec.ui.tabs.TabSelectionListener;
import javafx.animation.FadeTransition; import javafx.animation.FadeTransition;
import javafx.animation.PauseTransition; import javafx.animation.PauseTransition;
import javafx.animation.Transition; import javafx.animation.Transition;
import javafx.application.Platform;
import javafx.beans.binding.BooleanExpression; import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -30,18 +31,33 @@ import javafx.collections.FXCollections;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
/* import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType; */
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TextInputDialog; import javafx.scene.control.TextInputDialog;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.util.Duration; import javafx.util.Duration;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import lombok.Getter; 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.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.zip.*;
import static ctbrec.Settings.DirectoryStructure.*; import static ctbrec.Settings.DirectoryStructure.*;
import static ctbrec.Settings.ProxyType.*; import static ctbrec.Settings.ProxyType.*;
@ -340,7 +356,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Port", proxyPort).needsRestart(), Setting.of("Port", proxyPort).needsRestart(),
Setting.of("Username", proxyUser).needsRestart(), Setting.of("Username", proxyUser).needsRestart(),
Setting.of("Password", proxyPassword).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", Category.of("Advanced / Devtools",
Group.of("Networking", Group.of("Networking",
Setting.of("Playlist request timeout (ms)", playlistRequestTimeout, "Timeout in ms for playlist requests"), 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."), "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("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("Miscelaneous", Group.of("Miscellaneous",
Setting.of("Config file saving delay (ms)", configSavingDelayMs, Setting.of("Config file saving delay (ms)", configSavingDelayMs,
"Wait specified number of milliseconds before actually writing config to disk"))), "Wait specified number of milliseconds before actually writing config to disk")),
Category.of("Help/Cfg/Log", (new HelpTab()).getContent()), Group.of("Backup",
Category.of("Donate", (new DonateTabFx()).getContent())); Setting.of("Backup Config", createBackupConfigButton()))),
Category.of("Help/Cfg/Log", (new HelpTab()).getContent()),
Category.of("Donate", (new DonateTabFx()).getContent()));
Region preferencesView = prefs.getView(); Region preferencesView = prefs.getView();
prefs.onRestartRequired(this::showRestartRequired); prefs.onRestartRequired(this::showRestartRequired);
storage.setPreferences(prefs); storage.setPreferences(prefs);
@ -447,6 +466,87 @@ public class SettingsTab extends Tab implements TabSelectionListener {
return postProcessingHelpButton; 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() { private Button createVariablePlayGroundButton() {
var button = new Button("Variable Playground"); var button = new Button("Variable Playground");
button.setOnAction(e -> variablePlayGroundDialogFactory.openDialog(this.getTabPane().getScene(), config, recorder)); button.setOnAction(e -> variablePlayGroundDialogFactory.openDialog(this.getTabPane().getScene(), config, recorder));