]> git.r.bdr.sh - rbdr/captura/commitdiff
Allow URL configuration
authorRuben Beltran del Rio <redacted>
Mon, 31 Jul 2023 14:58:57 +0000 (16:58 +0200)
committerRuben Beltran del Rio <redacted>
Mon, 31 Jul 2023 14:58:57 +0000 (16:58 +0200)
Captura.xcodeproj/project.pbxproj
Captura/CapturaApp.swift
Captura/Data/CapturaSettings.swift
Captura/Data/CapturaURLDecoder.swift [new file with mode: 0644]
Captura/Data/SettingsStructs.swift
Captura/Domain/CaptureSessionConfiguration.swift [new file with mode: 0644]
Captura/Info.plist [new file with mode: 0644]
Captura/Presentation/Screens/PreferencesScreen.swift
Captura/Presentation/Settings/AdvancedSettings.swift

index 388af03fa5d897a18d2860586bb43d3f07369602..4c6ba9a48e7d24b3af17066e3a03748c3edec52b 100644 (file)
@@ -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 */; };
                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 */; };
                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 = "<group>"; };
                B5278B3F2A744297009F6462 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
                B5278B412A779CDB009F6462 /* BackendResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendResponse.swift; sourceTree = "<group>"; };
                B5278B3D2A74420F009F6462 /* CapturaRemoteFile.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CapturaRemoteFile.xcdatamodel; sourceTree = "<group>"; };
                B5278B3F2A744297009F6462 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
                B5278B412A779CDB009F6462 /* BackendResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendResponse.swift; sourceTree = "<group>"; };
+               B5278B432A77B43A009F6462 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
+               B5278B442A77D924009F6462 /* CaptureSessionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureSessionConfiguration.swift; sourceTree = "<group>"; };
+               B5278B462A77E8D7009F6462 /* CapturaURLDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaURLDecoder.swift; sourceTree = "<group>"; };
                B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+AppEvents.swift"; sourceTree = "<group>"; };
                B55DDFCD2A6F069D001A5E76 /* RecordingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingWindow.swift; sourceTree = "<group>"; };
                B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureState.swift; sourceTree = "<group>"; };
                B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+AppEvents.swift"; sourceTree = "<group>"; };
                B55DDFCD2A6F069D001A5E76 /* RecordingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingWindow.swift; sourceTree = "<group>"; };
                B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureState.swift; sourceTree = "<group>"; };
                                B5278B3C2A74420F009F6462 /* Captura.xcdatamodeld */,
                                B5278B3F2A744297009F6462 /* Persistence.swift */,
                                B5278B412A779CDB009F6462 /* BackendResponse.swift */,
                                B5278B3C2A74420F009F6462 /* Captura.xcdatamodeld */,
                                B5278B3F2A744297009F6462 /* Persistence.swift */,
                                B5278B412A779CDB009F6462 /* BackendResponse.swift */,
+                               B5278B462A77E8D7009F6462 /* CapturaURLDecoder.swift */,
                        );
                        path = Data;
                        sourceTree = "<group>";
                        );
                        path = Data;
                        sourceTree = "<group>";
                        children = (
                                B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */,
                                B5278B352A73B3AA009F6462 /* CapturaCaptureSession.swift */,
                        children = (
                                B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */,
                                B5278B352A73B3AA009F6462 /* CapturaCaptureSession.swift */,
+                               B5278B442A77D924009F6462 /* CaptureSessionConfiguration.swift */,
                        );
                        path = Domain;
                        sourceTree = "<group>";
                        );
                        path = Domain;
                        sourceTree = "<group>";
                B5F915502A6EF80D007ECE8E /* Captura */ = {
                        isa = PBXGroup;
                        children = (
                B5F915502A6EF80D007ECE8E /* Captura */ = {
                        isa = PBXGroup;
                        children = (
+                               B5278B432A77B43A009F6462 /* Info.plist */,
                                B5F915512A6EF80D007ECE8E /* CapturaApp.swift */,
                                B5278B262A739862009F6462 /* Core Extensions */,
                                B5278B1C2A71BD3C009F6462 /* Data */,
                                B5F915512A6EF80D007ECE8E /* CapturaApp.swift */,
                                B5278B262A739862009F6462 /* Core Extensions */,
                                B5278B1C2A71BD3C009F6462 /* Data */,
                                B5278B382A73D1EE009F6462 /* CapturaSettings.swift in Sources */,
                                B5278B282A739871009F6462 /* CGImage+resize.swift in Sources */,
                                B5278B422A779CDB009F6462 /* BackendResponse.swift in Sources */,
                                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 */,
                                B5278B2C2A739B3A009F6462 /* CapturaFile.swift in Sources */,
                                B5278B1F2A71BD9B009F6462 /* OutputSettings.swift in Sources */,
                                B5278B2A2A73992D009F6462 /* GifRenderer.swift in Sources */,
                                B5278B362A73B3AA009F6462 /* CapturaCaptureSession.swift in Sources */,
                                B5278B3E2A74420F009F6462 /* Captura.xcdatamodeld in Sources */,
                                B5F915522A6EF80D007ECE8E /* CapturaApp.swift in Sources */,
                                B5278B362A73B3AA009F6462 /* CapturaCaptureSession.swift in Sources */,
                                B5278B3E2A74420F009F6462 /* Captura.xcdatamodeld in Sources */,
                                B5F915522A6EF80D007ECE8E /* CapturaApp.swift in Sources */,
+                               B5278B472A77E8D7009F6462 /* CapturaURLDecoder.swift in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                                ENABLE_HARDENED_RUNTIME = YES;
                                ENABLE_PREVIEWS = YES;
                                GENERATE_INFOPLIST_FILE = YES;
                                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 = (
                                INFOPLIST_KEY_LSUIElement = YES;
                                INFOPLIST_KEY_NSHumanReadableCopyright = "";
                                LD_RUNPATH_SEARCH_PATHS = (
                                ENABLE_HARDENED_RUNTIME = YES;
                                ENABLE_PREVIEWS = YES;
                                GENERATE_INFOPLIST_FILE = YES;
                                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 = (
                                INFOPLIST_KEY_LSUIElement = YES;
                                INFOPLIST_KEY_NSHumanReadableCopyright = "";
                                LD_RUNPATH_SEARCH_PATHS = (
index e1c4e7e87b78c3017690d473aa89e4f45216a1b8..d9c3ff0bcfffd18786ae622bda386b149bf34733 100644 (file)
@@ -34,10 +34,10 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
   var images: [CGImage] = []
   var outputFile: CapturaFile? = nil
   var gifCallbackTimer = ContinuousClock.now
   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 pixelDensity: CGFloat = 1.0
   var stopTimer: DispatchWorkItem?
   var remoteFiles: [CapturaRemoteFile] = []
+  var captureSessionConfiguration: CaptureSessionConfiguration = CaptureSessionConfiguration()
   
   func applicationDidFinishLaunching(_ notification: Notification) {
     setupStatusBar()
   
   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) {
   // MARK: - UI Event Handlers
   
   func menuWillOpen(_ menu: NSMenu) {
@@ -211,7 +237,6 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
   func startRecording() {
     captureState = .recording
     updateImage()
   func startRecording() {
     captureState = .recording
     updateImage()
-    fps = CapturaSettings.frameRate
     outputFile = nil
     images = [];
     pixelDensity = recordingWindow?.pixelDensity ?? 1.0
     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()
           DispatchQueue.main.asyncAfter(deadline: .now() + 300, execute: stopTimer!)
           
           outputFile = CapturaFile()
-          if CapturaSettings.shouldSaveMp4 {
+          if captureSessionConfiguration.shouldSaveMp4 {
             captureSession.startRecording(to: outputFile!.mp4URL)
           } else {
             captureSession.startRunning()
             captureSession.startRecording(to: outputFile!.mp4URL)
           } else {
             captureSession.startRunning()
@@ -246,9 +271,9 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
     stop()
     
     Task.detached {
     stop()
     
     Task.detached {
-      if CapturaSettings.shouldSaveGif {
+      if self.captureSessionConfiguration.shouldSaveGif {
         if let outputFile = self.outputFile {
         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()
         }
       }
       let wasSuccessful = await self.uploadOrCopy()
@@ -277,7 +302,7 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
   func receivedFrame(_ frame: CVImageBuffer) {
     let now = ContinuousClock.now
     
   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) {
       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
     boxListener?.cancel()
     recordingWindow?.close()
     recordingWindow = nil
+    captureSessionConfiguration = CaptureSessionConfiguration()
   }
   
   private func uploadOrCopy() async -> Bool {
   }
   
   private func uploadOrCopy() async -> Bool {
-    if CapturaSettings.shouldUseBackend {
+    if captureSessionConfiguration.shouldUseBackend {
       let result = await uploadToBackend()
       let result = await uploadToBackend()
-      if result && !CapturaSettings.shouldKeepLocalFiles {
+      if result && !captureSessionConfiguration.shouldKeepLocalFiles {
         deleteLocalFiles()
       }
       return result
         deleteLocalFiles()
       }
       return result
@@ -374,8 +400,8 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
   }
   
   private func copyLocalToClipboard() {
   }
   
   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)
       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 {
   }
   
   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 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
           var request = URLRequest(url: remoteUrl)
           request.httpMethod = "POST"
           request.httpBody = data
@@ -424,12 +450,12 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
   }
   
   private func deleteLocalFiles() {
   }
   
   private func deleteLocalFiles() {
-    if CapturaSettings.shouldSaveGif {
+    if captureSessionConfiguration.shouldSaveGif {
       if let url = outputFile?.gifURL {
           try? FileManager.default.removeItem(at: url)
       }
     }
       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)
       }
       if let url = outputFile?.mp4URL {
         try? FileManager.default.removeItem(at: url)
       }
index 01ef78c56bf222a1ec2f960e284cff28134094e9..85368ea0695240404ce303ef403f6ccd82bed62e 100644 (file)
@@ -2,11 +2,21 @@ import Foundation
 
 struct CapturaSettings {
   static var frameRate: Int {
 
 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 {
   }
   
   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 {
   }
   
   static var shouldSaveMp4: Bool {
@@ -30,17 +40,54 @@ struct CapturaSettings {
   }
   
   static var backend: URL? {
   }
   
   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 {
   }
   
   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 {
   }
   
   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 (file)
index 0000000..3fa6412
--- /dev/null
@@ -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
+      }
+  }
+}
index 99f407c256a4da3d4c9bb2c9864c6e353f836e6e..16172cad331e17fe0d5e57c72b149d11d2f15d08 100644 (file)
@@ -5,6 +5,19 @@ enum OutputFormatSetting: Int {
   case mp4Only = 1
   case all = 2
   
   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
   }
   func shouldSaveGif() -> Bool {
     return self == .gifOnly || self == .all
   }
diff --git a/Captura/Domain/CaptureSessionConfiguration.swift b/Captura/Domain/CaptureSessionConfiguration.swift
new file mode 100644 (file)
index 0000000..69c1ebf
--- /dev/null
@@ -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 (file)
index 0000000..6110e45
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>CFBundleURLTypes</key>
+       <array>
+               <dict>
+                       <key>CFBundleURLSchemes</key>
+                       <array>
+                               <string>captura</string>
+                       </array>
+               </dict>
+       </array>
+</dict>
+</plist>
index a9c380b8a57fb5ed1f9eb6218f962472d1679b99..9a93960736aecbb80b9023e664df63a3ac1d0344 100644 (file)
@@ -6,11 +6,11 @@ struct PreferencesScreen: View {
       TabView {
         OutputSettings().tabItem {
           Label("Output", systemImage: "video.fill")
       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")
         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)
     }
 }
 
     }
 }
 
index 872d0c0fa4c698899a3b5b64d9692d47ac5c540c..8799436781ff8a7a4e86bcde88b5a0faf12feecc 100644 (file)
@@ -5,6 +5,8 @@ struct AdvancedSettings: View {
   @AppStorage("backendUrl") var backendUrl: String = ""
   @AppStorage("backendFormat") var outputFormats: OutputFormatSetting = .gifOnly
   @AppStorage("keepFiles") var keepFiles = true
   @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)
   
   var parsedBackendUrl: URL? {
     URL(string: backendUrl)
@@ -13,34 +15,59 @@ struct AdvancedSettings: View {
   var body: some View {
     Form {
       VStack (alignment: .center) {
   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()
       }
         }
         Spacer()
       }