From: Ruben Beltran del Rio Date: Mon, 16 Sep 2024 09:07:46 +0000 (+0200) Subject: Format the code X-Git-Url: https://git.r.bdr.sh/rbdr/captura/commitdiff_plain/505c1e620497828ffb914e05dd76d9ab124f144a?ds=sidebyside;hp=5802c153cae64142d84e3cd5f762939501ee7e53 Format the code --- diff --git a/Captura/CapturaApp.swift b/Captura/CapturaApp.swift index dc358f7..5fe8858 100644 --- a/Captura/CapturaApp.swift +++ b/Captura/CapturaApp.swift @@ -1,3 +1,7 @@ +import AVFoundation +import Cocoa +import Combine +import Sparkle /* Copyright (C) 2024 Rubén Beltrán del Río @@ -15,29 +19,28 @@ along with this program. If not, see https://captura.tranquil.systems. */ import SwiftUI -import Cocoa -import Combine -import AVFoundation -import Sparkle @main struct CapturaApp: App { - @NSApplicationDelegateAdaptor(CapturaAppDelegate.self) var appDelegate + @NSApplicationDelegateAdaptor(CapturaAppDelegate.self) var appDelegate - var body: some Scene { - WindowGroup { - PreferencesScreen() - .handlesExternalEvents(preferring: Set(arrayLiteral: "PreferencesScreen"), allowing: Set(arrayLiteral: "*")) - .frame(width: 650, height: 450) - } - .handlesExternalEvents(matching: Set(arrayLiteral: "PreferencesScreen")) - //.modelContainer(for: CapturaRemoteFile.self) - } + var body: some Scene { + WindowGroup { + PreferencesScreen() + .handlesExternalEvents( + preferring: Set(arrayLiteral: "PreferencesScreen"), allowing: Set(arrayLiteral: "*") + ) + .frame(width: 650, height: 450) + } + .handlesExternalEvents(matching: Set(arrayLiteral: "PreferencesScreen")) + //.modelContainer(for: CapturaRemoteFile.self) + } } -@objc(CapturaAppDelegate) class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { - +@objc(CapturaAppDelegate) class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate +{ + @Environment(\.openURL) var openURL var statusItem: NSStatusItem! var captureState: CaptureState = .idle @@ -54,13 +57,14 @@ struct CapturaApp: App { var stopTimer: DispatchWorkItem? var remoteFiles: [CapturaRemoteFile] = [] var captureSessionConfiguration: CaptureSessionConfiguration = CaptureSessionConfiguration() - + // Sparkle Configuration @IBOutlet var checkForUpdatesMenuItem: NSMenuItem! - let updaterController: SPUStandardUpdaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) - + let updaterController: SPUStandardUpdaterController = SPUStandardUpdaterController( + startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + @objc dynamic var scriptedPreferences: ScriptedPreferences = ScriptedPreferences() - + func applicationDidFinishLaunching(_ notification: Notification) { setupStatusBar() NotificationCenter.default.addObserver( @@ -71,54 +75,67 @@ struct CapturaApp: App { closeWindow() fetchRemoteItems() } - + // MARK: - Setup Functions - - + private func setupStatusBar() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - + if let button = statusItem.button { button.image = NSImage(named: "Idle") } - + statusItem.isVisible = true statusItem.menu = NSMenu() statusItem.menu?.delegate = self - + // Create the Popover popover = NSPopover() popover?.contentViewController = HelpPopoverViewController() popover?.behavior = .transient - + setupMenu() } - + private func setupMenu() { - + statusItem.menu?.removeAllItems() - - statusItem.menu?.addItem(NSMenuItem(title: "Record", action: #selector(CapturaAppDelegate.onClickStartRecording), keyEquivalent: "")) - if (remoteFiles.count > 0) { + + statusItem.menu?.addItem( + NSMenuItem( + title: "Record", action: #selector(CapturaAppDelegate.onClickStartRecording), + keyEquivalent: "")) + if remoteFiles.count > 0 { statusItem.menu?.addItem(NSMenuItem.separator()) for remoteFile in remoteFiles { - let remoteFileItem = NSMenuItem(title: remoteFile.name, action: #selector(CapturaAppDelegate.onClickRemoteFile), keyEquivalent: "") + let remoteFileItem = NSMenuItem( + title: remoteFile.name, action: #selector(CapturaAppDelegate.onClickRemoteFile), + keyEquivalent: "") remoteFileItem.representedObject = remoteFile statusItem.menu?.addItem(remoteFileItem) } } statusItem.menu?.addItem(NSMenuItem.separator()) - statusItem.menu?.addItem(NSMenuItem(title: "Open Local Folder", action: #selector(CapturaAppDelegate.onOpenFolder), keyEquivalent: "")) + statusItem.menu?.addItem( + NSMenuItem( + title: "Open Local Folder", action: #selector(CapturaAppDelegate.onOpenFolder), + keyEquivalent: "")) statusItem.menu?.addItem(NSMenuItem.separator()) - - checkForUpdatesMenuItem = NSMenuItem(title: "Check for Updates", action: #selector(SPUStandardUpdaterController.checkForUpdates(_:)), keyEquivalent: "") + + checkForUpdatesMenuItem = NSMenuItem( + title: "Check for Updates", + action: #selector(SPUStandardUpdaterController.checkForUpdates(_:)), keyEquivalent: "") checkForUpdatesMenuItem.target = updaterController statusItem.menu?.addItem(checkForUpdatesMenuItem) - - statusItem.menu?.addItem(NSMenuItem(title: "Preferences", action: #selector(CapturaAppDelegate.onOpenPreferences), keyEquivalent: "")) - statusItem.menu?.addItem(NSMenuItem(title: "Quit", action: #selector(CapturaAppDelegate.onQuit), keyEquivalent: "")) + + statusItem.menu?.addItem( + NSMenuItem( + title: "Preferences", action: #selector(CapturaAppDelegate.onOpenPreferences), + keyEquivalent: "")) + statusItem.menu?.addItem( + NSMenuItem(title: "Quit", action: #selector(CapturaAppDelegate.onQuit), keyEquivalent: "")) } - + private func closeWindow() { if let window = NSApplication.shared.windows.first { window.close() @@ -126,20 +143,24 @@ struct CapturaApp: App { } // MARK: - URL Event Handler - + func application(_ application: NSApplication, open urls: [URL]) { - if (CapturaSettings.shouldAllowURLAutomation) { + if CapturaSettings.shouldAllowURLAutomation { for url in urls { if let action = CapturaURLDecoder.decodeParams(url: url) { switch action { - case let .configure(config): - NotificationCenter.default.post(name: .setConfiguration, object: nil, userInfo: [ - "config": config - ]) - case let .record(config): - NotificationCenter.default.post(name: .setCaptureSessionConfiguration, object: nil, userInfo: [ - "config": config - ]) + case let .configure(config): + NotificationCenter.default.post( + name: .setConfiguration, object: nil, + userInfo: [ + "config": config + ]) + case let .record(config): + NotificationCenter.default.post( + name: .setCaptureSessionConfiguration, object: nil, + userInfo: [ + "config": config + ]) NotificationCenter.default.post(name: .startAreaSelection, object: nil, userInfo: nil) } } @@ -147,15 +168,16 @@ struct CapturaApp: App { } 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() + 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) { if captureState != .idle { menu.cancelTrackingWithoutAnimation() @@ -169,27 +191,29 @@ struct CapturaApp: App { } } } - + @objc private func onClickStartRecording() { NotificationCenter.default.post(name: .startAreaSelection, object: nil, userInfo: nil) } - + @objc private func onOpenPreferences() { NSApp.activate(ignoringOtherApps: true) if preferencesWindow == nil { - preferencesWindow = PreferencesWindow() + preferencesWindow = PreferencesWindow() } else { preferencesWindow?.makeKeyAndOrderFront(nil) preferencesWindow?.orderFrontRegardless() } } - + @objc private func onOpenFolder() { - if let directory = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first?.appendingPathComponent("captura") { + if let directory = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first? + .appendingPathComponent("captura") + { NSWorkspace.shared.open(directory) } } - + @objc private func onClickRemoteFile(_ sender: NSMenuItem) { if let remoteFile = sender.representedObject as? CapturaRemoteFile { if let urlString = remoteFile.url { @@ -199,15 +223,15 @@ struct CapturaApp: App { } } } - + @objc private func onQuit() { NSApplication.shared.terminate(self) } - + // MARK: - App State Event Listeners - + @objc func didReceiveNotification(_ notification: Notification) { - switch(notification.name) { + switch notification.name { case .startAreaSelection: startAreaSelection() case .startRecording: @@ -241,7 +265,7 @@ struct CapturaApp: App { } } case .reloadConfiguration: - reloadConfiguration() + reloadConfiguration() case .setCaptureSessionConfiguration: if let userInfo = notification.userInfo { if let config = userInfo["config"] as? RecordAction { @@ -257,8 +281,7 @@ struct CapturaApp: App { return } } - - + func startAreaSelection() { helpShown = false if captureState != .selectingArea { @@ -285,25 +308,26 @@ struct CapturaApp: App { } } } - + func startRecording() { captureState = .recording updateImage() outputFile = nil - images = []; + images = [] pixelDensity = recordingWindow?.pixelDensity ?? 1.0 recordingWindow?.recordingContentView.startRecording() if let box = recordingWindow?.recordingContentView.box { if let screen = recordingWindow?.screen { captureSession = CapturaCaptureSession(screen, box: box) - + if let captureSession { stopTimer = DispatchWorkItem { self.stopRecording() } - DispatchQueue.main.asyncAfter(deadline: .now() + Double(captureSessionConfiguration.maxLength), execute: stopTimer!) - + DispatchQueue.main.asyncAfter( + deadline: .now() + Double(captureSessionConfiguration.maxLength), execute: stopTimer!) + outputFile = CapturaFile() if captureSessionConfiguration.shouldSaveMp4 { captureSession.startRecording(to: outputFile!.mp4URL) @@ -316,16 +340,17 @@ struct CapturaApp: App { } NotificationCenter.default.post(name: .failedToStart, object: nil, userInfo: nil) } - + func stopRecording() { captureState = .uploading updateImage() stop() - + Task.detached { if self.captureSessionConfiguration.shouldSaveGif { if let outputFile = self.outputFile { - await GifRenderer.render(self.images, at: self.captureSessionConfiguration.frameRate, to: outputFile.gifURL) + await GifRenderer.render( + self.images, at: self.captureSessionConfiguration.frameRate, to: outputFile.gifURL) } } let wasSuccessful = await self.uploadOrCopy() @@ -336,7 +361,7 @@ struct CapturaApp: App { } } } - + func finalizeRecording() { captureState = .uploaded updateImage() @@ -344,18 +369,20 @@ struct CapturaApp: App { NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) } } - + func reset() { captureState = .idle updateImage() captureSessionConfiguration = CaptureSessionConfiguration() stop() } - + func receivedFrame(_ frame: CVImageBuffer) { let now = ContinuousClock.now - - if now - gifCallbackTimer > .nanoseconds(1_000_000_000 / UInt64(captureSessionConfiguration.frameRate)) { + + if now - gifCallbackTimer + > .nanoseconds(1_000_000_000 / UInt64(captureSessionConfiguration.frameRate)) + { gifCallbackTimer = now DispatchQueue.main.async { if var cgImage = frame.cgImage { @@ -367,7 +394,7 @@ struct CapturaApp: App { } } } - + func failed(_ requestPermission: Bool = false) { captureState = .error updateImage() @@ -379,41 +406,42 @@ struct CapturaApp: App { NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) } } - + func setConfiguration(_ config: ConfigureAction) { CapturaSettings.apply(config) } - + func reloadConfiguration() { self.captureSessionConfiguration = CaptureSessionConfiguration() } - + func setCaptureSessionConfiguration(_ config: RecordAction) { self.captureSessionConfiguration = CaptureSessionConfiguration(from: config) } - + // MARK: - CoreData - + private func fetchRemoteItems() { let viewContext = PersistenceController.shared.container.viewContext let fetchRequest = NSFetchRequest(entityName: "CapturaRemoteFile") fetchRequest.fetchLimit = 5 fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: false)] - + let results = try? viewContext.fetch(fetchRequest) remoteFiles = results ?? [] } - + // MARK: - Presentation Helpers - - + private func requestPermissionToRecord() { showPopoverWithMessage("Please grant Captura permission to record") - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenRecording") { + if let url = URL( + string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenRecording") + { NSWorkspace.shared.open(url) } } - + private func showPopoverWithMessage(_ message: String) { if let button = statusItem.button { (self.popover?.contentViewController as? HelpPopoverViewController)?.updateLabel(message) @@ -423,31 +451,32 @@ struct CapturaApp: App { } } } - + private func updateImage() { if let button = statusItem.button { - let image: String = switch captureState { - case .idle: - "Idle" - case .selectingArea: - if recordingWindow?.recordingContentView.box != nil { - "Ready to Record" - } else { - "Selecting" + let image: String = + switch captureState { + case .idle: + "Idle" + case .selectingArea: + if recordingWindow?.recordingContentView.box != nil { + "Ready to Record" + } else { + "Selecting" + } + case .recording: + "Stop Frame 1" + case .uploading: + "Upload Frame 1" + case .uploaded: + "OK" + case .error: + "ERR" } - case .recording: - "Stop Frame 1" - case .uploading: - "Upload Frame 1" - case .uploaded: - "OK" - case .error: - "ERR" - } button.image = NSImage(named: image) } } - + private func stop() { stopTimer?.cancel() captureSession?.stopRunning() @@ -456,7 +485,7 @@ struct CapturaApp: App { recordingWindow?.close() recordingWindow = nil } - + private func uploadOrCopy() async -> Bool { if captureSessionConfiguration.shouldUseBackend { let result = await uploadToBackend() @@ -469,10 +498,12 @@ struct CapturaApp: App { return true } } - + private func copyLocalToClipboard() { - let fileType: NSPasteboard.PasteboardType = .init(rawValue: captureSessionConfiguration.shouldSaveGif ? "com.compuserve.gif" : "public.mpeg-4") - if let url = captureSessionConfiguration.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) @@ -480,10 +511,12 @@ struct CapturaApp: App { } } } - + private func uploadToBackend() async -> Bool { let contentType = captureSessionConfiguration.shouldUploadGif ? "image/gif" : "video/mp4" - if let url = captureSessionConfiguration.shouldUploadGif ? outputFile?.gifURL : outputFile?.mp4URL { + if let url = captureSessionConfiguration.shouldUploadGif + ? outputFile?.gifURL : outputFile?.mp4URL + { if let data = try? Data(contentsOf: url) { if let remoteUrl = captureSessionConfiguration.backend { var request = URLRequest(url: remoteUrl) @@ -491,10 +524,10 @@ struct CapturaApp: App { request.httpBody = data request.setValue(contentType, forHTTPHeaderField: "Content-Type") request.setValue("Captura/1.0", forHTTPHeaderField: "User-Agent") - + do { let (data, response) = try await URLSession.shared.data(for: request) - + if let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode == 201 { let answer = try JSONDecoder().decode(BackendResponse.self, from: data) @@ -508,7 +541,7 @@ struct CapturaApp: App { } return false } - + private func createRemoteFile(_ url: URL) { let viewContext = PersistenceController.shared.container.viewContext let remoteFile = CapturaRemoteFile(context: viewContext) @@ -519,11 +552,11 @@ struct CapturaApp: App { pasteboard.declareTypes([.URL], owner: nil) pasteboard.setString(url.absoluteString, forType: .string) } - + private func deleteLocalFiles() { if captureSessionConfiguration.shouldSaveGif { if let url = outputFile?.gifURL { - try? FileManager.default.removeItem(at: url) + try? FileManager.default.removeItem(at: url) } } if captureSessionConfiguration.shouldSaveMp4 { diff --git a/Captura/Core Extensions/CGImage+resize.swift b/Captura/Core Extensions/CGImage+resize.swift index 02aaa6e..9d1adaa 100644 --- a/Captura/Core Extensions/CGImage+resize.swift +++ b/Captura/Core Extensions/CGImage+resize.swift @@ -20,18 +20,22 @@ extension CGImage { func resize(by scale: CGFloat) -> CGImage? { let width = Int(CGFloat(self.width) / scale) let height = Int(CGFloat(self.height) / scale) - + let bitsPerComponent = self.bitsPerComponent let colorSpace = self.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)! let bitmapInfo = self.bitmapInfo.rawValue - - guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo) else { - return nil + + guard + let context = CGContext( + data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: 0, + space: colorSpace, bitmapInfo: bitmapInfo) + else { + return nil } - + context.interpolationQuality = .high context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height)) - + return context.makeImage() } } diff --git a/Captura/Core Extensions/CVImageBuffer+cgImage.swift b/Captura/Core Extensions/CVImageBuffer+cgImage.swift index eb62e7a..ba98855 100644 --- a/Captura/Core Extensions/CVImageBuffer+cgImage.swift +++ b/Captura/Core Extensions/CVImageBuffer+cgImage.swift @@ -18,19 +18,20 @@ import Foundation import ReplayKit extension CVImageBuffer { - + private static let contextQueue = DispatchQueue(label: "com.example.contextQueue") static let sharedContext: CIContext = { - return CIContext() + return CIContext() }() - + var cgImage: CGImage? { var result: CGImage? CVImageBuffer.contextQueue.sync { let ciImage = CIImage(cvImageBuffer: self) let width = CVPixelBufferGetWidth(self) let height = CVPixelBufferGetHeight(self) - result = CVImageBuffer.sharedContext.createCGImage(ciImage, from: CGRect(x: 0, y: 0, width: width, height: height)) + result = CVImageBuffer.sharedContext.createCGImage( + ciImage, from: CGRect(x: 0, y: 0, width: width, height: height)) } return result } diff --git a/Captura/Data/BackendResponse.swift b/Captura/Data/BackendResponse.swift index f4316de..92636f0 100644 --- a/Captura/Data/BackendResponse.swift +++ b/Captura/Data/BackendResponse.swift @@ -15,6 +15,7 @@ along with this program. If not, see https://captura.tranquil.systems. */ import Foundation + struct BackendResponse: Decodable { - let url: URL + let url: URL } diff --git a/Captura/Data/CapturaFile.swift b/Captura/Data/CapturaFile.swift index 798c510..125e904 100644 --- a/Captura/Data/CapturaFile.swift +++ b/Captura/Data/CapturaFile.swift @@ -17,34 +17,36 @@ import Foundation struct CapturaFile { - + let name: String let baseDirectory: URL let appDirectory: String = "captura" - + private var baseURL: URL { baseDirectory.appendingPathComponent("\(appDirectory)/\(name)") } - + var mp4URL: URL { return baseURL.appendingPathExtension("mp4") } - + var gifURL: URL { return baseURL.appendingPathExtension("gif") } - + init() { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium dateFormatter.timeStyle = .medium dateFormatter.locale = Locale.current let dateString = dateFormatter.string(from: Date()).replacingOccurrences(of: ":", with: ".") - + self.name = "Captura \(dateString)" - self.baseDirectory = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first! - try? FileManager.default.createDirectory(at: self.baseDirectory.appendingPathComponent(appDirectory), - withIntermediateDirectories: true, - attributes: nil) + self.baseDirectory = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask) + .first! + try? FileManager.default.createDirectory( + at: self.baseDirectory.appendingPathComponent(appDirectory), + withIntermediateDirectories: true, + attributes: nil) } } diff --git a/Captura/Data/CapturaSettings.swift b/Captura/Data/CapturaSettings.swift index c8af096..bfaa1ce 100644 --- a/Captura/Data/CapturaSettings.swift +++ b/Captura/Data/CapturaSettings.swift @@ -29,7 +29,7 @@ struct CapturaSettings { UserDefaults.standard.setValue(newValue, forKey: "frameRate") } } - + static var outputFormats: OutputFormatSetting { get { OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "outputFormats")) ?? .all @@ -38,27 +38,27 @@ struct CapturaSettings { UserDefaults.standard.setValue(newValue.rawValue, forKey: "outputFormats") } } - + static var shouldSaveMp4: Bool { outputFormats.shouldSaveMp4() || (shouldUseBackend && shouldUploadMp4) } - + static var shouldSaveGif: Bool { outputFormats.shouldSaveGif() || (shouldUseBackend && shouldUploadGif) } - + static var shouldUploadGif: Bool { backendFormat.shouldSaveGif() } - + static var shouldUploadMp4: Bool { backendFormat.shouldSaveMp4() } - + static var shouldUseBackend: Bool { backend != nil } - + static var backend: URL? { get { if let url = UserDefaults.standard.string(forKey: "backendUrl") { @@ -70,16 +70,17 @@ struct CapturaSettings { UserDefaults.standard.setValue(newValue?.absoluteString, forKey: "backendUrl") } } - + static var backendFormat: OutputFormatSetting { get { - OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "backendFormat")) ?? .gifOnly + OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "backendFormat")) + ?? .gifOnly } set { UserDefaults.standard.setValue(newValue.rawValue, forKey: "backendFormat") } } - + static var shouldKeepLocalFiles: Bool { get { if UserDefaults.standard.object(forKey: "keepFiles") == nil { @@ -92,7 +93,7 @@ struct CapturaSettings { UserDefaults.standard.set(newValue, forKey: "keepFiles") } } - + static var shouldAllowURLAutomation: Bool { get { UserDefaults.standard.bool(forKey: "allowURLAutomation") @@ -101,7 +102,7 @@ struct CapturaSettings { UserDefaults.standard.setValue(newValue, forKey: "allowURLAutomation") } } - + static func apply(_ config: ConfigureAction) { if let fps = config.fps { frameRate = fps diff --git a/Captura/Data/CapturaURLDecoder.swift b/Captura/Data/CapturaURLDecoder.swift index cbb8a05..a325dbe 100644 --- a/Captura/Data/CapturaURLDecoder.swift +++ b/Captura/Data/CapturaURLDecoder.swift @@ -27,7 +27,7 @@ protocol ConfigureActionProtocol { protocol RecordActionProtocol { var action: String { get } - + var x: Int? { get } var y: Int? { get } var width: Int? { get } @@ -73,45 +73,47 @@ struct RecordAction: RecordActionProtocol { } enum CapturaAction { - case record(RecordAction) - case configure(ConfigureAction) + 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 - } + 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 + } - var paramsDict = [String: Any]() + 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 ?? "") - params.forEach { item in - paramsDict[item.name] = item.value + if fps != nil { + fps = min(10, max(4, fps!)) } - guard let action = paramsDict["action"] as? String else { - return nil + if backendOutput == .all { + backendOutput = .gifOnly } - 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( + return .configure( + ConfigureAction( action: action, fps: fps, outputs: outputs, @@ -120,41 +122,42 @@ struct CapturaURLDecoder { 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 ?? "") - let autoStart = Bool(paramsDict["auto_start"] as? String ?? "") - var maxLength = Int(paramsDict["max_length"] as? String ?? "") - - if fps != nil { - fps = min(10, max(4, fps!)) - } - - if maxLength != nil { - maxLength = min(300, max(1, fps!)) - } - - if backendOutput == .all { - backendOutput = .gifOnly - } - - var skipBackend = false - if let backendString = paramsDict["backend"] as? String { - if backendString == "" { - skipBackend = true - } + 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 ?? "") + let autoStart = Bool(paramsDict["auto_start"] as? String ?? "") + var maxLength = Int(paramsDict["max_length"] as? String ?? "") + + if fps != nil { + fps = min(10, max(4, fps!)) + } + + if maxLength != nil { + maxLength = min(300, max(1, fps!)) + } + + if backendOutput == .all { + backendOutput = .gifOnly + } + + var skipBackend = false + if let backendString = paramsDict["backend"] as? String { + if backendString == "" { + skipBackend = true } - - return .record(RecordAction( + } + + return .record( + RecordAction( action: action, x: x, y: y, @@ -172,8 +175,8 @@ struct CapturaURLDecoder { maxLength: maxLength )) - default: - return nil - } + default: + return nil + } } } diff --git a/Captura/Data/GifRenderer.swift b/Captura/Data/GifRenderer.swift index bfbb289..2435cb4 100644 --- a/Captura/Data/GifRenderer.swift +++ b/Captura/Data/GifRenderer.swift @@ -1,3 +1,5 @@ +import CoreGraphics +import SwiftUI /* Copyright (C) 2024 Rubén Beltrán del Río @@ -15,16 +17,22 @@ along with this program. If not, see https://captura.tranquil.systems. */ import UniformTypeIdentifiers -import SwiftUI -import CoreGraphics struct GifRenderer { static func render(_ images: [CGImage], at fps: Int, to url: URL) async { let framedelay = String(format: "%.3f", 1.0 / Double(fps)) - let fileProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]] - let gifProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFUnclampedDelayTime as String: framedelay]] + let fileProperties = [ + kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0] + ] + let gifProperties = [ + kCGImagePropertyGIFDictionary as String: [ + kCGImagePropertyGIFUnclampedDelayTime as String: framedelay + ] + ] let cfURL = url as CFURL - if let destination = CGImageDestinationCreateWithURL(cfURL, UTType.gif.identifier as CFString, images.count, nil) { + if let destination = CGImageDestinationCreateWithURL( + cfURL, UTType.gif.identifier as CFString, images.count, nil) + { CGImageDestinationSetProperties(destination, fileProperties as CFDictionary?) for image in images { CGImageDestinationAddImage(destination, image, gifProperties as CFDictionary?) diff --git a/Captura/Data/OutputFormatSetting.swift b/Captura/Data/OutputFormatSetting.swift index fd48f2f..2131caa 100644 --- a/Captura/Data/OutputFormatSetting.swift +++ b/Captura/Data/OutputFormatSetting.swift @@ -20,9 +20,9 @@ enum OutputFormatSetting: Int { case gifOnly = 0 case mp4Only = 1 case all = 2 - + init?(_ string: String) { - switch(string) { + switch string { case "gif": self = .gifOnly case "mp4": @@ -33,17 +33,17 @@ enum OutputFormatSetting: Int { return nil } } - + func shouldSaveGif() -> Bool { return self == .gifOnly || self == .all } - + func shouldSaveMp4() -> Bool { return self == .mp4Only || self == .all } - + func toString() -> String { - switch(self) { + switch self { case .gifOnly: return "gif" case .mp4Only: diff --git a/Captura/Data/Persistence.swift b/Captura/Data/Persistence.swift index 3ae66c3..55dee56 100644 --- a/Captura/Data/Persistence.swift +++ b/Captura/Data/Persistence.swift @@ -17,36 +17,36 @@ import CoreData struct PersistenceController { - static let shared = PersistenceController() + static let shared = PersistenceController() - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - return result - }() + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + return result + }() - let container: NSPersistentCloudKitContainer + let container: NSPersistentCloudKitContainer + + init(inMemory: Bool = false) { + container = NSPersistentCloudKitContainer(name: "Captura") - init(inMemory: Bool = false) { - container = NSPersistentCloudKitContainer(name: "Captura") - #if DEBUG - do { + do { // Use the container to initialize the development schema. try container.initializeCloudKitSchema(options: []) - } catch { + } catch { // Handle any errors. - } + } #endif - - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* + + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + + /* Typical reasons for an error here include: * The parent directory does not exist, cannot be created, or disallows writing. * The persistent store is not accessible, due to permissions or data protection when the device is locked. @@ -54,9 +54,9 @@ struct PersistenceController { * The store could not be migrated to the current model version. Check the error message to determine what the actual problem was. */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - container.viewContext.automaticallyMergesChangesFromParent = true - } + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + container.viewContext.automaticallyMergesChangesFromParent = true + } } diff --git a/Captura/Domain/CapturaCaptureSession.swift b/Captura/Domain/CapturaCaptureSession.swift index 8943282..dbc72b9 100644 --- a/Captura/Domain/CapturaCaptureSession.swift +++ b/Captura/Domain/CapturaCaptureSession.swift @@ -1,64 +1,75 @@ -import AppKit import AVFoundation +import AppKit -class CapturaCaptureSession: AVCaptureSession, AVCaptureFileOutputRecordingDelegate, AVCaptureVideoDataOutputSampleBufferDelegate { +class CapturaCaptureSession: AVCaptureSession, AVCaptureFileOutputRecordingDelegate, + AVCaptureVideoDataOutputSampleBufferDelegate +{ let videoOutput = AVCaptureVideoDataOutput() let movieFileOutput = AVCaptureMovieFileOutput() var receivedFrames = false - + init(_ screen: NSScreen, box: NSRect) { super.init() - - let displayId = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! CGDirectDisplayID + + let displayId = + screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! CGDirectDisplayID let screenInput = AVCaptureScreenInput(displayID: displayId) var croppingBox = NSOffsetRect(box, -screen.frame.origin.x, -screen.frame.origin.y) if croppingBox.width.truncatingRemainder(dividingBy: 2) != 0 { croppingBox.size.width -= 1 } screenInput?.cropRect = croppingBox.insetBy(dx: 1, dy: 1) - + if self.canAddInput(screenInput!) { self.addInput(screenInput!) } - - videoOutput.setSampleBufferDelegate(self, queue: Dispatch.DispatchQueue(label: "sample buffer delegate", attributes: [])) - + + videoOutput.setSampleBufferDelegate( + self, queue: Dispatch.DispatchQueue(label: "sample buffer delegate", attributes: [])) + if self.canAddOutput(videoOutput) { self.addOutput(videoOutput) } - + if self.canAddOutput(movieFileOutput) { self.addOutput(movieFileOutput) } } - + func startRecording() { receivedFrames = false self.startRunning() - + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { if !self.receivedFrames { NotificationCenter.default.post(name: .failedToStart, object: nil, userInfo: nil) } } } - + func startRecording(to url: URL) { self.startRecording() movieFileOutput.startRecording(to: url, recordingDelegate: self) } - + // MARK: - AVCaptureVideoDataOutputSampleBufferDelegate Implementation - - func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + + func captureOutput( + _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) { receivedFrames = true - + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } - NotificationCenter.default.post(name: .receivedFrame, object: nil, userInfo: ["frame": imageBuffer]) + NotificationCenter.default.post( + name: .receivedFrame, object: nil, userInfo: ["frame": imageBuffer]) } - + // MARK: - AVCaptureFileOutputRecordingDelegate Implementation - - func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {} + + func fileOutput( + _ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, + from connections: [AVCaptureConnection], error: Error? + ) {} } diff --git a/Captura/Domain/CaptureSessionConfiguration.swift b/Captura/Domain/CaptureSessionConfiguration.swift index c2ebe75..673db5f 100644 --- a/Captura/Domain/CaptureSessionConfiguration.swift +++ b/Captura/Domain/CaptureSessionConfiguration.swift @@ -14,7 +14,7 @@ struct CaptureSessionConfiguration { let preventResize: Bool let autoStart: Bool let maxLength: Int - + init( frameRate: Int? = nil, outputFormats: OutputFormatSetting? = nil, @@ -36,7 +36,7 @@ struct CaptureSessionConfiguration { autoStart = false maxLength = 300 } - + init(from action: RecordAction) { self.frameRate = action.fps ?? CapturaSettings.frameRate self.outputFormats = action.outputs ?? CapturaSettings.outputFormats @@ -56,23 +56,23 @@ struct CaptureSessionConfiguration { autoStart = action.autoStart ?? false maxLength = action.maxLength ?? 300 } - + 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/Intents/CapturaShortcutsProvider.swift b/Captura/Intents/CapturaShortcutsProvider.swift index 9df2170..8846f2c 100644 --- a/Captura/Intents/CapturaShortcutsProvider.swift +++ b/Captura/Intents/CapturaShortcutsProvider.swift @@ -18,9 +18,9 @@ import AppIntents struct CapturaShortcutsProvider: AppShortcutsProvider { - static var appShortcuts: [AppShortcut] { + static var appShortcuts: [AppShortcut] { - AppShortcut(intent: GetRemoteCaptures(), phrases: ["Get \(.applicationName) remote captures"]) + AppShortcut(intent: GetRemoteCaptures(), phrases: ["Get \(.applicationName) remote captures"]) - } + } } diff --git a/Captura/Intents/GetRemoteCaptures.swift b/Captura/Intents/GetRemoteCaptures.swift index a20cd72..b066801 100644 --- a/Captura/Intents/GetRemoteCaptures.swift +++ b/Captura/Intents/GetRemoteCaptures.swift @@ -18,28 +18,28 @@ import AppIntents import CoreData struct GetRemoteCaptures: AppIntent { - static var title: LocalizedStringResource = "Get remote captures" - - static var description = - IntentDescription("Return a list of remote captures") - + static var title: LocalizedStringResource = "Get remote captures" + + static var description = + IntentDescription("Return a list of remote captures") + @Parameter(title: "Count") var count: Int? - + static var parameterSummary: some ParameterSummary { - Summary("Get \(\.$count) latest captures.") + Summary("Get \(\.$count) latest captures.") } - + func perform() async throws -> some IntentResult & ReturnsValue { let viewContext = PersistenceController.shared.container.viewContext let fetchRequest = NSFetchRequest(entityName: "CapturaRemoteFile") fetchRequest.fetchLimit = min(10, max(1, count ?? 5)) fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: false)] - + let results = await viewContext.perform { return try? viewContext.fetch(fetchRequest) } - - let finalResults = results?.compactMap { URL(string: $0.url ?? "")} ?? [] + + let finalResults = results?.compactMap { URL(string: $0.url ?? "") } ?? [] return .result(value: finalResults) } } diff --git a/Captura/Presentation/Popovers/HelpPopoverViewController.swift b/Captura/Presentation/Popovers/HelpPopoverViewController.swift index 2c48c78..5db7731 100644 --- a/Captura/Presentation/Popovers/HelpPopoverViewController.swift +++ b/Captura/Presentation/Popovers/HelpPopoverViewController.swift @@ -17,29 +17,29 @@ import Cocoa class HelpPopoverViewController: NSViewController { - - var labelString: String = "Captura" - let textField = NSTextField() - - override func loadView() { - self.view = NSView() - self.view.frame = NSRect(x: 0, y: 0, width: 250, height: 40) - - textField.stringValue = labelString - textField.font = NSFont(name: "Hiragino Mincho ProN", size: 12) - textField.isEditable = false - textField.isBezeled = false - textField.isSelectable = false - textField.backgroundColor = NSColor.clear - textField.sizeToFit() - - let x = (view.frame.width - textField.frame.width) / 2 - let y = (view.frame.height - textField.frame.height) / 2 - textField.frame.origin = NSPoint(x: x, y: y) - - self.view.addSubview(textField) - } - + + var labelString: String = "Captura" + let textField = NSTextField() + + override func loadView() { + self.view = NSView() + self.view.frame = NSRect(x: 0, y: 0, width: 250, height: 40) + + textField.stringValue = labelString + textField.font = NSFont(name: "Hiragino Mincho ProN", size: 12) + textField.isEditable = false + textField.isBezeled = false + textField.isSelectable = false + textField.backgroundColor = NSColor.clear + textField.sizeToFit() + + let x = (view.frame.width - textField.frame.width) / 2 + let y = (view.frame.height - textField.frame.height) / 2 + textField.frame.origin = NSPoint(x: x, y: y) + + self.view.addSubview(textField) + } + func updateLabel(_ newLabel: String) { labelString = newLabel textField.stringValue = labelString diff --git a/Captura/Presentation/Screens/PreferencesScreen.swift b/Captura/Presentation/Screens/PreferencesScreen.swift index 9d1b2e0..e149af1 100644 --- a/Captura/Presentation/Screens/PreferencesScreen.swift +++ b/Captura/Presentation/Screens/PreferencesScreen.swift @@ -17,19 +17,19 @@ import SwiftUI struct PreferencesScreen: View { - var body: some View { - TabView { - OutputSettings().tabItem { - Label("Output", systemImage: "video.fill") - }.padding(8.0).frame(minWidth: 300, minHeight: 130) - AdvancedSettings().tabItem { - Label("Advanced", systemImage: "gear") - }.padding(8.0).frame(minWidth: 300, minHeight: 260) - AboutSettings().tabItem { - Label("About", systemImage: "questionmark.circle.fill") - }.padding(8.0).frame(minWidth: 300, minHeight: 260) - }.padding(16.0) - } + var body: some View { + TabView { + OutputSettings().tabItem { + Label("Output", systemImage: "video.fill") + }.padding(8.0).frame(minWidth: 300, minHeight: 130) + AdvancedSettings().tabItem { + Label("Advanced", systemImage: "gear") + }.padding(8.0).frame(minWidth: 300, minHeight: 260) + AboutSettings().tabItem { + Label("About", systemImage: "questionmark.circle.fill") + }.padding(8.0).frame(minWidth: 300, minHeight: 260) + }.padding(16.0) + } } #Preview { diff --git a/Captura/Presentation/Settings/AboutSettings.swift b/Captura/Presentation/Settings/AboutSettings.swift index 089ae1d..529907c 100644 --- a/Captura/Presentation/Settings/AboutSettings.swift +++ b/Captura/Presentation/Settings/AboutSettings.swift @@ -17,18 +17,22 @@ import SwiftUI struct AboutSettings: View { - + var appVersion: String { - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" - let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" - return "Version \(version) (\(build))" + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" + let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" + return "Version \(version) (\(build))" } - - var imprint = (try? AttributedString(markdown: "Captura is open source. Help and more information available at [captura.tranquil.systems](https://captura.tranquil.systems)")) ?? "" + + var imprint = + (try? AttributedString( + markdown: + "Captura is open source. Help and more information available at [captura.tranquil.systems](https://captura.tranquil.systems)" + )) ?? "" var body: some View { Form { - VStack (alignment: .center) { + VStack(alignment: .center) { Text("Captura").bold() Text(appVersion).foregroundStyle(.secondary) Spacer() diff --git a/Captura/Presentation/Settings/AdvancedSettings.swift b/Captura/Presentation/Settings/AdvancedSettings.swift index 79b6bb1..e148f01 100644 --- a/Captura/Presentation/Settings/AdvancedSettings.swift +++ b/Captura/Presentation/Settings/AdvancedSettings.swift @@ -17,28 +17,32 @@ import SwiftUI struct AdvancedSettings: View { - + @AppStorage("backendUrl") var backendUrl: String = "" @AppStorage("backendFormat") var backendFormat: OutputFormatSetting = .gifOnly @AppStorage("keepFiles") var keepFiles = true @AppStorage("allowURLAutomation") var allowURLAutomation = false @State var showConfirmation = false - - private var anyState: String { "\(backendUrl), \(backendFormat), \(keepFiles), \(allowURLAutomation)" } - + + private var anyState: String { + "\(backendUrl), \(backendFormat), \(keepFiles), \(allowURLAutomation)" + } + var parsedBackendUrl: URL? { URL(string: backendUrl) } - + var body: some View { Form { - VStack (alignment: .center) { + VStack(alignment: .center) { Section { - VStack (alignment: .center) { + 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.") + .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: $backendFormat, label: Text("Backend Format").font(.headline)) { Text("GIF") .tag(OutputFormatSetting.gifOnly) @@ -51,15 +55,21 @@ struct AdvancedSettings: View { } .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.") + .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.") + .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) + 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) @@ -71,21 +81,29 @@ struct AdvancedSettings: View { 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 + .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() } diff --git a/Captura/Presentation/Settings/OutputSettings.swift b/Captura/Presentation/Settings/OutputSettings.swift index bfad7c7..9dc5be9 100644 --- a/Captura/Presentation/Settings/OutputSettings.swift +++ b/Captura/Presentation/Settings/OutputSettings.swift @@ -17,17 +17,17 @@ import SwiftUI struct OutputSettings: View { - + @AppStorage("outputFormats") var outputFormats: OutputFormatSetting = .all @AppStorage("frameRate") var frameRate = 10.0 - + private var anyState: String { "\(outputFormats), \(frameRate)" } - + var body: some View { Form { - VStack (alignment: .center) { + VStack(alignment: .center) { LabeledContent("GIF Framerate") { - Slider(value: $frameRate, in: 4...10, step: 1) { + Slider(value: $frameRate, in: 4...10, step: 1) { Text("\(Int(frameRate))").font(.body).frame(width: 24) } minimumValueLabel: { Text("4") @@ -44,7 +44,7 @@ struct OutputSettings: View { .tag(OutputFormatSetting.gifOnly) .padding(.horizontal, 4.0) .padding(.vertical, 2.0) - + Text("Only MP4") .tag(OutputFormatSetting.mp4Only) .padding(.horizontal, 4.0) diff --git a/Captura/Presentation/Windows/PreferencesWindow.swift b/Captura/Presentation/Windows/PreferencesWindow.swift index 95d479b..233d21b 100644 --- a/Captura/Presentation/Windows/PreferencesWindow.swift +++ b/Captura/Presentation/Windows/PreferencesWindow.swift @@ -15,12 +15,11 @@ along with this program. If not, see https://captura.tranquil.systems. */ import Cocoa -import SwiftUI - import Foundation +import SwiftUI class PreferencesWindow: NSWindow { - + init() { super.init( contentRect: NSRect(x: 0, y: 0, width: 600, height: 600), diff --git a/Captura/Presentation/Windows/RecordingWindow.swift b/Captura/Presentation/Windows/RecordingWindow.swift index a12c5fb..71e3a55 100644 --- a/Captura/Presentation/Windows/RecordingWindow.swift +++ b/Captura/Presentation/Windows/RecordingWindow.swift @@ -18,26 +18,25 @@ import Cocoa import Combine class RecordingWindow: NSWindow { - + var pixelDensity: CGFloat { self.screen?.backingScaleFactor ?? 1.0 } - + var recordingContentView: RecordingContentView { self.contentView as! RecordingContentView } - + init(_ configuration: CaptureSessionConfiguration, _ button: NSRect?) { - + let boundingBox = NSScreen.screenWithMouse?.frame ?? NSZeroRect - + super.init( contentRect: boundingBox, styleMask: [.borderless], backing: .buffered, defer: false) - self.isReleasedWhenClosed = false self.collectionBehavior = [.canJoinAllSpaces] self.isMovableByWindowBackground = false @@ -46,7 +45,8 @@ class RecordingWindow: NSWindow { self.titlebarAppearsTransparent = true self.setFrame(boundingBox, display: true) self.titleVisibility = .hidden - let recordingView = RecordingContentView(configuration, frame: boundingBox, button: button ?? NSZeroRect) + let recordingView = RecordingContentView( + configuration, frame: boundingBox, button: button ?? NSZeroRect) recordingView.frame = boundingBox self.contentView = recordingView self.backgroundColor = NSColor(white: 1.0, alpha: 0.001) @@ -56,30 +56,30 @@ class RecordingWindow: NSWindow { self.isOpaque = false self.hasShadow = false } - + // MARK: - Window Behavior Overrides - + override func resetCursorRects() { super.resetCursorRects() let cursor = NSCursor.crosshair self.contentView?.addCursorRect(self.contentView!.bounds, cursor: cursor) } - + override var canBecomeKey: Bool { return true } - + override var canBecomeMain: Bool { return true } - + override func resignMain() { super.resignMain() if (self.contentView as? RecordingContentView)?.state != .recording { self.ignoresMouseEvents = false } } - + override func becomeMain() { super.becomeMain() if (self.contentView as? RecordingContentView)?.state != .recording { @@ -89,11 +89,11 @@ class RecordingWindow: NSWindow { } enum RecordingWindowState { - case passthrough, idle, drawing, moving, resizing, recording; + case passthrough, idle, drawing, moving, resizing, recording } class RecordingContentView: NSView { - + init(_ configuration: CaptureSessionConfiguration, frame: NSRect, button: NSRect) { self.buttonSize = button.size var buttonOffset = NSPoint() @@ -109,11 +109,15 @@ class RecordingContentView: NSView { preventResize = configuration.preventResize preventMove = configuration.preventMove autoStart = configuration.autoStart - + self.bounds = frame - self.button = NSRect(x: frame.maxX - buttonOffset.x, y: frame.maxY - buttonOffset.y, width: buttonSize.width, height: buttonSize.height) - - if configuration.x != nil || configuration.y != nil || configuration.width != nil || configuration.height != nil { + self.button = NSRect( + x: frame.maxX - buttonOffset.x, y: frame.maxY - buttonOffset.y, width: buttonSize.width, + height: buttonSize.height) + + if configuration.x != nil || configuration.y != nil || configuration.width != nil + || configuration.height != nil + { box = NSRect( x: configuration.x ?? Int(frame.width / 2.0), y: configuration.y ?? Int(frame.height / 2.0), @@ -121,18 +125,18 @@ class RecordingContentView: NSView { height: configuration.height ?? 400 ) } - + if autoStart { DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { NotificationCenter.default.post(name: .startRecording, object: nil, userInfo: nil) } } } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private let buttonSize: NSSize private let buttonOffset: NSPoint public var button: NSRect? = nil @@ -144,68 +148,71 @@ class RecordingContentView: NSView { private var preventResize = false private var preventMove = false private var autoStart = false - + private var resizeBox: NSRect? { if let box { return NSRect(x: box.maxX - 5, y: box.minY - 5, width: 10, height: 10) } return nil } - + private var shouldPassthrough: Bool { state == .recording || state == .passthrough } - + // MARK: - State changing API - + public func startRecording() { state = .recording window?.ignoresMouseEvents = true } - + public func stopRecording() { - + } - + public func reset() { state = .idle window?.ignoresMouseEvents = false } - + public func startPassthrough() { state = .passthrough window?.ignoresMouseEvents = true } - + public func stopPassthrough() { state = .idle window?.ignoresMouseEvents = false } - + // MARK: - View Behavior Overrides - + override func updateTrackingAreas() { super.updateTrackingAreas() - + for trackingArea in self.trackingAreas { self.removeTrackingArea(trackingArea) } - let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeInKeyWindow, .cursorUpdate, .mouseMoved] - let trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) + let options: NSTrackingArea.Options = [ + .mouseEnteredAndExited, .activeInKeyWindow, .cursorUpdate, .mouseMoved, + ] + let trackingArea = NSTrackingArea( + rect: self.bounds, options: options, owner: self, userInfo: nil) self.addTrackingArea(trackingArea) } - + override func mouseExited(with event: NSEvent) { if state == .idle && box == nil { self.moveWindow() } } - + override func mouseMoved(with event: NSEvent) { - + self.mouseLocation = self.convert(event.locationInWindow, from: nil) - + if shouldPassthrough { NSCursor.arrow.set() } else { @@ -231,7 +238,7 @@ class RecordingContentView: NSView { self.setNeedsDisplay(self.bounds) } - + override func mouseDragged(with event: NSEvent) { self.mouseLocation = self.convert(event.locationInWindow, from: nil) if state == .drawing { @@ -242,7 +249,7 @@ class RecordingContentView: NSView { height: round(abs(mouseLocation.y - origin.y)) ) } - + if box != nil { if state == .moving { NSCursor.closedHand.set() @@ -250,7 +257,7 @@ class RecordingContentView: NSView { x: self.boxOrigin.x - self.origin.x + self.mouseLocation.x, y: self.boxOrigin.y - self.origin.y + self.mouseLocation.y) } - + if state == .resizing { box = NSRect( x: round(min(origin.x, mouseLocation.x)), @@ -262,7 +269,7 @@ class RecordingContentView: NSView { } self.setNeedsDisplay(self.bounds) } - + override func cursorUpdate(with event: NSEvent) { NSCursor.crosshair.set() } @@ -270,7 +277,7 @@ class RecordingContentView: NSView { override func hitTest(_ point: NSPoint) -> NSView? { return shouldPassthrough ? nil : self } - + override var acceptsFirstResponder: Bool { return true } @@ -278,14 +285,14 @@ class RecordingContentView: NSView { override func mouseDown(with event: NSEvent) { self.origin = self.convert(event.locationInWindow, from: nil) if let box { - + if let button { if button.contains(origin) { NotificationCenter.default.post(name: .startRecording, object: nil, userInfo: nil) return } } - + if resizeBox!.contains(origin) && !preventResize { self.origin = NSPoint(x: box.minX, y: box.maxY) state = .resizing @@ -297,11 +304,11 @@ class RecordingContentView: NSView { return } } - + if preventResize || preventMove { return } - + state = .drawing } @@ -310,16 +317,16 @@ class RecordingContentView: NSView { state = .idle } } - + override func keyDown(with event: NSEvent) { switch event.keyCode { - case 53: // Escape key - NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) - default: - super.keyDown(with: event) + case 53: // Escape key + NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) + default: + super.keyDown(with: event) } } - + override func flagsChanged(with event: NSEvent) { if state == .idle { if event.modifierFlags.contains(.shift) { @@ -329,7 +336,7 @@ class RecordingContentView: NSView { } } } - + override func draw(_ dirtyRect: NSRect) { if shouldPassthrough { NSColor.clear.setFill() @@ -337,54 +344,54 @@ class RecordingContentView: NSView { NSColor(white: 1.0, alpha: 0.001).setFill() } dirtyRect.fill() - + let dashLength: CGFloat = 5.0 let lineWidth = 0.5 -// Uncomment below to debug button placement visually -// if let button { -// let buttonPath = NSBezierPath() -// buttonPath.move(to: NSPoint(x: button.minX, y: button.minY)) -// buttonPath.line(to: NSPoint(x: button.maxX, y: button.minY)) -// buttonPath.line(to: NSPoint(x: button.maxX, y: button.maxY)) -// buttonPath.line(to: NSPoint(x: button.minX, y: button.maxY)) -// buttonPath.line(to: NSPoint(x: button.minX, y: button.minY)) -// NSColor(red: 1, green: 0, blue: 1, alpha: 1).setFill() -// buttonPath.fill() -// } + // Uncomment below to debug button placement visually + // if let button { + // let buttonPath = NSBezierPath() + // buttonPath.move(to: NSPoint(x: button.minX, y: button.minY)) + // buttonPath.line(to: NSPoint(x: button.maxX, y: button.minY)) + // buttonPath.line(to: NSPoint(x: button.maxX, y: button.maxY)) + // buttonPath.line(to: NSPoint(x: button.minX, y: button.maxY)) + // buttonPath.line(to: NSPoint(x: button.minX, y: button.minY)) + // NSColor(red: 1, green: 0, blue: 1, alpha: 1).setFill() + // buttonPath.fill() + // } if state == .idle && box == nil { let blackLine = NSBezierPath() blackLine.lineWidth = lineWidth blackLine.setLineDash([dashLength, dashLength], count: 2, phase: 0) - + // Vertical line (Black) blackLine.move(to: NSPoint(x: self.mouseLocation.x, y: NSMinY(self.bounds))) blackLine.line(to: NSPoint(x: self.mouseLocation.x, y: NSMaxY(self.bounds))) - + // Horizontal line (Black) blackLine.move(to: NSPoint(x: NSMinX(self.bounds), y: self.mouseLocation.y)) blackLine.line(to: NSPoint(x: NSMaxX(self.bounds), y: self.mouseLocation.y)) - + NSColor.black.setStroke() blackLine.stroke() - + let whiteLine = NSBezierPath() whiteLine.lineWidth = lineWidth whiteLine.setLineDash([dashLength, dashLength], count: 2, phase: dashLength) - + // Vertical line (White) whiteLine.move(to: NSPoint(x: self.mouseLocation.x, y: NSMinY(self.bounds))) whiteLine.line(to: NSPoint(x: self.mouseLocation.x, y: NSMaxY(self.bounds))) - + // Horizontal line (White) whiteLine.move(to: NSPoint(x: NSMinX(self.bounds), y: self.mouseLocation.y)) whiteLine.line(to: NSPoint(x: NSMaxX(self.bounds), y: self.mouseLocation.y)) - + NSColor.white.setStroke() whiteLine.stroke() } - + if let box { let blackBox = NSBezierPath() blackBox.lineWidth = lineWidth @@ -396,7 +403,7 @@ class RecordingContentView: NSView { blackBox.line(to: NSPoint(x: box.minX, y: box.minY)) NSColor.black.setStroke() blackBox.stroke() - + let whiteBox = NSBezierPath() whiteBox.lineWidth = lineWidth whiteBox.setLineDash([dashLength, dashLength], count: 2, phase: dashLength) @@ -407,11 +414,11 @@ class RecordingContentView: NSView { whiteBox.line(to: NSPoint(x: box.minX, y: box.minY)) NSColor.white.setStroke() whiteBox.stroke() - + if state == .recording { return } - + if let resizeBox { let clearBox = NSBezierPath() clearBox.move(to: NSPoint(x: resizeBox.minX, y: resizeBox.minY)) @@ -422,46 +429,53 @@ class RecordingContentView: NSView { NSColor(white: 0, alpha: 0.2).setFill() clearBox.fill() } - + if state == .moving { let string = "\(Int(box.minX)), \(Int(box.maxY))" as NSString - drawText(string, NSPoint( - x: box.minX, - y: box.maxY - ), true) + drawText( + string, + NSPoint( + x: box.minX, + y: box.maxY + ), true) } - + if state == .resizing { let string = "\(Int(mouseLocation.x)), \(Int(mouseLocation.y))" as NSString drawText(string, mouseLocation) } - + if box.contains(mouseLocation) && state != .resizing { - return; + return } } - + // Draw text let string = "\(Int(mouseLocation.x)), \(Int(mouseLocation.y))" as NSString drawText(string, mouseLocation) } - + // MARK: - Utilities - + private func drawText(_ text: NSString, _ location: NSPoint, _ isBottomRight: Bool = false) { - + let textAttributes = [ - NSAttributedString.Key.font: NSFont(name: "Hiragino Mincho ProN", size: 12) ?? NSFont.systemFont(ofSize: 12), + NSAttributedString.Key.font: NSFont(name: "Hiragino Mincho ProN", size: 12) + ?? NSFont.systemFont(ofSize: 12), NSAttributedString.Key.foregroundColor: NSColor.white, ] let offset = NSPoint(x: 10, y: 10) let padding = NSPoint(x: 5, y: 2) let size = text.size(withAttributes: textAttributes) - var rect = NSRect(x: location.x + offset.x, y: location.y + offset.y, width: size.width + 2 * padding.x, height: size.height + 2 * padding.y) - var textRect = NSRect(x: location.x + offset.x + padding.x, y: location.y + offset.y + padding.y, width: size.width, height: size.height) - - if (isBottomRight) { + var rect = NSRect( + x: location.x + offset.x, y: location.y + offset.y, width: size.width + 2 * padding.x, + height: size.height + 2 * padding.y) + var textRect = NSRect( + x: location.x + offset.x + padding.x, y: location.y + offset.y + padding.y, width: size.width, + height: size.height) + + if isBottomRight { rect = rect.offsetBy(dx: -size.width - 2 * offset.x, dy: 0) textRect = textRect.offsetBy(dx: -size.width - 2 * offset.x, dy: 0) } @@ -471,7 +485,7 @@ class RecordingContentView: NSView { text.draw(in: textRect, withAttributes: textAttributes) } - + private func moveWindow() { let screen = NSScreen.screenWithMouse if let currentScreen = self.window?.screen { @@ -480,10 +494,12 @@ class RecordingContentView: NSView { self.frame = CGRect(origin: NSZeroPoint, size: frame.size) self.bounds = CGRect(origin: NSZeroPoint, size: frame.size) self.updateTrackingAreas() - + if let window = self.window { self.bounds = frame - self.button = NSRect(x: frame.maxX - buttonOffset.x, y: frame.maxY - buttonOffset.y, width: buttonSize.width, height: buttonSize.height) + self.button = NSRect( + x: frame.maxX - buttonOffset.x, y: frame.maxY - buttonOffset.y, width: buttonSize.width, + height: buttonSize.height) window.setFrame(frame, display: true, animate: false) window.makeKeyAndOrderFront(nil) window.orderFrontRegardless() diff --git a/Captura/Scripting/ConfigureCommand.swift b/Captura/Scripting/ConfigureCommand.swift index bb58c03..28cfe36 100644 --- a/Captura/Scripting/ConfigureCommand.swift +++ b/Captura/Scripting/ConfigureCommand.swift @@ -18,32 +18,34 @@ import Foundation @objc(ConfigureCommand) class ConfigureCommand: NSScriptCommand { - override func performDefaultImplementation() -> Any? { - - let args = self.directParameter as? [String: Any] ?? [:] - - // Here you can extract the parameters from the args dictionary and configure your settings - let fps = args["fps"] as? Int - let outputs = OutputFormatSetting(args["outputs"] as? String ?? "") - let backend = URL(string: args["backend"] as? String ?? "") - let backendOutput = OutputFormatSetting(args["backend_output"] as? String ?? "") - let keepLocalFiles = args["keep_local_files"] as? Bool - - let config = ConfigureAction( - action: "configure", - fps: fps, - outputs: outputs, - backend: backend, - backendOutput: backendOutput, - keepLocalFiles: keepLocalFiles - ) - - NotificationCenter.default.post(name: .setConfiguration, object: nil, userInfo: [ + override func performDefaultImplementation() -> Any? { + + let args = self.directParameter as? [String: Any] ?? [:] + + // Here you can extract the parameters from the args dictionary and configure your settings + let fps = args["fps"] as? Int + let outputs = OutputFormatSetting(args["outputs"] as? String ?? "") + let backend = URL(string: args["backend"] as? String ?? "") + let backendOutput = OutputFormatSetting(args["backend_output"] as? String ?? "") + let keepLocalFiles = args["keep_local_files"] as? Bool + + let config = ConfigureAction( + action: "configure", + fps: fps, + outputs: outputs, + backend: backend, + backendOutput: backendOutput, + keepLocalFiles: keepLocalFiles + ) + + NotificationCenter.default.post( + name: .setConfiguration, object: nil, + userInfo: [ "config": config ]) - NotificationCenter.default.post(name: .reloadConfiguration, object: nil, userInfo: nil) + NotificationCenter.default.post(name: .reloadConfiguration, object: nil, userInfo: nil) - // Return a result if needed - return nil - } + // Return a result if needed + return nil + } } diff --git a/Captura/Scripting/RecordCommand.swift b/Captura/Scripting/RecordCommand.swift index 43cdd32..da2d28c 100644 --- a/Captura/Scripting/RecordCommand.swift +++ b/Captura/Scripting/RecordCommand.swift @@ -18,56 +18,58 @@ import Foundation @objc(RecordCommand) class RecordCommand: NSScriptCommand { - override func performDefaultImplementation() -> Any? { - - let args = self.directParameter as? [String: Any] ?? [:] - - // Here you can extract the parameters from the args dictionary and configure your settings - let x = args["x"] as? Int - let y = args["y"] as? Int - let width = args["width"] as? Int - let height = args["height"] as? Int - let preventResize = args["prevent_resize"] as? Bool - let preventMove = args["prevent_move"] as? Bool - let fps = args["fps"] as? Int - let outputs = OutputFormatSetting(args["outputs"] as? String ?? "") - let backend = URL(string: args["backend"] as? String ?? "") - let backendOutput = OutputFormatSetting(args["backend_output"] as? String ?? "") - let keepLocalFiles = args["keep_local_files"] as? Bool - let autoStart = args["auto_start"] as? Bool - let maxLength = args["max_length"] as? Int - - var skipBackend = false - if let backendString = args["backend"] as? String { - if backendString == "" { - skipBackend = true - } + override func performDefaultImplementation() -> Any? { + + let args = self.directParameter as? [String: Any] ?? [:] + + // Here you can extract the parameters from the args dictionary and configure your settings + let x = args["x"] as? Int + let y = args["y"] as? Int + let width = args["width"] as? Int + let height = args["height"] as? Int + let preventResize = args["prevent_resize"] as? Bool + let preventMove = args["prevent_move"] as? Bool + let fps = args["fps"] as? Int + let outputs = OutputFormatSetting(args["outputs"] as? String ?? "") + let backend = URL(string: args["backend"] as? String ?? "") + let backendOutput = OutputFormatSetting(args["backend_output"] as? String ?? "") + let keepLocalFiles = args["keep_local_files"] as? Bool + let autoStart = args["auto_start"] as? Bool + let maxLength = args["max_length"] as? Int + + var skipBackend = false + if let backendString = args["backend"] as? String { + if backendString == "" { + skipBackend = true } - - let config = RecordAction( - action: "record", - x: x, - y: y, - width: width, - height: height, - preventResize: preventResize, - preventMove: preventMove, - fps: fps, - outputs: outputs, - backend: backend, - backendOutput: backendOutput, - keepLocalFiles: keepLocalFiles, - autoStart: autoStart, - skipBackend: skipBackend, - maxLength: maxLength - ) - - NotificationCenter.default.post(name: .setCaptureSessionConfiguration, object: nil, userInfo: [ + } + + let config = RecordAction( + action: "record", + x: x, + y: y, + width: width, + height: height, + preventResize: preventResize, + preventMove: preventMove, + fps: fps, + outputs: outputs, + backend: backend, + backendOutput: backendOutput, + keepLocalFiles: keepLocalFiles, + autoStart: autoStart, + skipBackend: skipBackend, + maxLength: maxLength + ) + + NotificationCenter.default.post( + name: .setCaptureSessionConfiguration, object: nil, + userInfo: [ "config": config ]) - NotificationCenter.default.post(name: .startAreaSelection, object: nil, userInfo: nil) + NotificationCenter.default.post(name: .startAreaSelection, object: nil, userInfo: nil) - // Return a result if needed - return nil - } + // Return a result if needed + return nil + } } diff --git a/Captura/Scripting/ScriptedPreferences.swift b/Captura/Scripting/ScriptedPreferences.swift index 547c3cf..72aa45e 100644 --- a/Captura/Scripting/ScriptedPreferences.swift +++ b/Captura/Scripting/ScriptedPreferences.swift @@ -35,7 +35,7 @@ class ScriptedPreferences: NSObject { CapturaSettings.outputFormats = OutputFormatSetting(newValue) ?? .gifOnly } } - + @objc dynamic var backend: String { get { CapturaSettings.backend?.absoluteString ?? "" @@ -44,7 +44,7 @@ class ScriptedPreferences: NSObject { CapturaSettings.backend = URL(string: newValue) } } - + @objc dynamic var backend_output: String { get { CapturaSettings.backendFormat.toString() @@ -53,7 +53,7 @@ class ScriptedPreferences: NSObject { CapturaSettings.backendFormat = OutputFormatSetting(newValue) ?? .gifOnly } } - + @objc dynamic var keep_local_files: Bool { get { CapturaSettings.shouldKeepLocalFiles diff --git a/CapturaTests/CapturaTests.swift b/CapturaTests/CapturaTests.swift index 363f42b..26061af 100644 --- a/CapturaTests/CapturaTests.swift +++ b/CapturaTests/CapturaTests.swift @@ -16,31 +16,32 @@ */ import XCTest + @testable import Captura final class CapturaTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. } + } } diff --git a/CapturaUITests/CapturaUITests.swift b/CapturaUITests/CapturaUITests.swift index 429c6b2..f29b0e4 100644 --- a/CapturaUITests/CapturaUITests.swift +++ b/CapturaUITests/CapturaUITests.swift @@ -19,33 +19,33 @@ import XCTest final class CapturaUITests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() - // Use XCTAssert and related functions to verify your tests produce the correct results. - } + // Use XCTAssert and related functions to verify your tests produce the correct results. + } - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } } + } } diff --git a/CapturaUITests/CapturaUITestsLaunchTests.swift b/CapturaUITests/CapturaUITestsLaunchTests.swift index 98bab31..a8d1b69 100644 --- a/CapturaUITests/CapturaUITestsLaunchTests.swift +++ b/CapturaUITests/CapturaUITestsLaunchTests.swift @@ -19,24 +19,24 @@ import XCTest final class CapturaUITestsLaunchTests: XCTestCase { - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } } diff --git a/Makefile b/Makefile index dd8bf56..f90daf6 100644 --- a/Makefile +++ b/Makefile @@ -19,4 +19,10 @@ archive: prepare prepare: mkdir -p $(build_directory) -.PHONY: package prepare archive generate_appcast package distribute +format: + swift format -i -r . + +lint: + swift format lint -r . + +.PHONY: package prepare archive generate_appcast package distribute format lint