From: Ruben Beltran del Rio Date: Mon, 31 Jul 2023 08:52:11 +0000 (+0200) Subject: Complete initial release X-Git-Tag: 1.0.0~9 X-Git-Url: https://git.r.bdr.sh/rbdr/captura/commitdiff_plain/533cd932281300fb444c07e80f81fc683a410b60?ds=sidebyside Complete initial release --- diff --git a/Captura.xcodeproj/project.pbxproj b/Captura.xcodeproj/project.pbxproj index 3238749..388af03 100644 --- a/Captura.xcodeproj/project.pbxproj +++ b/Captura.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ B5278B312A73AEAE009F6462 /* CVImageBuffer+cgImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B302A73AEAE009F6462 /* CVImageBuffer+cgImage.swift */; }; B5278B362A73B3AA009F6462 /* CapturaCaptureSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B352A73B3AA009F6462 /* CapturaCaptureSession.swift */; }; B5278B382A73D1EE009F6462 /* CapturaSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B372A73D1EE009F6462 /* CapturaSettings.swift */; }; + B5278B3E2A74420F009F6462 /* Captura.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B5278B3C2A74420F009F6462 /* Captura.xcdatamodeld */; }; + B5278B402A744297009F6462 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B3F2A744297009F6462 /* Persistence.swift */; }; + B5278B422A779CDB009F6462 /* BackendResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B412A779CDB009F6462 /* BackendResponse.swift */; }; 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 */; }; @@ -25,7 +28,7 @@ B5F915542A6EF80D007ECE8E /* PreferencesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F915532A6EF80D007ECE8E /* PreferencesScreen.swift */; }; B5F915562A6EF80E007ECE8E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5F915552A6EF80E007ECE8E /* Assets.xcassets */; }; B5F915592A6EF80E007ECE8E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5F915582A6EF80E007ECE8E /* Preview Assets.xcassets */; }; - B5F9155B2A6EF80E007ECE8E /* CapturaRemoteFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9155A2A6EF80E007ECE8E /* CapturaRemoteFile.swift */; }; + B5F9155B2A6EF80E007ECE8E /* CapturaRemoteFile+name.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9155A2A6EF80E007ECE8E /* CapturaRemoteFile+name.swift */; }; B5F915662A6EF80E007ECE8E /* CapturaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F915652A6EF80E007ECE8E /* CapturaTests.swift */; }; B5F915702A6EF80E007ECE8E /* CapturaUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F9156F2A6EF80E007ECE8E /* CapturaUITests.swift */; }; B5F915722A6EF80E007ECE8E /* CapturaUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F915712A6EF80E007ECE8E /* CapturaUITestsLaunchTests.swift */; }; @@ -60,6 +63,9 @@ B5278B302A73AEAE009F6462 /* CVImageBuffer+cgImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CVImageBuffer+cgImage.swift"; sourceTree = ""; }; B5278B352A73B3AA009F6462 /* CapturaCaptureSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaCaptureSession.swift; sourceTree = ""; }; B5278B372A73D1EE009F6462 /* CapturaSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaSettings.swift; sourceTree = ""; }; + B5278B3D2A74420F009F6462 /* CapturaRemoteFile.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CapturaRemoteFile.xcdatamodel; sourceTree = ""; }; + B5278B3F2A744297009F6462 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + B5278B412A779CDB009F6462 /* BackendResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendResponse.swift; sourceTree = ""; }; B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+AppEvents.swift"; sourceTree = ""; }; B55DDFCD2A6F069D001A5E76 /* RecordingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingWindow.swift; sourceTree = ""; }; B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureState.swift; sourceTree = ""; }; @@ -68,7 +74,7 @@ B5F915532A6EF80D007ECE8E /* PreferencesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesScreen.swift; sourceTree = ""; }; B5F915552A6EF80E007ECE8E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B5F915582A6EF80E007ECE8E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - B5F9155A2A6EF80E007ECE8E /* CapturaRemoteFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaRemoteFile.swift; sourceTree = ""; }; + B5F9155A2A6EF80E007ECE8E /* CapturaRemoteFile+name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CapturaRemoteFile+name.swift"; sourceTree = ""; }; B5F9155C2A6EF80E007ECE8E /* Captura.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Captura.entitlements; sourceTree = ""; }; B5F915612A6EF80E007ECE8E /* CapturaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CapturaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B5F915652A6EF80E007ECE8E /* CapturaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaTests.swift; sourceTree = ""; }; @@ -144,8 +150,11 @@ B5278B202A71BFC3009F6462 /* SettingsStructs.swift */, B5278B292A73992D009F6462 /* GifRenderer.swift */, B5278B2B2A739B3A009F6462 /* CapturaFile.swift */, - B5F9155A2A6EF80E007ECE8E /* CapturaRemoteFile.swift */, + B5F9155A2A6EF80E007ECE8E /* CapturaRemoteFile+name.swift */, B5278B372A73D1EE009F6462 /* CapturaSettings.swift */, + B5278B3C2A74420F009F6462 /* Captura.xcdatamodeld */, + B5278B3F2A744297009F6462 /* Persistence.swift */, + B5278B412A779CDB009F6462 /* BackendResponse.swift */, ); path = Data; sourceTree = ""; @@ -254,6 +263,8 @@ dependencies = ( ); name = Captura; + packageProductDependencies = ( + ); productName = Captura; productReference = B5F9154E2A6EF80D007ECE8E /* Captura.app */; productType = "com.apple.product-type.application"; @@ -326,6 +337,8 @@ Base, ); mainGroup = B5F915452A6EF80D007ECE8E; + packageReferences = ( + ); productRefGroup = B5F9154F2A6EF80D007ECE8E /* Products */; projectDirPath = ""; projectRoot = ""; @@ -371,19 +384,22 @@ B5F915542A6EF80D007ECE8E /* PreferencesScreen.swift in Sources */, B5278B382A73D1EE009F6462 /* CapturaSettings.swift in Sources */, B5278B282A739871009F6462 /* CGImage+resize.swift in Sources */, + B5278B422A779CDB009F6462 /* BackendResponse.swift in Sources */, B5278B2C2A739B3A009F6462 /* CapturaFile.swift in Sources */, B5278B1F2A71BD9B009F6462 /* OutputSettings.swift in Sources */, B5278B2A2A73992D009F6462 /* GifRenderer.swift in Sources */, B5278B212A71BFC3009F6462 /* SettingsStructs.swift in Sources */, B55DDFCE2A6F069D001A5E76 /* RecordingWindow.swift in Sources */, + B5278B402A744297009F6462 /* Persistence.swift in Sources */, B55DDFCC2A6F0253001A5E76 /* Notification+AppEvents.swift in Sources */, - B5F9155B2A6EF80E007ECE8E /* CapturaRemoteFile.swift in Sources */, + B5F9155B2A6EF80E007ECE8E /* CapturaRemoteFile+name.swift in Sources */, B5278B232A71C140009F6462 /* PreferencesWindow.swift in Sources */, B5278B172A71528F009F6462 /* HelpPopoverViewController.swift in Sources */, B56C70CD2A6EFDF4009B97EB /* CaptureState.swift in Sources */, B5278B312A73AEAE009F6462 /* CVImageBuffer+cgImage.swift in Sources */, B5278B252A71CA80009F6462 /* AdvancedSettings.swift in Sources */, B5278B362A73B3AA009F6462 /* CapturaCaptureSession.swift in Sources */, + B5278B3E2A74420F009F6462 /* Captura.xcdatamodeld in Sources */, B5F915522A6EF80D007ECE8E /* CapturaApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -474,7 +490,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -531,7 +547,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -559,6 +575,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Captura; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -587,6 +604,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Captura; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -707,6 +725,19 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + B5278B3C2A74420F009F6462 /* Captura.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + B5278B3D2A74420F009F6462 /* CapturaRemoteFile.xcdatamodel */, + ); + currentVersion = B5278B3D2A74420F009F6462 /* CapturaRemoteFile.xcdatamodel */; + path = Captura.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = B5F915462A6EF80D007ECE8E /* Project object */; } diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/Contents.json b/Captura/Assets.xcassets/AppIcon.appiconset/Contents.json index 3f00db4..64dc11e 100644 --- a/Captura/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Captura/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,51 +1,61 @@ { "images" : [ { + "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { + "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { + "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { + "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { + "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { + "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { + "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { + "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { + "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { + "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..ad5cdfa Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000..fa683a1 Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000..b410569 Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000..d047c23 Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000..6bd883f Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000..28b26c3 Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000..d047c23 Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000..033fe06 Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000..28b26c3 Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/Captura/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Captura/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000..a5b5d80 Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/Captura/Captura.entitlements b/Captura/Captura.entitlements index b288fda..2391ad2 100644 --- a/Captura/Captura.entitlements +++ b/Captura/Captura.entitlements @@ -5,14 +5,20 @@ com.apple.developer.aps-environment development com.apple.developer.icloud-container-identifiers - + + iCloud.pizza.unlimited.captura + com.apple.developer.icloud-services - + + CloudKit + com.apple.security.app-sandbox com.apple.security.assets.pictures.read-write com.apple.security.files.user-selected.read-only + com.apple.security.network.client + diff --git a/Captura/CapturaApp.swift b/Captura/CapturaApp.swift index 4f1ceff..e1c4e7e 100644 --- a/Captura/CapturaApp.swift +++ b/Captura/CapturaApp.swift @@ -16,7 +16,7 @@ struct CapturaApp: App { .frame(width: 650, height: 450) } .handlesExternalEvents(matching: Set(arrayLiteral: "PreferencesScreen")) - .modelContainer(for: CapturaRemoteFile.self) + //.modelContainer(for: CapturaRemoteFile.self) } } @@ -37,21 +37,23 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { var fps = CapturaSettings.frameRate var pixelDensity: CGFloat = 1.0 var stopTimer: DispatchWorkItem? + var remoteFiles: [CapturaRemoteFile] = [] func applicationDidFinishLaunching(_ notification: Notification) { - setupMenu() + setupStatusBar() NotificationCenter.default.addObserver( self, selector: #selector(self.didReceiveNotification(_:)), name: nil, object: nil) closeWindow() + fetchRemoteItems() } // MARK: - Setup Functions - private func setupMenu() { + private func setupStatusBar() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem.button { @@ -67,17 +69,27 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { popover?.contentViewController = HelpPopoverViewController() popover?.behavior = .transient + setupMenu() + } + + private func setupMenu() { - let recordItem = NSMenuItem(title: "Record", action: #selector(CapturaAppDelegate.onClickStartRecording), keyEquivalent: "6") - recordItem.keyEquivalentModifierMask = [.command, .shift] - statusItem.menu?.addItem(recordItem) - statusItem.menu?.addItem(NSMenuItem.separator()) - - let preferencesItem = NSMenuItem(title: "Preferences", action: #selector(CapturaAppDelegate.onOpenPreferences), keyEquivalent: "") - statusItem.menu?.addItem(preferencesItem) + statusItem.menu?.removeAllItems() - let quitItem = NSMenuItem(title: "Quit", action: #selector(CapturaAppDelegate.onQuit), keyEquivalent: "") - statusItem.menu?.addItem(quitItem) + 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: "") + 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.separator()) + 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() { @@ -107,6 +119,23 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { 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") { + NSWorkspace.shared.open(directory) + } + } + + @objc private func onClickRemoteFile(_ sender: NSMenuItem) { + if let remoteFile = sender.representedObject as? CapturaRemoteFile { + if let urlString = remoteFile.url { + if let url = URL(string: urlString) { + NSWorkspace.shared.open(url) + } + } } } @@ -131,11 +160,22 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { case .reset: reset() case .failedToStart: - failedToStart() + DispatchQueue.main.async { + self.failed(true) + } + case .failedtoUpload: + DispatchQueue.main.async { + self.failed() + } case .receivedFrame: if let frame = notification.userInfo?["frame"] { receivedFrame(frame as! CVImageBuffer) } + case .NSManagedObjectContextObjectsDidChange: + DispatchQueue.main.async { + self.fetchRemoteItems() + self.setupMenu() + } default: return } @@ -144,13 +184,15 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { func startAreaSelection() { helpShown = false - NSApp.activate(ignoringOtherApps: true) if captureState != .selectingArea { captureState = .selectingArea if let button = statusItem.button { let rectInWindow = button.convert(button.bounds, to: nil) let rectInScreen = button.window?.convertToScreen(rectInWindow) + NSApp.activate(ignoringOtherApps: true) recordingWindow = RecordingWindow(rectInScreen) + recordingWindow?.makeKeyAndOrderFront(nil) + recordingWindow?.orderFrontRegardless() boxListener = recordingWindow?.recordingContentView.$box .debounce(for: .seconds(0.3), scheduler: RunLoop.main) .sink { newValue in @@ -203,22 +245,23 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { updateImage() stop() - if !CapturaSettings.shouldSaveMp4 { - NotificationCenter.default.post(name: .finalizeRecording, object: nil, userInfo: nil) - return - } - Task.detached { - if let outputFile = self.outputFile { - await GifRenderer.render(self.images, at: self.fps, to: outputFile.gifURL) + if CapturaSettings.shouldSaveGif { + if let outputFile = self.outputFile { + await GifRenderer.render(self.images, at: self.fps, to: outputFile.gifURL) + } + } + let wasSuccessful = await self.uploadOrCopy() + if wasSuccessful { NotificationCenter.default.post(name: .finalizeRecording, object: nil, userInfo: nil) + } else { + NotificationCenter.default.post(name: .failedtoUpload, object: nil, userInfo: nil) } } } func finalizeRecording() { captureState = .uploaded - copyToClipboard() updateImage() DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) @@ -244,16 +287,30 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } } - func failedToStart() { + func failed(_ requestPermission: Bool = false) { captureState = .error updateImage() - requestPermissionToRecord() + if requestPermission { + requestPermissionToRecord() + } stop() DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) } } + // 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 @@ -303,7 +360,20 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { recordingWindow = nil } - private func copyToClipboard() { + private func uploadOrCopy() async -> Bool { + if CapturaSettings.shouldUseBackend { + let result = await uploadToBackend() + if result && !CapturaSettings.shouldKeepLocalFiles { + deleteLocalFiles() + } + return result + } else { + copyLocalToClipboard() + return true + } + } + + 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 { if let data = try? Data(contentsOf: url) { @@ -313,4 +383,56 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } } } + + private func uploadToBackend() async -> Bool { + let contentType = CapturaSettings.shouldUploadGif ? "image/gif" : "video/mp4" + if let url = CapturaSettings.shouldUploadGif ? outputFile?.gifURL : outputFile?.mp4URL { + if let data = try? Data(contentsOf: url) { + if let remoteUrl = CapturaSettings.backend { + var request = URLRequest(url: remoteUrl) + request.httpMethod = "POST" + 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) + createRemoteFile(answer.url) + return true + } + } + } catch {} + } + } + } + return false + } + + private func createRemoteFile(_ url: URL) { + let viewContext = PersistenceController.shared.container.viewContext + let remoteFile = CapturaRemoteFile(context: viewContext) + remoteFile.url = url.absoluteString + remoteFile.timestamp = Date() + try? viewContext.save() + let pasteboard = NSPasteboard.general + pasteboard.declareTypes([.URL], owner: nil) + pasteboard.setString(url.absoluteString, forType: .string) + } + + private func deleteLocalFiles() { + if CapturaSettings.shouldSaveGif { + if let url = outputFile?.gifURL { + try? FileManager.default.removeItem(at: url) + } + } + if CapturaSettings.shouldSaveMp4 { + if let url = outputFile?.mp4URL { + try? FileManager.default.removeItem(at: url) + } + } + } } diff --git a/Captura/Core Extensions/Notification+AppEvents.swift b/Captura/Core Extensions/Notification+AppEvents.swift index 5d00937..0a0f820 100644 --- a/Captura/Core Extensions/Notification+AppEvents.swift +++ b/Captura/Core Extensions/Notification+AppEvents.swift @@ -8,4 +8,5 @@ extension Notification.Name { static let reset = Notification.Name("reset") static let failedToStart = Notification.Name("failedToStart") static let receivedFrame = Notification.Name("receivedFrame") + static let failedtoUpload = Notification.Name("failedToUpload") } diff --git a/Captura/Data/BackendResponse.swift b/Captura/Data/BackendResponse.swift new file mode 100644 index 0000000..25c1988 --- /dev/null +++ b/Captura/Data/BackendResponse.swift @@ -0,0 +1,4 @@ +import Foundation +struct BackendResponse: Decodable { + let url: URL +} diff --git a/Captura/Data/Captura.xcdatamodeld/CapturaRemoteFile.xcdatamodel/contents b/Captura/Data/Captura.xcdatamodeld/CapturaRemoteFile.xcdatamodel/contents new file mode 100644 index 0000000..3311233 --- /dev/null +++ b/Captura/Data/Captura.xcdatamodeld/CapturaRemoteFile.xcdatamodel/contents @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Captura/Data/CapturaRemoteFile+name.swift b/Captura/Data/CapturaRemoteFile+name.swift new file mode 100644 index 0000000..feebdba --- /dev/null +++ b/Captura/Data/CapturaRemoteFile+name.swift @@ -0,0 +1,13 @@ +import Foundation +extension CapturaRemoteFile { + var name: String { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .medium + dateFormatter.locale = Locale.current + if let timestamp { + return dateFormatter.string(from: timestamp).replacingOccurrences(of: ":", with: ".") + } + return "Unknown" + } +} diff --git a/Captura/Data/CapturaRemoteFile.swift b/Captura/Data/CapturaRemoteFile.swift deleted file mode 100644 index 24d821e..0000000 --- a/Captura/Data/CapturaRemoteFile.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import SwiftData - -@Model -final class CapturaRemoteFile { - var timestamp: Date - var url: URL - - init(url: URL) { - self.timestamp = Date() - self.url = url - } -} diff --git a/Captura/Data/CapturaSettings.swift b/Captura/Data/CapturaSettings.swift index d0d04f2..01ef78c 100644 --- a/Captura/Data/CapturaSettings.swift +++ b/Captura/Data/CapturaSettings.swift @@ -10,20 +10,37 @@ struct CapturaSettings { } static var shouldSaveMp4: Bool { - outputFormats.shouldSaveMp4() + outputFormats.shouldSaveMp4() || (shouldUseBackend && shouldUploadMp4) } static var shouldSaveGif: Bool { - outputFormats.shouldSaveGif() + outputFormats.shouldSaveGif() || (shouldUseBackend && shouldUploadGif) } + static var shouldUploadGif: Bool { + backendFormat.shouldSaveGif() + } - static var shouldSendNotifications: Bool { - get { - UserDefaults.standard.bool(forKey: "shouldSendNotifications") - } - set { - UserDefaults.standard.setValue(newValue, forKey: "shouldSendNotifications") + static var shouldUploadMp4: Bool { + backendFormat.shouldSaveMp4() + } + + static var shouldUseBackend: Bool { + backend != nil + } + + static var backend: URL? { + if let url = UserDefaults.standard.string(forKey: "backendUrl") { + return URL(string: url) } + return nil + } + + static var backendFormat: OutputFormatSetting { + OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "backendFormat")) ?? .all + } + + static var shouldKeepLocalFiles: Bool { + UserDefaults.standard.bool(forKey: "keepFiles") } } diff --git a/Captura/Data/Persistence.swift b/Captura/Data/Persistence.swift new file mode 100644 index 0000000..4490e38 --- /dev/null +++ b/Captura/Data/Persistence.swift @@ -0,0 +1,46 @@ +import CoreData + +struct PersistenceController { + static let shared = PersistenceController() + + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + return result + }() + + let container: NSPersistentCloudKitContainer + + init(inMemory: Bool = false) { + container = NSPersistentCloudKitContainer(name: "Captura") + + #if DEBUG + do { + // Use the container to initialize the development schema. + try container.initializeCloudKitSchema(options: []) + } 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. + + /* + 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. + * The device is out of space. + * 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 + } +} diff --git a/Captura/Presentation/Settings/AdvancedSettings.swift b/Captura/Presentation/Settings/AdvancedSettings.swift index e5b77ce..872d0c0 100644 --- a/Captura/Presentation/Settings/AdvancedSettings.swift +++ b/Captura/Presentation/Settings/AdvancedSettings.swift @@ -16,6 +16,7 @@ struct AdvancedSettings: View { 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) @@ -28,16 +29,16 @@ 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.") 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) - Button { - print("Not yet!") - } label: { + Link(destination: URL(string: "https://captura.tranquil.systems")!) { Image(systemName: "info.circle") }.buttonStyle(.borderless) } diff --git a/Captura/Presentation/Windows/PreferencesWindow.swift b/Captura/Presentation/Windows/PreferencesWindow.swift index 40eac0d..6022be5 100644 --- a/Captura/Presentation/Windows/PreferencesWindow.swift +++ b/Captura/Presentation/Windows/PreferencesWindow.swift @@ -11,10 +11,11 @@ class PreferencesWindow: NSWindow { styleMask: [.titled, .closable, .resizable, .fullSizeContentView], backing: .buffered, defer: false) - super.center() + self.center() self.isReleasedWhenClosed = false - super.setFrameAutosaveName("Preferences Window") - super.contentView = NSHostingView(rootView: PreferencesScreen()) - super.makeKeyAndOrderFront(nil) + self.setFrameAutosaveName("Preferences Window") + self.contentView = NSHostingView(rootView: PreferencesScreen()) + self.makeKeyAndOrderFront(nil) + self.orderFrontRegardless() } } diff --git a/Captura/Presentation/Windows/RecordingWindow.swift b/Captura/Presentation/Windows/RecordingWindow.swift index b9f212c..3395c54 100644 --- a/Captura/Presentation/Windows/RecordingWindow.swift +++ b/Captura/Presentation/Windows/RecordingWindow.swift @@ -27,9 +27,9 @@ class RecordingWindow: NSWindow { self.isReleasedWhenClosed = false self.collectionBehavior = [.canJoinAllSpaces] - self.center() self.isMovableByWindowBackground = false self.isMovable = false + self.canHide = false self.titlebarAppearsTransparent = true self.setFrame(boundingBox, display: true) self.titleVisibility = .hidden @@ -41,7 +41,6 @@ class RecordingWindow: NSWindow { self.level = .screenSaver self.isOpaque = false self.hasShadow = false - self.makeKeyAndOrderFront(nil) } // MARK: - Window Behavior Overrides