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 */; };
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>"; };
B5278B3C2A74420F009F6462 /* Captura.xcdatamodeld */,
B5278B3F2A744297009F6462 /* Persistence.swift */,
B5278B412A779CDB009F6462 /* BackendResponse.swift */,
+ B5278B462A77E8D7009F6462 /* CapturaURLDecoder.swift */,
);
path = Data;
sourceTree = "<group>";
children = (
B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */,
B5278B352A73B3AA009F6462 /* CapturaCaptureSession.swift */,
+ B5278B442A77D924009F6462 /* CaptureSessionConfiguration.swift */,
);
path = Domain;
sourceTree = "<group>";
B5F915502A6EF80D007ECE8E /* Captura */ = {
isa = PBXGroup;
children = (
+ B5278B432A77B43A009F6462 /* Info.plist */,
B5F915512A6EF80D007ECE8E /* CapturaApp.swift */,
B5278B262A739862009F6462 /* Core Extensions */,
B5278B1C2A71BD3C009F6462 /* Data */,
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 */,
B5278B362A73B3AA009F6462 /* CapturaCaptureSession.swift in Sources */,
B5278B3E2A74420F009F6462 /* Captura.xcdatamodeld in Sources */,
B5F915522A6EF80D007ECE8E /* CapturaApp.swift in Sources */,
+ B5278B472A77E8D7009F6462 /* CapturaURLDecoder.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
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 = (
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 = (
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()
}
}
+ // 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) {
func startRecording() {
captureState = .recording
updateImage()
- fps = CapturaSettings.frameRate
outputFile = nil
images = [];
pixelDensity = recordingWindow?.pixelDensity ?? 1.0
DispatchQueue.main.asyncAfter(deadline: .now() + 300, execute: stopTimer!)
outputFile = CapturaFile()
- if CapturaSettings.shouldSaveMp4 {
+ if captureSessionConfiguration.shouldSaveMp4 {
captureSession.startRecording(to: outputFile!.mp4URL)
} else {
captureSession.startRunning()
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()
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) {
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
}
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)
}
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
}
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)
}
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 {
}
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
+ }
}
}
--- /dev/null
+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
+ }
+ }
+}
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
}
--- /dev/null
+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
+ }
+}
--- /dev/null
+<?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>
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)
}
}
@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 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()
}