]> git.r.bdr.sh - rbdr/captura/commitdiff
Complete initial release
authorRuben Beltran del Rio <redacted>
Mon, 31 Jul 2023 08:52:11 +0000 (10:52 +0200)
committerRuben Beltran del Rio <redacted>
Mon, 31 Jul 2023 08:52:11 +0000 (10:52 +0200)
24 files changed:
Captura.xcodeproj/project.pbxproj
Captura/Assets.xcassets/AppIcon.appiconset/Contents.json
Captura/Assets.xcassets/AppIcon.appiconset/icon_128x128.png [new file with mode: 0644]
Captura/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png [new file with mode: 0644]
Captura/Assets.xcassets/AppIcon.appiconset/icon_16x16.png [new file with mode: 0644]
Captura/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png [new file with mode: 0644]
Captura/Assets.xcassets/AppIcon.appiconset/icon_256x256.png [new file with mode: 0644]
Captura/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png [new file with mode: 0644]
Captura/Assets.xcassets/AppIcon.appiconset/icon_32x32.png [new file with mode: 0644]
Captura/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png [new file with mode: 0644]
Captura/Assets.xcassets/AppIcon.appiconset/icon_512x512.png [new file with mode: 0644]
Captura/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png [new file with mode: 0644]
Captura/Captura.entitlements
Captura/CapturaApp.swift
Captura/Core Extensions/Notification+AppEvents.swift
Captura/Data/BackendResponse.swift [new file with mode: 0644]
Captura/Data/Captura.xcdatamodeld/CapturaRemoteFile.xcdatamodel/contents [new file with mode: 0644]
Captura/Data/CapturaRemoteFile+name.swift [new file with mode: 0644]
Captura/Data/CapturaRemoteFile.swift [deleted file]
Captura/Data/CapturaSettings.swift
Captura/Data/Persistence.swift [new file with mode: 0644]
Captura/Presentation/Settings/AdvancedSettings.swift
Captura/Presentation/Windows/PreferencesWindow.swift
Captura/Presentation/Windows/RecordingWindow.swift

index 3238749d7765e34628411ccdf095fd9bf86c377a..388af03fa5d897a18d2860586bb43d3f07369602 100644 (file)
@@ -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 = "<group>"; };
                B5278B352A73B3AA009F6462 /* CapturaCaptureSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaCaptureSession.swift; sourceTree = "<group>"; };
                B5278B372A73D1EE009F6462 /* CapturaSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaSettings.swift; sourceTree = "<group>"; };
+               B5278B3D2A74420F009F6462 /* CapturaRemoteFile.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CapturaRemoteFile.xcdatamodel; sourceTree = "<group>"; };
+               B5278B3F2A744297009F6462 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
+               B5278B412A779CDB009F6462 /* BackendResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendResponse.swift; sourceTree = "<group>"; };
                B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+AppEvents.swift"; sourceTree = "<group>"; };
                B55DDFCD2A6F069D001A5E76 /* RecordingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingWindow.swift; sourceTree = "<group>"; };
                B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureState.swift; sourceTree = "<group>"; };
@@ -68,7 +74,7 @@
                B5F915532A6EF80D007ECE8E /* PreferencesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesScreen.swift; sourceTree = "<group>"; };
                B5F915552A6EF80E007ECE8E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
                B5F915582A6EF80E007ECE8E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
-               B5F9155A2A6EF80E007ECE8E /* CapturaRemoteFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaRemoteFile.swift; sourceTree = "<group>"; };
+               B5F9155A2A6EF80E007ECE8E /* CapturaRemoteFile+name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CapturaRemoteFile+name.swift"; sourceTree = "<group>"; };
                B5F9155C2A6EF80E007ECE8E /* Captura.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Captura.entitlements; sourceTree = "<group>"; };
                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 = "<group>"; };
                                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 = "<group>";
                        dependencies = (
                        );
                        name = Captura;
+                       packageProductDependencies = (
+                       );
                        productName = Captura;
                        productReference = B5F9154E2A6EF80D007ECE8E /* Captura.app */;
                        productType = "com.apple.product-type.application";
                                Base,
                        );
                        mainGroup = B5F915452A6EF80D007ECE8E;
+                       packageReferences = (
+                       );
                        productRefGroup = B5F9154F2A6EF80D007ECE8E /* Products */;
                        projectDirPath = "";
                        projectRoot = "";
                                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;
                                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;
                                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;
                                        "$(inherited)",
                                        "@executable_path/../Frameworks",
                                );
+                               MACOSX_DEPLOYMENT_TARGET = 13.0;
                                MARKETING_VERSION = 1.0;
                                PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Captura;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                        "$(inherited)",
                                        "@executable_path/../Frameworks",
                                );
+                               MACOSX_DEPLOYMENT_TARGET = 13.0;
                                MARKETING_VERSION = 1.0;
                                PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Captura;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                        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 = "<group>";
+                       versionGroupType = wrapper.xcdatamodel;
+               };
+/* End XCVersionGroup section */
        };
        rootObject = B5F915462A6EF80D007ECE8E /* Project object */;
 }
index 3f00db43ec3c8b462759505d635dc5545d4e8e50..64dc11ee7438ff9d41f0fb625e861d6bb881024c 100644 (file)
@@ -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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..a5b5d80
Binary files /dev/null and b/Captura/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ
index b288fda0a667f7cffc7e81497e29fe3c12400bfd..2391ad2592b5cd42188403af857e3b0a1b792873 100644 (file)
@@ -5,14 +5,20 @@
        <key>com.apple.developer.aps-environment</key>
        <string>development</string>
        <key>com.apple.developer.icloud-container-identifiers</key>
-       <array/>
+       <array>
+               <string>iCloud.pizza.unlimited.captura</string>
+       </array>
        <key>com.apple.developer.icloud-services</key>
-       <array/>
+       <array>
+               <string>CloudKit</string>
+       </array>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.assets.pictures.read-write</key>
        <true/>
        <key>com.apple.security.files.user-selected.read-only</key>
        <true/>
+       <key>com.apple.security.network.client</key>
+       <true/>
 </dict>
 </plist>
index 4f1ceffe2a5f43a9ebe8accf8c6145654484df0e..e1c4e7e87b78c3017690d473aa89e4f45216a1b8 100644 (file)
@@ -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<CapturaRemoteFile>(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)
+      }
+    }
+  }
 }
index 5d00937968a6d39187d12674929a2635c35d1a9b..0a0f820dde0a70402baaf8054611dcb99e78d06a 100644 (file)
@@ -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 (file)
index 0000000..25c1988
--- /dev/null
@@ -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 (file)
index 0000000..3311233
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22189.1" systemVersion="23A5286i" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+    <entity name="CapturaRemoteFile" representedClassName="CapturaRemoteFile" syncable="YES" codeGenerationType="class">
+        <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="url" optional="YES" attributeType="String"/>
+    </entity>
+</model>
\ No newline at end of file
diff --git a/Captura/Data/CapturaRemoteFile+name.swift b/Captura/Data/CapturaRemoteFile+name.swift
new file mode 100644 (file)
index 0000000..feebdba
--- /dev/null
@@ -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 (file)
index 24d821e..0000000
+++ /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
-    }
-}
index d0d04f2506c3c851afc2e8620fc08fa64150c909..01ef78c56bf222a1ec2f960e284cff28134094e9 100644 (file)
@@ -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 (file)
index 0000000..4490e38
--- /dev/null
@@ -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
+    }
+}
index e5b77ce430651896ebc7a2610e5fc43273e72912..872d0c0fa4c698899a3b5b64d9692d47ac5c540c 100644 (file)
@@ -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)
         }
index 40eac0d319740606cedb60f5dfbfd3104e5808e1..6022be59201fb14f45917677763d8f49805ef2c9 100644 (file)
@@ -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()
   }
 }
index b9f212c63633a0b64fe6752ce2d3c5a6b9dfac4c..3395c549c65ba6a7734772354de906da45442f1e 100644 (file)
@@ -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