From: Ruben Beltran del Rio Date: Mon, 31 Jul 2023 14:58:57 +0000 (+0200) Subject: Allow URL configuration X-Git-Tag: 1.0.0~8 X-Git-Url: https://git.r.bdr.sh/rbdr/captura/commitdiff_plain/ba17de891507da74fb07423803fd636a4457354c?ds=inline;hp=533cd932281300fb444c07e80f81fc683a410b60 Allow URL configuration --- diff --git a/Captura.xcodeproj/project.pbxproj b/Captura.xcodeproj/project.pbxproj index 388af03..4c6ba9a 100644 --- a/Captura.xcodeproj/project.pbxproj +++ b/Captura.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ B5278B3E2A74420F009F6462 /* Captura.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B5278B3C2A74420F009F6462 /* Captura.xcdatamodeld */; }; B5278B402A744297009F6462 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B3F2A744297009F6462 /* Persistence.swift */; }; B5278B422A779CDB009F6462 /* BackendResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B412A779CDB009F6462 /* BackendResponse.swift */; }; + B5278B452A77D924009F6462 /* CaptureSessionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B442A77D924009F6462 /* CaptureSessionConfiguration.swift */; }; + B5278B472A77E8D7009F6462 /* CapturaURLDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B462A77E8D7009F6462 /* CapturaURLDecoder.swift */; }; B55DDFCC2A6F0253001A5E76 /* Notification+AppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */; }; B55DDFCE2A6F069D001A5E76 /* RecordingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55DDFCD2A6F069D001A5E76 /* RecordingWindow.swift */; }; B56C70CD2A6EFDF4009B97EB /* CaptureState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */; }; @@ -66,6 +68,9 @@ B5278B3D2A74420F009F6462 /* CapturaRemoteFile.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CapturaRemoteFile.xcdatamodel; sourceTree = ""; }; B5278B3F2A744297009F6462 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; B5278B412A779CDB009F6462 /* BackendResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendResponse.swift; sourceTree = ""; }; + B5278B432A77B43A009F6462 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + B5278B442A77D924009F6462 /* CaptureSessionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureSessionConfiguration.swift; sourceTree = ""; }; + B5278B462A77E8D7009F6462 /* CapturaURLDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaURLDecoder.swift; sourceTree = ""; }; B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+AppEvents.swift"; sourceTree = ""; }; B55DDFCD2A6F069D001A5E76 /* RecordingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingWindow.swift; sourceTree = ""; }; B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureState.swift; sourceTree = ""; }; @@ -155,6 +160,7 @@ B5278B3C2A74420F009F6462 /* Captura.xcdatamodeld */, B5278B3F2A744297009F6462 /* Persistence.swift */, B5278B412A779CDB009F6462 /* BackendResponse.swift */, + B5278B462A77E8D7009F6462 /* CapturaURLDecoder.swift */, ); path = Data; sourceTree = ""; @@ -183,6 +189,7 @@ children = ( B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */, B5278B352A73B3AA009F6462 /* CapturaCaptureSession.swift */, + B5278B442A77D924009F6462 /* CaptureSessionConfiguration.swift */, ); path = Domain; sourceTree = ""; @@ -210,6 +217,7 @@ B5F915502A6EF80D007ECE8E /* Captura */ = { isa = PBXGroup; children = ( + B5278B432A77B43A009F6462 /* Info.plist */, B5F915512A6EF80D007ECE8E /* CapturaApp.swift */, B5278B262A739862009F6462 /* Core Extensions */, B5278B1C2A71BD3C009F6462 /* Data */, @@ -385,6 +393,7 @@ B5278B382A73D1EE009F6462 /* CapturaSettings.swift in Sources */, B5278B282A739871009F6462 /* CGImage+resize.swift in Sources */, B5278B422A779CDB009F6462 /* BackendResponse.swift in Sources */, + B5278B452A77D924009F6462 /* CaptureSessionConfiguration.swift in Sources */, B5278B2C2A739B3A009F6462 /* CapturaFile.swift in Sources */, B5278B1F2A71BD9B009F6462 /* OutputSettings.swift in Sources */, B5278B2A2A73992D009F6462 /* GifRenderer.swift in Sources */, @@ -401,6 +410,7 @@ B5278B362A73B3AA009F6462 /* CapturaCaptureSession.swift in Sources */, B5278B3E2A74420F009F6462 /* Captura.xcdatamodeld in Sources */, B5F915522A6EF80D007ECE8E /* CapturaApp.swift in Sources */, + B5278B472A77E8D7009F6462 /* CapturaURLDecoder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -569,6 +579,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Captura/Info.plist; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( @@ -598,6 +609,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Captura/Info.plist; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Captura/CapturaApp.swift b/Captura/CapturaApp.swift index e1c4e7e..d9c3ff0 100644 --- a/Captura/CapturaApp.swift +++ b/Captura/CapturaApp.swift @@ -34,10 +34,10 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { var images: [CGImage] = [] var outputFile: CapturaFile? = nil var gifCallbackTimer = ContinuousClock.now - var fps = CapturaSettings.frameRate var pixelDensity: CGFloat = 1.0 var stopTimer: DispatchWorkItem? var remoteFiles: [CapturaRemoteFile] = [] + var captureSessionConfiguration: CaptureSessionConfiguration = CaptureSessionConfiguration() func applicationDidFinishLaunching(_ notification: Notification) { setupStatusBar() @@ -98,6 +98,32 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } } + // MARK: - URL Event Handler + + func application(_ application: NSApplication, open urls: [URL]) { + print("AAAH OPENING") + if (CapturaSettings.shouldAllowURLAutomation) { + for url in urls { + if let action = CapturaURLDecoder.decodeParams(url: url) { + switch action { + case let .configure(config): + print("AAAH CONFIGURING \(config)") + CapturaSettings.apply(config) + case let .record(config): + print(config) + } + } + } + } else { + let alert = NSAlert() + alert.messageText = "URL Automation Prevented" + alert.informativeText = "A website or application attempted to record your screen using URL Automation. If you want to allow this, enable it in Preferences." + alert.alertStyle = .warning + alert.addButton(withTitle: "OK") + alert.runModal() + } + } + // MARK: - UI Event Handlers func menuWillOpen(_ menu: NSMenu) { @@ -211,7 +237,6 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { func startRecording() { captureState = .recording updateImage() - fps = CapturaSettings.frameRate outputFile = nil images = []; pixelDensity = recordingWindow?.pixelDensity ?? 1.0 @@ -228,7 +253,7 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { DispatchQueue.main.asyncAfter(deadline: .now() + 300, execute: stopTimer!) outputFile = CapturaFile() - if CapturaSettings.shouldSaveMp4 { + if captureSessionConfiguration.shouldSaveMp4 { captureSession.startRecording(to: outputFile!.mp4URL) } else { captureSession.startRunning() @@ -246,9 +271,9 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { stop() Task.detached { - if CapturaSettings.shouldSaveGif { + if self.captureSessionConfiguration.shouldSaveGif { if let outputFile = self.outputFile { - await GifRenderer.render(self.images, at: self.fps, to: outputFile.gifURL) + await GifRenderer.render(self.images, at: self.captureSessionConfiguration.frameRate, to: outputFile.gifURL) } } let wasSuccessful = await self.uploadOrCopy() @@ -277,7 +302,7 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { func receivedFrame(_ frame: CVImageBuffer) { let now = ContinuousClock.now - if now - gifCallbackTimer > .nanoseconds(1_000_000_000 / UInt64(fps)) { + if now - gifCallbackTimer > .nanoseconds(1_000_000_000 / UInt64(captureSessionConfiguration.frameRate)) { gifCallbackTimer = now DispatchQueue.main.async { if let cgImage = frame.cgImage?.resize(by: self.pixelDensity) { @@ -358,12 +383,13 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { boxListener?.cancel() recordingWindow?.close() recordingWindow = nil + captureSessionConfiguration = CaptureSessionConfiguration() } private func uploadOrCopy() async -> Bool { - if CapturaSettings.shouldUseBackend { + if captureSessionConfiguration.shouldUseBackend { let result = await uploadToBackend() - if result && !CapturaSettings.shouldKeepLocalFiles { + if result && !captureSessionConfiguration.shouldKeepLocalFiles { deleteLocalFiles() } return result @@ -374,8 +400,8 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } private func copyLocalToClipboard() { - let fileType: NSPasteboard.PasteboardType = .init(rawValue: CapturaSettings.shouldSaveGif ? "com.compuserve.gif" : "public.mpeg-4") - if let url = CapturaSettings.shouldSaveGif ? outputFile?.gifURL : outputFile?.mp4URL { + let fileType: NSPasteboard.PasteboardType = .init(rawValue: captureSessionConfiguration.shouldSaveGif ? "com.compuserve.gif" : "public.mpeg-4") + if let url = captureSessionConfiguration.shouldSaveGif ? outputFile?.gifURL : outputFile?.mp4URL { if let data = try? Data(contentsOf: url) { let pasteboard = NSPasteboard.general pasteboard.declareTypes([fileType], owner: nil) @@ -385,10 +411,10 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } private func uploadToBackend() async -> Bool { - let contentType = CapturaSettings.shouldUploadGif ? "image/gif" : "video/mp4" - if let url = CapturaSettings.shouldUploadGif ? outputFile?.gifURL : outputFile?.mp4URL { + let contentType = captureSessionConfiguration.shouldUploadGif ? "image/gif" : "video/mp4" + if let url = captureSessionConfiguration.shouldUploadGif ? outputFile?.gifURL : outputFile?.mp4URL { if let data = try? Data(contentsOf: url) { - if let remoteUrl = CapturaSettings.backend { + if let remoteUrl = captureSessionConfiguration.backend { var request = URLRequest(url: remoteUrl) request.httpMethod = "POST" request.httpBody = data @@ -424,12 +450,12 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } private func deleteLocalFiles() { - if CapturaSettings.shouldSaveGif { + if captureSessionConfiguration.shouldSaveGif { if let url = outputFile?.gifURL { try? FileManager.default.removeItem(at: url) } } - if CapturaSettings.shouldSaveMp4 { + if captureSessionConfiguration.shouldSaveMp4 { if let url = outputFile?.mp4URL { try? FileManager.default.removeItem(at: url) } diff --git a/Captura/Data/CapturaSettings.swift b/Captura/Data/CapturaSettings.swift index 01ef78c..85368ea 100644 --- a/Captura/Data/CapturaSettings.swift +++ b/Captura/Data/CapturaSettings.swift @@ -2,11 +2,21 @@ import Foundation struct CapturaSettings { static var frameRate: Int { - UserDefaults.standard.integer(forKey: "frameRate") + get { + UserDefaults.standard.integer(forKey: "frameRate") + } + set { + UserDefaults.standard.setValue(newValue, forKey: "frameRate") + } } static var outputFormats: OutputFormatSetting { - OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "outputFormats")) ?? .all + get { + OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "outputFormats")) ?? .all + } + set { + UserDefaults.standard.setValue(newValue.rawValue, forKey: "outputFormats") + } } static var shouldSaveMp4: Bool { @@ -30,17 +40,54 @@ struct CapturaSettings { } static var backend: URL? { - if let url = UserDefaults.standard.string(forKey: "backendUrl") { - return URL(string: url) + get { + if let url = UserDefaults.standard.string(forKey: "backendUrl") { + return URL(string: url) + } + return nil + } + set { + UserDefaults.standard.setValue(newValue?.absoluteString, forKey: "backendUrl") } - return nil } static var backendFormat: OutputFormatSetting { - OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "backendFormat")) ?? .all + get { + OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "backendFormat")) ?? .gifOnly + } + set { + UserDefaults.standard.setValue(newValue.rawValue, forKey: "backendFormat") + } } static var shouldKeepLocalFiles: Bool { - UserDefaults.standard.bool(forKey: "keepFiles") + get { + UserDefaults.standard.bool(forKey: "keepFiles") + } + set { + UserDefaults.standard.set(newValue, forKey: "keepFiles") + } + } + + static var shouldAllowURLAutomation: Bool { + UserDefaults.standard.bool(forKey: "allowURLAutomation") + } + + static func apply(_ config: ConfigureAction) { + if let fps = config.fps { + frameRate = fps + } + if let outputs = config.outputs { + outputFormats = outputs + } + if let newBackend = config.backend { + backend = newBackend + } + if let backendOutput = config.backendOutput { + backendFormat = backendOutput + } + if let keepLocalFiles = config.keepLocalFiles { + shouldKeepLocalFiles = keepLocalFiles + } } } diff --git a/Captura/Data/CapturaURLDecoder.swift b/Captura/Data/CapturaURLDecoder.swift new file mode 100644 index 0000000..3fa6412 --- /dev/null +++ b/Captura/Data/CapturaURLDecoder.swift @@ -0,0 +1,144 @@ +import Foundation + + +protocol ConfigureActionProtocol { + var action: String { get } + var fps: Int? { get } + var outputs: OutputFormatSetting? { get } + var backend: URL? { get } + var backendOutput: OutputFormatSetting? { get } + var keepLocalFiles: Bool? { get } +} + +protocol RecordActionProtocol { + var action: String { get } + + var x: Int? { get } + var y: Int? { get } + var width: Int? { get } + var height: Int? { get } + var preventResize: Bool? { get } + var preventMove: Bool? { get } + var fps: Int? { get } + var backend: URL? { get } + var outputs: OutputFormatSetting? { get } + var backendOutput: OutputFormatSetting? { get } + var keepLocalFiles: Bool? { get } + var autoStart: Bool? { get } +} + +// The concrete implementations +struct ConfigureAction: ConfigureActionProtocol { + let action: String + var fps: Int? + var outputs: OutputFormatSetting? + var backend: URL? + var backendOutput: OutputFormatSetting? + var keepLocalFiles: Bool? +} + +struct RecordAction: RecordActionProtocol { + let action: String + var x: Int? + var y: Int? + var width: Int? + var height: Int? + var preventResize: Bool? + var preventMove: Bool? + var fps: Int? + var outputs: OutputFormatSetting? + var backend: URL? + var backendOutput: OutputFormatSetting? + var keepLocalFiles: Bool? + var autoStart: Bool? +} + +enum CapturaAction { + case record(RecordAction) + case configure(ConfigureAction) +} + +struct CapturaURLDecoder { + + static func decodeParams(url: URL) -> CapturaAction? { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let params = components.queryItems else { + return nil + } + + var paramsDict = [String: Any]() + + params.forEach { item in + paramsDict[item.name] = item.value + } + + guard let action = paramsDict["action"] as? String else { + return nil + } + + switch action { + case "configure": + var fps = Int(paramsDict["fps"] as? String ?? "") + let backend = URL(string: paramsDict["backend"] as? String ?? "") + let keepLocalFiles = Bool(paramsDict["keep_local_files"] as? String ?? "") + let outputs = OutputFormatSetting(paramsDict["outputs"] as? String ?? "") + var backendOutput = OutputFormatSetting(paramsDict["backend_output"] as? String ?? "") + + if fps != nil { + fps = min(10, max(4, fps!)) + } + + if backendOutput == .all { + backendOutput = .gifOnly + } + + return .configure(ConfigureAction( + action: action, + fps: fps, + outputs: outputs, + backend: backend, + backendOutput: backendOutput, + keepLocalFiles: keepLocalFiles + )) + + case "record": + let x = Int(paramsDict["x"] as? String ?? "") + let y = Int(paramsDict["y"] as? String ?? "") + let width = Int(paramsDict["width"] as? String ?? "") + let height = Int(paramsDict["height"] as? String ?? "") + let preventResize = Bool(paramsDict["prevent_resize"] as? String ?? "") + let preventMove = Bool(paramsDict["prevent_move"] as? String ?? "") + var fps = Int(paramsDict["fps"] as? String ?? "") + let backend = URL(string: paramsDict["backend"] as? String ?? "") + let keepLocalFiles = Bool(paramsDict["keep_local_files"] as? String ?? "") + let outputs = OutputFormatSetting(paramsDict["outputs"] as? String ?? "") + var backendOutput = OutputFormatSetting(paramsDict["backend_output"] as? String ?? "") + + if fps != nil { + fps = min(10, max(4, fps!)) + } + + if backendOutput == .all { + backendOutput = .gifOnly + } + + return .record(RecordAction( + action: action, + x: x, + y: y, + width: width, + height: height, + preventResize: preventResize, + preventMove: preventMove, + fps: fps, + outputs: outputs, + backend: backend, + backendOutput: backendOutput, + keepLocalFiles: keepLocalFiles + )) + + default: + return nil + } + } +} diff --git a/Captura/Data/SettingsStructs.swift b/Captura/Data/SettingsStructs.swift index 99f407c..16172ca 100644 --- a/Captura/Data/SettingsStructs.swift +++ b/Captura/Data/SettingsStructs.swift @@ -5,6 +5,19 @@ enum OutputFormatSetting: Int { case mp4Only = 1 case all = 2 + init?(_ string: String) { + switch(string) { + case "gif": + self = .gifOnly + case "mp4": + self = .mp4Only + case "all": + self = .all + default: + return nil + } + } + func shouldSaveGif() -> Bool { return self == .gifOnly || self == .all } diff --git a/Captura/Domain/CaptureSessionConfiguration.swift b/Captura/Domain/CaptureSessionConfiguration.swift new file mode 100644 index 0000000..69c1ebf --- /dev/null +++ b/Captura/Domain/CaptureSessionConfiguration.swift @@ -0,0 +1,43 @@ +import Foundation + +struct CaptureSessionConfiguration { + let frameRate: Int + let outputFormats: OutputFormatSetting + let backendFormat: OutputFormatSetting + let backend: URL? + let shouldKeepLocalFiles: Bool + + init( + frameRate: Int? = nil, + outputFormats: OutputFormatSetting? = nil, + backendFormat: OutputFormatSetting? = nil, + backend: URL? = nil, + shouldKeepLocalFiles: Bool? = nil + ) { + self.frameRate = frameRate ?? CapturaSettings.frameRate + self.outputFormats = outputFormats ?? CapturaSettings.outputFormats + self.backendFormat = backendFormat ?? CapturaSettings.backendFormat + self.backend = backend ?? CapturaSettings.backend + self.shouldKeepLocalFiles = shouldKeepLocalFiles ?? CapturaSettings.shouldKeepLocalFiles + } + + var shouldSaveMp4: Bool { + outputFormats.shouldSaveMp4() || (shouldUseBackend && shouldUploadMp4) + } + + var shouldSaveGif: Bool { + outputFormats.shouldSaveGif() || (shouldUseBackend && shouldUploadGif) + } + + var shouldUploadGif: Bool { + backendFormat.shouldSaveGif() + } + + var shouldUploadMp4: Bool { + backendFormat.shouldSaveMp4() + } + + var shouldUseBackend: Bool { + backend != nil + } +} diff --git a/Captura/Info.plist b/Captura/Info.plist new file mode 100644 index 0000000..6110e45 --- /dev/null +++ b/Captura/Info.plist @@ -0,0 +1,15 @@ + + + + + CFBundleURLTypes + + + CFBundleURLSchemes + + captura + + + + + diff --git a/Captura/Presentation/Screens/PreferencesScreen.swift b/Captura/Presentation/Screens/PreferencesScreen.swift index a9c380b..9a93960 100644 --- a/Captura/Presentation/Screens/PreferencesScreen.swift +++ b/Captura/Presentation/Screens/PreferencesScreen.swift @@ -6,11 +6,11 @@ struct PreferencesScreen: View { TabView { OutputSettings().tabItem { Label("Output", systemImage: "video.fill") - }.padding(8.0) + }.padding(8.0).frame(minWidth: 300, minHeight: 130) AdvancedSettings().tabItem { Label("Advanced", systemImage: "gear") - }.padding(8.0) - }.padding(16.0).frame(minWidth: 300, minHeight: 250) + }.padding(8.0).frame(minWidth: 300, minHeight: 260) + }.padding(16.0) } } diff --git a/Captura/Presentation/Settings/AdvancedSettings.swift b/Captura/Presentation/Settings/AdvancedSettings.swift index 872d0c0..8799436 100644 --- a/Captura/Presentation/Settings/AdvancedSettings.swift +++ b/Captura/Presentation/Settings/AdvancedSettings.swift @@ -5,6 +5,8 @@ struct AdvancedSettings: View { @AppStorage("backendUrl") var backendUrl: String = "" @AppStorage("backendFormat") var outputFormats: OutputFormatSetting = .gifOnly @AppStorage("keepFiles") var keepFiles = true + @AppStorage("allowURLAutomation") var allowURLAutomation = false + @State var showConfirmation = false var parsedBackendUrl: URL? { URL(string: backendUrl) @@ -13,34 +15,59 @@ struct AdvancedSettings: View { var body: some View { Form { VStack (alignment: .center) { - LabeledContent("Backend URL") { - TextField("", text: $backendUrl).font(.body) - }.font(.headline) - .help("The Backend URL to use. If this is empty, no backend will be used and the options below won't have an effect.") - Picker(selection: $outputFormats, label: Text("Backend Format").font(.headline)) { - Text("GIF") - .tag(OutputFormatSetting.gifOnly) - .padding(.horizontal, 4.0) - .padding(.vertical, 2.0) - Text("MP4") - .tag(OutputFormatSetting.mp4Only) - .padding(.horizontal, 4.0) - .padding(.vertical, 2.0) + Section { + VStack (alignment: .center) { + LabeledContent("Backend URL") { + TextField("", text: $backendUrl).font(.body) + }.font(.headline) + .help("The Backend URL to use. If this is empty, no backend will be used and the options below won't have an effect.") + Picker(selection: $outputFormats, label: Text("Backend Format").font(.headline)) { + Text("GIF") + .tag(OutputFormatSetting.gifOnly) + .padding(.horizontal, 4.0) + .padding(.vertical, 2.0) + Text("MP4") + .tag(OutputFormatSetting.mp4Only) + .padding(.horizontal, 4.0) + .padding(.vertical, 2.0) + } + .pickerStyle(.radioGroup) + .disabled(parsedBackendUrl == nil) + .help("The format picked here will be generated regardless of what option you pick in the output settings. It doesn't prevent files from being rendered.") + Toggle("Keep Local Files", isOn: $keepFiles) + .font(.headline) + .disabled(parsedBackendUrl == nil) + .padding(.vertical, 8.0) + .help("If this is off, locally generated recordings will be deleted immediately after a successful upload.") + HStack { + Text("These settings can break things! Please make sure you understand how to use them before enabling.") + .lineLimit(3...10) + Link(destination: URL(string: "https://captura.tranquil.systems")!) { + Image(systemName: "info.circle") + }.buttonStyle(.borderless) + } + } } - .pickerStyle(.radioGroup) - .disabled(parsedBackendUrl == nil) - .help("The format picked here will be generated regardless of what option you pick in the output settings. It doesn't prevent files from being rendered.") - Toggle("Keep Local Files", isOn: $keepFiles) - .font(.headline) - .disabled(parsedBackendUrl == nil) - .padding(.vertical, 8.0) - .help("If this is off, locally generated recordings will be deleted immediately after a successful upload.") - HStack { - Text("These settings can break things! Please make sure you understand how to use them before enabling.") - .lineLimit(3...10) - Link(destination: URL(string: "https://captura.tranquil.systems")!) { - Image(systemName: "info.circle") - }.buttonStyle(.borderless) + Divider().padding(.vertical, 8.0) + Section { + Toggle("Allow URL Based Automation", isOn: $allowURLAutomation) + .font(.headline) + .padding(.vertical, 8.0) + .help("If this is on, the app can be controlled remotely using the captura: URL scheme.") + .confirmationDialog("This may be dangerous and can allow websites to remotely record your computer.", isPresented: $showConfirmation, actions: { + Button("I Understand The Risk", role: .destructive) { + showConfirmation = false + } + Button("Cancel", role: .cancel) { + showConfirmation = false + allowURLAutomation = false + } + }) + .onChange(of: allowURLAutomation, perform: { newValue in + if newValue { + showConfirmation = true + } + }) } Spacer() }