From e12d8b78519939888fa4d74b8397e6c2eb3fd74b Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Thu, 28 Nov 2024 22:16:47 +0100 Subject: [PATCH 1/7] fix: markdownlint, probably fix app store builds, fix swift 6 warnings --- .markdownlint.json | 5 ++++ README.md | 30 +++++++++++-------- .../xcschemes/xcschememanagement.plist | 2 +- ishare/App.swift | 2 +- ishare/Capture/VideoCapture.swift | 11 +++---- ishare/Views/MainMenuView.swift | 2 +- 6 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..4c98f54 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,5 @@ +{ + "MD013": false, + "MD033": false, + "MD041": false +} diff --git a/README.md b/README.md index aa86d6d..58532f7 100644 --- a/README.md +++ b/README.md @@ -58,34 +58,38 @@
Versatile Screen Capture - - **Custom Region**: Instantly and easily define and capture specific portions of your screen. - - **Window Capture**: Capture individual application windows without any clutter. - - **Entire Display Capture**: Snapshot your whole screen with a single action. +- **Custom Region**: Instantly and easily define and capture specific portions of your screen. +- **Window Capture**: Capture individual application windows without any clutter. +- **Entire Display Capture**: Snapshot your whole screen with a single action. +
Flexible Screen Recording - - **Video Recording**: Record videos of entire screens or specific windows. - - **GIF Recording**: Capture your moments in GIF format, perfect for quick shares. - - **Customizable Codecs and Compression**: Fine-tune the parameters of the output video files. +- **Video Recording**: Record videos of entire screens or specific windows. +- **GIF Recording**: Capture your moments in GIF format, perfect for quick shares. +- **Customizable Codecs and Compression**: Fine-tune the parameters of the output video files. +
Easy Uploading - - **Custom Upload Destinations**: Define your own server or service to upload your media. - - **Built-in Imgur Uploader**: Quickly upload your results to Imgur automatically. +- **Custom Upload Destinations**: Define your own server or service to upload your media. +- **Built-in Imgur Uploader**: Quickly upload your results to Imgur automatically. +
High Customizability - - **Custom Keybinds**: Set keyboard shortcuts that match your workflow. - - **File Format Preferences**: Choose the formats for your screenshots (e.g. PNG, JPG) and recordings. - - **Custom File Naming**: Define your own prefix for filenames, so you always know which app took the shot. - - **Custom Save Path**: Decide where exactly on your system you want to save your captures and recordings. - - **Application Exclusions**: Exclude specific apps from being recorded. +- **Custom Keybinds**: Set keyboard shortcuts that match your workflow. +- **File Format Preferences**: Choose the formats for your screenshots (e.g. PNG, JPG) and recordings. +- **Custom File Naming**: Define your own prefix for filenames, so you always know which app took the shot. +- **Custom Save Path**: Decide where exactly on your system you want to save your captures and recordings. +- **Application Exclusions**: Exclude specific apps from being recorded. +
diff --git a/ishare.xcodeproj/xcuserdata/adrian.xcuserdatad/xcschemes/xcschememanagement.plist b/ishare.xcodeproj/xcuserdata/adrian.xcuserdatad/xcschemes/xcschememanagement.plist index 423aa57..93e2896 100644 --- a/ishare.xcodeproj/xcuserdata/adrian.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/ishare.xcodeproj/xcuserdata/adrian.xcuserdatad/xcschemes/xcschememanagement.plist @@ -38,7 +38,7 @@ sharemenuext.xcscheme_^#shared#^_ orderHint - 4 + 2 SuppressBuildableAutocreation diff --git a/ishare/App.swift b/ishare/App.swift index 347ed5d..4533fa8 100644 --- a/ishare/App.swift +++ b/ishare/App.swift @@ -9,7 +9,7 @@ import Defaults import MenuBarExtraAccess import SwiftUI -#if GITHUB_RELEASE +#if canImport(Sparkle) import Sparkle #endif diff --git a/ishare/Capture/VideoCapture.swift b/ishare/Capture/VideoCapture.swift index 305ce43..66359ac 100644 --- a/ishare/Capture/VideoCapture.swift +++ b/ishare/Capture/VideoCapture.swift @@ -138,11 +138,11 @@ func exportGif(from videoURL: URL) async throws -> URL { let totalDuration = duration.seconds let frameRate: CGFloat = 30 let totalFrames = Int(totalDuration * TimeInterval(frameRate)) - var timeValues: [NSValue] = [] + var timeValues: [CMTime] = [] for frameNumber in 0 ..< totalFrames { let time = CMTime(seconds: Double(frameNumber) / Double(frameRate), preferredTimescale: Int32(NSEC_PER_SEC)) - timeValues.append(NSValue(time: time)) + timeValues.append(time) } let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) @@ -156,7 +156,7 @@ func exportGif(from videoURL: URL) async throws -> URL { let delayBetweenFrames: TimeInterval = 1.0 / TimeInterval(frameRate) let fileProperties: [String: Any] = [ kCGImagePropertyGIFDictionary as String: [ - kCGImagePropertyGIFLoopCount as String: 0, + kCGImagePropertyGIFLoopCount: 0, ], ] let frameProperties: [String: Any] = [ @@ -169,12 +169,13 @@ func exportGif(from videoURL: URL) async throws -> URL { let imageDestination = CGImageDestinationCreateWithURL(outputURL as CFURL, UTType.gif.identifier as CFString, totalFrames, nil)! CGImageDestinationSetProperties(imageDestination, fileProperties as CFDictionary) + let localTimeValues = timeValues return try await withCheckedThrowingContinuation { continuation in - generator.generateCGImagesAsynchronously(forTimes: timeValues) { requestedTime, resultingImage, _, _, _ in + generator.generateCGImagesAsynchronously(forTimes: localTimeValues.map { NSValue(time: $0) }) { requestedTime, resultingImage, _, _, _ in if let image = resultingImage { CGImageDestinationAddImage(imageDestination, image, frameProperties as CFDictionary) } - if requestedTime == timeValues.last?.timeValue { + if requestedTime == localTimeValues.last { let success = CGImageDestinationFinalize(imageDestination) if success { do { diff --git a/ishare/Views/MainMenuView.swift b/ishare/Views/MainMenuView.swift index 11416ca..fb32ea9 100644 --- a/ishare/Views/MainMenuView.swift +++ b/ishare/Views/MainMenuView.swift @@ -11,7 +11,7 @@ import ScreenCaptureKit import SettingsAccess import SwiftUI import UniformTypeIdentifiers -#if GITHUB_RELEASE +#if canImport(Sparkle) import Sparkle #endif From 480d630684a5462fb5193b9607b068adc0b5d114 Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Thu, 28 Nov 2024 22:55:49 +0100 Subject: [PATCH 2/7] feat: update packages and migrate to swift 6 --- ishare.xcodeproj/project.pbxproj | 75 ++++++++++ .../xcshareddata/swiftpm/Package.resolved | 30 ++-- .../xcshareddata/xcschemes/GitHub.xcscheme | 1 + .../xcshareddata/xcschemes/ishare.xcscheme | 1 + ishare/App.swift | 57 +++++--- ishare/Capture/CaptureEngine.swift | 39 +++-- .../Capture/ContentSharingPickerManager.swift | 83 +++++++++-- ishare/Capture/ImageCapture.swift | 58 ++++---- ishare/Capture/ScreenRecorder.swift | 29 ++-- ishare/Capture/VideoCapture.swift | 45 +++--- ishare/Http/Custom.swift | 136 ++++++++++-------- ishare/Http/Imgur.swift | 48 ++++--- ishare/Http/Upload.swift | 4 +- ishare/Http/UploadManager.swift | 19 +-- ishare/Util/AppState.swift | 34 ++++- ishare/Util/Constants.swift | 41 ++---- ishare/Util/CustomUploader.swift | 6 +- ishare/Util/ToastPopover.swift | 25 ++-- ishare/Views/MainMenuView.swift | 24 ++-- ishare/Views/Settings/UploaderSettings.swift | 16 +-- sharemenuext/ShareViewController.swift | 59 +++++--- 21 files changed, 539 insertions(+), 291 deletions(-) diff --git a/ishare.xcodeproj/project.pbxproj b/ishare.xcodeproj/project.pbxproj index df4968e..556f713 100644 --- a/ishare.xcodeproj/project.pbxproj +++ b/ishare.xcodeproj/project.pbxproj @@ -471,7 +471,22 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ENABLE_BARE_SLASH_REGEX = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; + SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; + SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES; + SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES; + SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES; + SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES; + SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES; + SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; + SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT = YES; + SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; + SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -534,7 +549,22 @@ MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_ENABLE_BARE_SLASH_REGEX = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; + SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; + SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES; + SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES; + SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES; + SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES; + SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES; + SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; + SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT = YES; + SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; + SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; + SWIFT_VERSION = 6.0; }; name = Release; }; @@ -571,6 +601,16 @@ SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; + SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = NO; + SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES; + SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = NO; + SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES; + SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES; + SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; + SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT = YES; + SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; SWIFT_VERSION = 5.0; }; name = Debug; @@ -610,6 +650,16 @@ SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; + SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = NO; + SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES; + SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = NO; + SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES; + SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES; + SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; + SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT = YES; + SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; SWIFT_VERSION = 5.0; }; name = Release; @@ -673,7 +723,22 @@ MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_ENABLE_BARE_SLASH_REGEX = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; + SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; + SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES; + SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES; + SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES; + SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES; + SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES; + SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; + SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT = YES; + SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; + SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES; + SWIFT_VERSION = 6.0; }; name = GitHub; }; @@ -711,6 +776,16 @@ SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES; + SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = NO; + SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES; + SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = NO; + SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES; + SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES; + SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES; + SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT = YES; + SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES; SWIFT_VERSION = 5.0; }; name = GitHub; diff --git a/ishare.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ishare.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8adb7e7..6e7271e 100644 --- a/ishare.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ishare.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "0ff71dfe84467feffceb6531c60ede9b64ff3b4c3d2be319eef820d7bd3a275c", + "originHash" : "457379c019910912d45c64f6cf194185cf499a36d1e88eb6d8948ac28538815f", "pins" : [ { "identity" : "alamofire", "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", - "version" : "5.9.1" + "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", + "version" : "5.10.2" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/BlueHuskyStudios/BezelNotification.git", "state" : { - "revision" : "4a4d9c485f2c019ed83bcfa23d1d1939610090de", - "version" : "2.0.0" + "revision" : "dcdd70b3abb50007d49a8ce23c14e07079d8b0f2", + "version" : "2.1.0" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sindresorhus/KeyboardShortcuts", "state" : { - "revision" : "09e4a10ed6b65b3a40fe07b6bf0c84809313efc4", - "version" : "2.0.0" + "revision" : "c3c361f409b8dbe1eab186078b41c330a6a82c9a", + "version" : "2.2.2" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/orchetect/MenuBarExtraAccess.git", "state" : { - "revision" : "8757eb7c2cd708320df92e6ad6572efe90e58f16", - "version" : "1.0.4" + "revision" : "f041bb68e9d464c907e240cf3d74f9086a10ad41", + "version" : "1.2.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/orchetect/SettingsAccess", "state" : { - "revision" : "dbd2726bda227ff1b8eac32c043668c3b390d6b5", - "version" : "1.2.0" + "revision" : "0fd73c8b5892e88acb13adb7f36a4ba9293a0061", + "version" : "1.4.0" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/RougeWare/Swift-Function-Tools.git", "state" : { - "revision" : "0bd76ca2c00120acf7ff840b5dcc1e700870ae22", - "version" : "1.2.3" + "revision" : "a854f4ed89b7e404019b5a09d24e067acc15432d", + "version" : "1.2.4" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftyJSON/SwiftyJSON.git", "state" : { - "revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", - "version" : "5.0.1" + "revision" : "af76cf3ef710b6ca5f8c05f3a31307d44a3c5828", + "version" : "5.0.2" } }, { diff --git a/ishare.xcodeproj/xcshareddata/xcschemes/GitHub.xcscheme b/ishare.xcodeproj/xcshareddata/xcschemes/GitHub.xcscheme index 76216eb..c0dbf4a 100644 --- a/ishare.xcodeproj/xcshareddata/xcschemes/GitHub.xcscheme +++ b/ishare.xcodeproj/xcshareddata/xcschemes/GitHub.xcscheme @@ -38,6 +38,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/ishare.xcodeproj/xcshareddata/xcschemes/ishare.xcscheme b/ishare.xcodeproj/xcshareddata/xcschemes/ishare.xcscheme index eb63c1e..1705130 100644 --- a/ishare.xcodeproj/xcshareddata/xcschemes/ishare.xcscheme +++ b/ishare.xcodeproj/xcshareddata/xcschemes/ishare.xcscheme @@ -38,6 +38,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/ishare/App.swift b/ishare/App.swift index 4533fa8..d21c0b8 100644 --- a/ishare/App.swift +++ b/ishare/App.swift @@ -60,10 +60,13 @@ struct ishare: App { print("Received file URL: \(fileURL.absoluteString)") @Default(.uploadType) var uploadType + let localFileURL = fileURL uploadFile(fileURL: fileURL, uploadType: uploadType) { - showToast(fileURL: fileURL) { - NSSound.beep() + Task { @MainActor in + showToast(fileURL: localFileURL) { + NSSound.beep() + } } } } @@ -82,24 +85,29 @@ struct ishare: App { updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: self, userDriverDelegate: nil) } + @MainActor func stopRecording() { + let wasRecordingGif = recordGif + let recorder = screenRecorder + Task { - await screenRecorder.stop { [self] result in - switch result { - case let .success(url): - print("Recording stopped successfully. URL: \(url)") - postRecordingTasks(url, recordGif) - case let .failure(error): - print("Error while stopping recording: \(error.localizedDescription)") + recorder?.stop { result in + Task { @MainActor in + switch result { + case let .success(url): + print("Recording stopped successfully. URL: \(url)") + postRecordingTasks(url, wasRecordingGif) + case let .failure(error): + print("Error while stopping recording: \(error.localizedDescription)") + } } } } } } #else - class AppDelegate: NSObject, NSApplicationDelegate { - private(set) static var shared: AppDelegate! = nil + @MainActor private(set) static var shared: AppDelegate! = nil var recordGif = false var screenRecorder: ScreenRecorder! @@ -118,10 +126,13 @@ struct ishare: App { print("Received file URL: \(fileURL.absoluteString)") @Default(.uploadType) var uploadType + let localFileURL = fileURL uploadFile(fileURL: fileURL, uploadType: uploadType) { - showToast(fileURL: fileURL) { - NSSound.beep() + Task { @MainActor in + showToast(fileURL: localFileURL) { + NSSound.beep() + } } } } @@ -138,15 +149,21 @@ struct ishare: App { } } + @MainActor func stopRecording() { + let wasRecordingGif = recordGif + let recorder = screenRecorder + Task { - await screenRecorder.stop { [self] result in - switch result { - case let .success(url): - print("Recording stopped successfully. URL: \(url)") - postRecordingTasks(url, recordGif) - case let .failure(error): - print("Error while stopping recording: \(error.localizedDescription)") + recorder?.stop { result in + Task { @MainActor in + switch result { + case let .success(url): + print("Recording stopped successfully. URL: \(url)") + postRecordingTasks(url, wasRecordingGif) + case let .failure(error): + print("Error while stopping recording: \(error.localizedDescription)") + } } } } diff --git a/ishare/Capture/CaptureEngine.swift b/ishare/Capture/CaptureEngine.swift index 5a995ec..5091b74 100644 --- a/ishare/Capture/CaptureEngine.swift +++ b/ishare/Capture/CaptureEngine.swift @@ -5,14 +5,14 @@ // Created by Adrian Castro on 29.07.23. // -import AVFAudio +@preconcurrency import AVFAudio import Combine import Defaults import Foundation import ScreenCaptureKit /// A structure that contains the video data to render. -struct CapturedFrame { +struct CapturedFrame : Sendable { static let invalid = CapturedFrame(surface: nil, contentRect: .zero, contentScale: 0, scaleFactor: 0) let surface: IOSurface? @@ -37,14 +37,18 @@ class CaptureEngine: NSObject, @unchecked Sendable, SCRecordingOutputDelegate { var audioLevels: AudioLevels { powerMeter.levels } // Store the the startCapture continuation, so that you can cancel it when you call stopCapture(). - private var continuation: AsyncThrowingStream.Continuation? + private var continuation: AsyncThrowingStream.Continuation? private var startTime = Date() private var streamOutput: CaptureEngineStreamOutput? /// - Tag: StartCapture - func startCapture(configuration: SCStreamConfiguration, filter: SCContentFilter, fileURL: URL) -> AsyncThrowingStream { - AsyncThrowingStream { continuation in + func startCapture(configuration: SCStreamConfiguration, filter: SCContentFilter, fileURL: URL) -> AsyncThrowingStream { + let config = configuration + let contentFilter = filter + let outputURL = fileURL + + return AsyncThrowingStream { continuation in // The stream output object. let output = CaptureEngineStreamOutput(continuation: continuation) streamOutput = output @@ -54,8 +58,8 @@ class CaptureEngine: NSObject, @unchecked Sendable, SCRecordingOutputDelegate { self.startTime = Date() do { - stream = SCStream(filter: filter, configuration: configuration, delegate: streamOutput) - self.fileURL = fileURL + stream = SCStream(filter: contentFilter, configuration: config, delegate: streamOutput) + self.fileURL = outputURL // Add a stream output to capture screen content. try stream?.addStreamOutput(streamOutput!, type: .screen, sampleHandlerQueue: videoSampleBufferQueue) @@ -63,7 +67,7 @@ class CaptureEngine: NSObject, @unchecked Sendable, SCRecordingOutputDelegate { let recordingConfiguration = SCRecordingOutputConfiguration() - recordingConfiguration.outputURL = fileURL + recordingConfiguration.outputURL = outputURL recordingConfiguration.outputFileType = recordMP4 ? .mp4 : .mov recordingConfiguration.videoCodecType = useHEVC ? .hevc : .h264 @@ -78,7 +82,7 @@ class CaptureEngine: NSObject, @unchecked Sendable, SCRecordingOutputDelegate { } } - func stopCapture() async -> (@escaping (Result) -> Void) -> Void { + func stopCapture() async -> (@escaping (Result) -> Void) -> Void { enum ScreenRecorderError: Error { case missingFileURL } @@ -97,9 +101,16 @@ class CaptureEngine: NSObject, @unchecked Sendable, SCRecordingOutputDelegate { /// - Tag: UpdateStreamConfiguration func update(configuration: SCStreamConfiguration, filter: SCContentFilter) async { + struct SendableParams: @unchecked Sendable { + let configuration: SCStreamConfiguration + let filter: SCContentFilter + } + + let params = SendableParams(configuration: configuration, filter: filter) + do { - try await stream?.updateConfiguration(configuration) - try await stream?.updateContentFilter(filter) + try await stream?.updateConfiguration(params.configuration) + try await stream?.updateContentFilter(params.filter) } catch { print("Failed to update the stream session: \(String(describing: error))") } @@ -112,9 +123,9 @@ private class CaptureEngineStreamOutput: NSObject, SCStreamOutput, SCStreamDeleg var capturedFrameHandler: ((CapturedFrame) -> Void)? // Store the the startCapture continuation, so you can cancel it if an error occurs. - private var continuation: AsyncThrowingStream.Continuation? + private var continuation: AsyncThrowingStream.Continuation? - init(continuation: AsyncThrowingStream.Continuation?) { + init(continuation: AsyncThrowingStream.Continuation?) { self.continuation = continuation } @@ -184,7 +195,7 @@ private class CaptureEngineStreamOutput: NSObject, SCStreamOutput, SCStreamDeleg return AVAudioPCMBuffer(pcmFormat: format, bufferListNoCopy: audioBufferList) } - func stream(_: SCStream, didStopWithError error: Error) { + func stream(_: SCStream, didStopWithError error: any Error) { if (error as NSError).code == -3817 { // User stopped the stream. Call AppDelegate's method to stop recording gracefully DispatchQueue.main.async { diff --git a/ishare/Capture/ContentSharingPickerManager.swift b/ishare/Capture/ContentSharingPickerManager.swift index e9dc479..31d4bf5 100644 --- a/ishare/Capture/ContentSharingPickerManager.swift +++ b/ishare/Capture/ContentSharingPickerManager.swift @@ -7,16 +7,55 @@ import Defaults import Foundation -import ScreenCaptureKit +@preconcurrency import ScreenCaptureKit + +actor CallbackStore { + private var contentSelected: (@Sendable (SCContentFilter, SCStream?) -> Void)? + private var contentSelectionFailed: (@Sendable (any Error) -> Void)? + private var contentSelectionCancelled: (@Sendable (SCStream?) -> Void)? + + func setContentSelectedCallback(_ callback: @Sendable @escaping (SCContentFilter, SCStream?) -> Void) { + contentSelected = callback + } + + func setContentSelectionFailedCallback(_ callback: @Sendable @escaping (any Error) -> Void) { + contentSelectionFailed = callback + } + + func setContentSelectionCancelledCallback(_ callback: @Sendable @escaping (SCStream?) -> Void) { + contentSelectionCancelled = callback + } + + func getContentSelectedCallback() -> (@Sendable (SCContentFilter, SCStream?) -> Void)? { + contentSelected + } + + func getContentSelectionFailedCallback() -> (@Sendable (any Error) -> Void)? { + contentSelectionFailed + } + + func getContentSelectionCancelledCallback() -> (@Sendable (SCStream?) -> Void)? { + contentSelectionCancelled + } +} @MainActor class ContentSharingPickerManager: NSObject, SCContentSharingPickerObserver { static let shared = ContentSharingPickerManager() private let picker = SCContentSharingPicker.shared + private let callbackStore = CallbackStore() - var contentSelected: ((SCContentFilter, SCStream?) -> Void)? - var contentSelectionFailed: ((Error) -> Void)? - var contentSelectionCancelled: ((SCStream?) -> Void)? + func setContentSelectedCallback(_ callback: @Sendable @escaping (SCContentFilter, SCStream?) -> Void) async { + await callbackStore.setContentSelectedCallback(callback) + } + + func setContentSelectionFailedCallback(_ callback: @Sendable @escaping (any Error) -> Void) async { + await callbackStore.setContentSelectionFailedCallback(callback) + } + + func setContentSelectionCancelledCallback(_ callback: @Sendable @escaping (SCStream?) -> Void) async { + await callbackStore.setContentSelectionCancelledCallback(callback) + } @Default(.ignoredBundleIdentifiers) var ignoredBundleIdentifiers @@ -41,20 +80,42 @@ class ContentSharingPickerManager: NSObject, SCContentSharingPickerObserver { } nonisolated func contentSharingPicker(_: SCContentSharingPicker, didUpdateWith filter: SCContentFilter, for stream: SCStream?) { - DispatchQueue.main.async { - self.contentSelected?(filter, stream) + struct SendableParams: @unchecked Sendable { + let filter: SCContentFilter + let stream: SCStream? + } + let params = SendableParams(filter: filter, stream: stream) + + Task { @MainActor in + if let callback = await ContentSharingPickerManager.shared.callbackStore.getContentSelectedCallback() { + callback(params.filter, params.stream) + } } } nonisolated func contentSharingPicker(_: SCContentSharingPicker, didCancelFor stream: SCStream?) { - DispatchQueue.main.async { - self.contentSelectionCancelled?(stream) + struct SendableParams: @unchecked Sendable { + let stream: SCStream? + } + let params = SendableParams(stream: stream) + + Task { @MainActor in + if let callback = await ContentSharingPickerManager.shared.callbackStore.getContentSelectionCancelledCallback() { + callback(params.stream) + } } } - nonisolated func contentSharingPickerStartDidFailWithError(_ error: Error) { - DispatchQueue.main.async { - self.contentSelectionFailed?(error) + nonisolated func contentSharingPickerStartDidFailWithError(_ error: any Error) { + struct SendableParams: @unchecked Sendable { + let error: any Error + } + let params = SendableParams(error: error) + + Task { @MainActor in + if let callback = await ContentSharingPickerManager.shared.callbackStore.getContentSelectionFailedCallback() { + callback(params.error) + } } } } diff --git a/ishare/Capture/ImageCapture.swift b/ishare/Capture/ImageCapture.swift index 750b8b8..c94a6e8 100644 --- a/ishare/Capture/ImageCapture.swift +++ b/ishare/Capture/ImageCapture.swift @@ -24,16 +24,17 @@ enum FileType: String, CaseIterable, Identifiable, Defaults.Serializable { var id: Self { self } } -func captureScreen(type: CaptureType, display: Int = 1) { - @Default(.capturePath) var capturePath - @Default(.captureFileType) var fileType - @Default(.captureFileName) var fileName - @Default(.copyToClipboard) var copyToClipboard - @Default(.openInFinder) var openInFinder - @Default(.uploadMedia) var uploadMedia - @Default(.captureBinary) var captureBinary - @Default(.uploadType) var uploadType - @Default(.saveToDisk) var saveToDisk +@MainActor +func captureScreen(type: CaptureType, display: Int = 1) async { + let capturePath = Defaults[.capturePath] + let fileType = Defaults[.captureFileType] + let fileName = Defaults[.captureFileName] + let copyToClipboard = Defaults[.copyToClipboard] + let openInFinder = Defaults[.openInFinder] + let uploadMedia = Defaults[.uploadMedia] + let captureBinary = Defaults[.captureBinary] + let uploadType = Defaults[.uploadType] + let saveToDisk = Defaults[.saveToDisk] let timestamp = Int(Date().timeIntervalSince1970) let uniqueFilename = "\(fileName)-\(timestamp)" @@ -56,7 +57,6 @@ func captureScreen(type: CaptureType, display: Int = 1) { if copyToClipboard { let pasteboard = NSPasteboard.general pasteboard.clearContents() - pasteboard.setString(fileURL.absoluteString, forType: .fileURL) } @@ -65,28 +65,36 @@ func captureScreen(type: CaptureType, display: Int = 1) { } if uploadMedia { + let shouldSaveToDisk = saveToDisk + let localFileURL = fileURL uploadFile(fileURL: fileURL, uploadType: uploadType) { - showToast(fileURL: fileURL) { - NSSound.beep() + Task { @MainActor in + showToast(fileURL: localFileURL) { + NSSound.beep() - if !saveToDisk { - do { - try FileManager.default.removeItem(at: fileURL) - } catch { - print("Error deleting the file: \(error)") + if !shouldSaveToDisk { + do { + try FileManager.default.removeItem(at: localFileURL) + } catch { + print("Error deleting the file: \(error)") + } } } } } } else { - showToast(fileURL: fileURL) { - NSSound.beep() + let shouldSaveToDisk = saveToDisk + let localFileURL = fileURL + Task { @MainActor in + showToast(fileURL: localFileURL) { + NSSound.beep() - if !saveToDisk { - do { - try FileManager.default.removeItem(at: fileURL) - } catch { - print("Error deleting the file: \(error)") + if !shouldSaveToDisk { + do { + try FileManager.default.removeItem(at: localFileURL) + } catch { + print("Error deleting the file: \(error)") + } } } } diff --git a/ishare/Capture/ScreenRecorder.swift b/ishare/Capture/ScreenRecorder.swift index d3e632a..4238c61 100644 --- a/ishare/Capture/ScreenRecorder.swift +++ b/ishare/Capture/ScreenRecorder.swift @@ -8,7 +8,7 @@ import Combine import Defaults import Foundation -import ScreenCaptureKit +@preconcurrency import ScreenCaptureKit import SwiftUI class AudioLevelsProvider: ObservableObject { @@ -42,21 +42,28 @@ class ScreenRecorder: ObservableObject { isRunning = true let pickerManager = ContentSharingPickerManager.shared - pickerManager.contentSelected = { [weak self] filter, _ in - Task { - await self?.startCapture(with: filter, fileURL: fileURL) + let localFileURL = fileURL + + await pickerManager.setContentSelectedCallback { [weak self] filter, _ in + Task { @MainActor [weak self] in + guard let self else { return } + await self.startCapture(with: filter, fileURL: localFileURL) } } - pickerManager.contentSelectionCancelled = { _ in - self.isRunning = false - Task { - self.stop(completion:) + await pickerManager.setContentSelectionCancelledCallback { [weak self] _ in + Task { @MainActor [weak self] in + guard let self else { return } + self.isRunning = false + self.stop { _ in } } } - pickerManager.contentSelectionFailed = { _ in - self.isRunning = false + await pickerManager.setContentSelectionFailedCallback { [weak self] _ in + Task { @MainActor [weak self] in + guard let self else { return } + self.isRunning = false + } } let config = SCStreamConfiguration() @@ -67,7 +74,7 @@ class ScreenRecorder: ObservableObject { pickerManager.showPicker() } - func stop(completion: @escaping (Result) -> Void) { + func stop(completion: @escaping (Result) -> Void) { Task { let stopClosure = await captureEngine.stopCapture() stopClosure { result in diff --git a/ishare/Capture/VideoCapture.swift b/ishare/Capture/VideoCapture.swift index 66359ac..5b5c801 100644 --- a/ishare/Capture/VideoCapture.swift +++ b/ishare/Capture/VideoCapture.swift @@ -5,7 +5,7 @@ // Created by Adrian Castro on 24.07.23. // -import AppKit +@preconcurrency import AppKit import AVFoundation import BezelNotification import Cocoa @@ -44,7 +44,7 @@ func recordScreen(gif: Bool? = false) { } } -func postRecordingTasks(_ URL: URL, _ recordGif: Bool) { +@MainActor func postRecordingTasks(_ URL: URL, _ recordGif: Bool) { @Default(.copyToClipboard) var copyToClipboard @Default(.openInFinder) var openInFinder @Default(.recordingPath) var recordingPath @@ -95,31 +95,39 @@ func postRecordingTasks(_ URL: URL, _ recordGif: Bool) { } if uploadMedia { + let shouldSaveToDisk = saveToDisk + let localFileURL = fileURL uploadFile(fileURL: fileURL, uploadType: uploadType) { - showToast(fileURL: fileURL) { + Task { @MainActor in + showToast(fileURL: localFileURL) { + NSSound.beep() + + if !shouldSaveToDisk { + do { + try FileManager.default.removeItem(at: localFileURL) + } catch { + print("Error deleting the file: \(error)") + } + } + } + } + } + } else { + let shouldSaveToDisk = saveToDisk + let localFileURL = fileURL + Task { @MainActor in + showToast(fileURL: localFileURL) { NSSound.beep() - if !saveToDisk { + if !shouldSaveToDisk { do { - try FileManager.default.removeItem(at: fileURL) + try FileManager.default.removeItem(at: localFileURL) } catch { print("Error deleting the file: \(error)") } } } } - } else { - showToast(fileURL: fileURL) { - NSSound.beep() - - if !saveToDisk { - do { - try FileManager.default.removeItem(at: fileURL) - } catch { - print("Error deleting the file: \(error)") - } - } - } } AppDelegate.shared.recordGif = false shareBasedOnPreferences(fileURL) @@ -170,10 +178,11 @@ func exportGif(from videoURL: URL) async throws -> URL { CGImageDestinationSetProperties(imageDestination, fileProperties as CFDictionary) let localTimeValues = timeValues + let localFrameProperties = frameProperties as CFDictionary return try await withCheckedThrowingContinuation { continuation in generator.generateCGImagesAsynchronously(forTimes: localTimeValues.map { NSValue(time: $0) }) { requestedTime, resultingImage, _, _, _ in if let image = resultingImage { - CGImageDestinationAddImage(imageDestination, image, frameProperties as CFDictionary) + CGImageDestinationAddImage(imageDestination, image, localFrameProperties) } if requestedTime == localTimeValues.last { let success = CGImageDestinationFinalize(imageDestination) diff --git a/ishare/Http/Custom.swift b/ishare/Http/Custom.swift index 6cd662e..203b093 100644 --- a/ishare/Http/Custom.swift +++ b/ishare/Http/Custom.swift @@ -7,6 +7,7 @@ import Alamofire import AppKit +import BezelNotification import Defaults import Foundation import SwiftyJSON @@ -17,7 +18,8 @@ enum CustomUploadError: Error { case fileReadError } -func customUpload(fileURL: URL, specification: CustomUploader, callback: ((Error?, URL?) -> Void)? = nil, completion: @escaping () -> Void) { +@MainActor +func customUpload(fileURL: URL, specification: CustomUploader, callback: (@Sendable ((any Error)?, URL?) -> Void)? = nil, completion: @Sendable @escaping () -> Void) { @Default(.captureFileType) var fileType guard specification.isValid() else { @@ -39,7 +41,12 @@ func customUpload(fileURL: URL, specification: CustomUploader, callback: ((Error } } -func uploadMultipartFormData(fileURL: URL, url: URL, headers: HTTPHeaders, specification: CustomUploader, callback: ((Error?, URL?) -> Void)?, completion: @escaping () -> Void) { +@MainActor +private func uploadMultipartFormData(fileURL: URL, url: URL, headers: HTTPHeaders, specification: CustomUploader, callback: (@Sendable ((any Error)?, URL?) -> Void)?, completion: @Sendable @escaping () -> Void) { + let uploadManager = UploadManager.shared + let localCallback = callback + let localCompletion = completion + AF.upload(multipartFormData: { multipartFormData in if let formData = specification.formData { for (key, value) in formData { @@ -53,73 +60,54 @@ func uploadMultipartFormData(fileURL: URL, url: URL, headers: HTTPHeaders, speci }, to: url, method: .post, headers: headers) .uploadProgress { progress in - UploadManager.shared.updateProgress(fraction: progress.fractionCompleted) + Task { @MainActor in + uploadManager.updateProgress(fraction: progress.fractionCompleted) + } } .response { response in - UploadManager.shared.uploadCompleted() - print(response) - if let data = response.data { - handleResponse(data: data, specification: specification, callback: callback, completion: completion) - } else { - callback?(CustomUploadError.responseRetrieval, nil) - completion() + Task { @MainActor in + uploadManager.uploadCompleted() + print(response) + if let data = response.data { + handleResponse(data: data, specification: specification, callback: localCallback, completion: localCompletion) + } else { + localCallback?(CustomUploadError.responseRetrieval, nil) + localCompletion() + } } } } -func uploadBinaryData(fileURL: URL, url: URL, headers: inout HTTPHeaders, specification: CustomUploader, callback: ((Error?, URL?) -> Void)?, completion: @escaping () -> Void) { +@MainActor +private func uploadBinaryData(fileURL: URL, url: URL, headers: inout HTTPHeaders, specification: CustomUploader, callback: (@Sendable ((any Error)?, URL?) -> Void)?, completion: @Sendable @escaping () -> Void) { + let uploadManager = UploadManager.shared + let localCallback = callback + let localCompletion = completion let mimeType = mimeTypeForPathExtension(fileURL.pathExtension) headers.add(name: "Content-Type", value: mimeType) AF.upload(fileURL, to: url, method: .post, headers: headers) .uploadProgress { progress in - UploadManager.shared.updateProgress(fraction: progress.fractionCompleted) - } - .response { response in - UploadManager.shared.uploadCompleted() - if let data = response.data { - handleResponse(data: data, specification: specification, callback: callback, completion: completion) - } else { - callback?(CustomUploadError.responseRetrieval, nil) - completion() + Task { @MainActor in + uploadManager.updateProgress(fraction: progress.fractionCompleted) } } -} - -func performDeletionRequest(deletionUrl: String, completion: @escaping (Result) -> Void) { - guard let url = URL(string: deletionUrl) else { - completion(.failure(CustomUploadError.responseParsing)) - return - } - - @Default(.activeCustomUploader) var activeCustomUploader - let uploader = CustomUploader.allCases.first(where: { $0.id == activeCustomUploader }) - let headers = HTTPHeaders(uploader?.headers ?? [:]) - - func sendRequest(with method: HTTPMethod) { - AF.request(url, method: method, headers: headers).response { response in - switch response.result { - case .success: - completion(.success("Deleted file successfully")) - case let .failure(error): - completion(.failure(error)) + .response { response in + Task { @MainActor in + uploadManager.uploadCompleted() + if let data = response.data { + handleResponse(data: data, specification: specification, callback: localCallback, completion: localCompletion) + } else { + localCallback?(CustomUploadError.responseRetrieval, nil) + localCompletion() + } } } - } - - switch uploader?.deleteRequestType { - case .GET: - sendRequest(with: .get) - case .DELETE: - sendRequest(with: .delete) - case nil: - sendRequest(with: .get) - } } -func handleResponse(data: Data, specification: CustomUploader, callback: ((Error?, URL?) -> Void)?, completion: @escaping () -> Void) { +@MainActor +private func handleResponse(data: Data, specification: CustomUploader, callback: (@Sendable ((any Error)?, URL?) -> Void)?, completion: @Sendable () -> Void) { let json = JSON(data) - let fileUrl = constructUrl(from: specification.responseURL, using: json) let deletionUrl = constructUrl(from: specification.deletionURL, using: json) @@ -137,7 +125,7 @@ func handleResponse(data: Data, specification: CustomUploader, callback: ((Error completion() } -func constructUrl(from format: String?, using json: JSON) -> String { +private func constructUrl(from format: String?, using json: JSON) -> String { guard let format else { return "" } let (taggedUrl, tags) = tagPlaceholders(in: format) var url = taggedUrl @@ -151,7 +139,7 @@ func constructUrl(from format: String?, using json: JSON) -> String { return url } -func tagPlaceholders(in url: String) -> (taggedUrl: String, tags: [(String, String)]) { +private func tagPlaceholders(in url: String) -> (taggedUrl: String, tags: [(String, String)]) { var taggedUrl = url var tags: [(String, String)] = [] @@ -170,7 +158,7 @@ func tagPlaceholders(in url: String) -> (taggedUrl: String, tags: [(String, Stri return (taggedUrl, tags) } -func getNestedJSONValue(json: JSON, keyPath: String) -> String? { +private func getNestedJSONValue(json: JSON, keyPath: String) -> String? { var currentJSON = json let keyPathElements = keyPath.components(separatedBy: ".") @@ -197,10 +185,10 @@ func getNestedJSONValue(json: JSON, keyPath: String) -> String? { return currentJSON.stringValue } -func fileNameWithLowercaseExtension(from url: URL) -> String { - let fileName = url.deletingPathExtension().lastPathComponent +private func fileNameWithLowercaseExtension(from url: URL) -> String { + let fileName = url.lastPathComponent let fileExtension = url.pathExtension.lowercased() - return fileExtension.isEmpty ? fileName : "\(fileName).\(fileExtension)" + return fileName.replacingOccurrences(of: url.pathExtension, with: fileExtension) } func mimeTypeForPathExtension(_ ext: String) -> String { @@ -221,3 +209,37 @@ func mimeTypeForPathExtension(_ ext: String) -> String { "application/octet-stream" } } + +@MainActor +func performDeletionRequest(deletionUrl: String, completion: @Sendable @escaping (Result) -> Void) { + guard let url = URL(string: deletionUrl) else { + completion(.failure(CustomUploadError.responseParsing)) + return + } + + @Default(.activeCustomUploader) var activeCustomUploader + let uploader = CustomUploader.allCases.first(where: { $0.id == activeCustomUploader }) + let headers = HTTPHeaders(uploader?.headers ?? [:]) + + func sendRequest(with method: HTTPMethod) { + AF.request(url, method: method, headers: headers).response { response in + Task { @MainActor in + switch response.result { + case .success: + completion(.success("Deleted file successfully")) + case let .failure(error): + completion(.failure(error)) + } + } + } + } + + switch uploader?.deleteRequestType { + case .GET: + sendRequest(with: .get) + case .DELETE: + sendRequest(with: .delete) + case nil: + sendRequest(with: .get) + } +} diff --git a/ishare/Http/Imgur.swift b/ishare/Http/Imgur.swift index 953cc88..a23dbf8 100644 --- a/ishare/Http/Imgur.swift +++ b/ishare/Http/Imgur.swift @@ -12,8 +12,9 @@ import Defaults import Foundation import SwiftyJSON -func imgurUpload(_ fileURL: URL, completion: @escaping () -> Void) { +@MainActor func imgurUpload(_ fileURL: URL, completion: @Sendable @escaping () -> Void) { @Default(.imgurClientId) var imgurClientId + let uploadManager = UploadManager.shared let url = "https://api.imgur.com/3/upload" @@ -25,31 +26,40 @@ func imgurUpload(_ fileURL: URL, completion: @escaping () -> Void) { multipartFormData.append(fileURL, withName: fileFormName, fileName: fileName, mimeType: mimeType) }, to: url, method: .post, headers: ["Authorization": "Client-ID " + imgurClientId]) .uploadProgress { progress in - UploadManager.shared.updateProgress(fraction: progress.fractionCompleted) + Task { @MainActor in + uploadManager.updateProgress(fraction: progress.fractionCompleted) + } } .response { response in - UploadManager.shared.uploadCompleted() - if let data = response.data { - let json = JSON(data) - if let link = json["data"]["link"].string { - print("Image uploaded successfully. Link: \(link)") - - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setString(link, forType: .string) - - let historyItem = HistoryItem(fileUrl: link) - addToUploadHistory(historyItem) - completion() - } else { - print("Error parsing response or retrieving image link") - BezelNotification.show(messageText: "An error occured", icon: ToastIcon) - completion() + Task { @MainActor in + uploadManager.uploadCompleted() + if let data = response.data { + let json = JSON(data) + if let link = json["data"]["link"].string { + print("Image uploaded successfully. Link: \(link)") + + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(link, forType: .string) + + let historyItem = HistoryItem(fileUrl: link) + addToUploadHistory(historyItem) + completion() + } else { + print("Error parsing response or retrieving image link") + showErrorNotification() + completion() + } } } } } +@MainActor +private func showErrorNotification() { + BezelNotification.show(messageText: "An error occured", icon: ToastIcon) +} + func determineFileFormName(for fileURL: URL) -> String { switch fileURL.pathExtension.lowercased() { case "mp4", "mov": diff --git a/ishare/Http/Upload.swift b/ishare/Http/Upload.swift index 4891ab1..45e083c 100644 --- a/ishare/Http/Upload.swift +++ b/ishare/Http/Upload.swift @@ -14,8 +14,8 @@ enum UploadType: String, CaseIterable, Identifiable, Codable, Defaults.Serializa var id: Self { self } } -func uploadFile(fileURL: URL, uploadType: UploadType, completion: @escaping () -> Void) { - @Default(.activeCustomUploader) var activeUploader +@MainActor func uploadFile(fileURL: URL, uploadType: UploadType, completion: @Sendable @escaping () -> Void) { + let activeUploader = Defaults[.activeCustomUploader] switch uploadType { case .IMGUR: diff --git a/ishare/Http/UploadManager.swift b/ishare/Http/UploadManager.swift index 326a283..a5c2bb8 100644 --- a/ishare/Http/UploadManager.swift +++ b/ishare/Http/UploadManager.swift @@ -9,17 +9,18 @@ import AppKit import Foundation import SwiftUI -class UploadManager { +@MainActor +final class UploadManager: @unchecked Sendable { static let shared = UploadManager() - var progress = Progress() - var statusItem: NSStatusItem? - var hostingView: NSHostingView? + private var progress = Progress() + private var statusItem: NSStatusItem? + private var hostingView: NSHostingView? - init() { + private init() { setupMenu() } - func setupMenu() { + private func setupMenu() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem?.button { @@ -33,14 +34,14 @@ class UploadManager { } func updateProgress(fraction: Double) { - DispatchQueue.main.async { + Task { @MainActor in self.progress.completedUnitCount = Int64(fraction * 100) self.hostingView?.rootView = CircularProgressView(progress: fraction) } } func uploadCompleted() { - DispatchQueue.main.async { + Task { @MainActor in if let item = self.statusItem { NSStatusBar.system.removeStatusItem(item) self.statusItem = nil @@ -50,7 +51,7 @@ class UploadManager { } struct CircularProgressView: View { - var progress: Double + let progress: Double var body: some View { ZStack { diff --git a/ishare/Util/AppState.swift b/ishare/Util/AppState.swift index 6ee1f90..10e597b 100644 --- a/ishare/Util/AppState.swift +++ b/ishare/Util/AppState.swift @@ -9,29 +9,49 @@ import Defaults import KeyboardShortcuts import SwiftUI +@MainActor final class AppState: ObservableObject { + static let shared = AppState() + @Default(.showMainMenu) var showMainMenu @Default(.uploadHistory) var uploadHistory init() { - KeyboardShortcuts.onKeyUp(for: .toggleMainMenu) { [self] in - showMainMenu = true + setupKeyboardShortcuts() + } + + func setupKeyboardShortcuts() { + KeyboardShortcuts.onKeyUp(for: .toggleMainMenu) { [weak self] in + self?.showMainMenu = true } - KeyboardShortcuts.onKeyUp(for: .openHistoryWindow) { [self] in - openHistoryWindow(uploadHistory: uploadHistory) + + KeyboardShortcuts.onKeyUp(for: .openHistoryWindow) { [weak self] in + guard let self = self else { return } + openHistoryWindow(uploadHistory: self.uploadHistory) } + KeyboardShortcuts.onKeyUp(for: .captureRegion) { - captureScreen(type: .REGION) + Task { @MainActor in + await captureScreen(type: .REGION) + } } + KeyboardShortcuts.onKeyUp(for: .captureWindow) { - captureScreen(type: .WINDOW) + Task { @MainActor in + await captureScreen(type: .WINDOW) + } } + KeyboardShortcuts.onKeyUp(for: .captureScreen) { - captureScreen(type: .SCREEN) + Task { @MainActor in + await captureScreen(type: .SCREEN) + } } + KeyboardShortcuts.onKeyUp(for: .recordScreen) { recordScreen() } + KeyboardShortcuts.onKeyUp(for: .recordGif) { recordScreen(gif: true) } diff --git a/ishare/Util/Constants.swift b/ishare/Util/Constants.swift index bff90d6..e259c05 100644 --- a/ishare/Util/Constants.swift +++ b/ishare/Util/Constants.swift @@ -1,5 +1,5 @@ // -// Constants.swift +// Constants.swift // ishare // // Created by Adrian Castro on 12.07.23. @@ -55,18 +55,6 @@ extension Defaults.Keys { static let aussieMode = Key("aussieMode", default: false, iCloud: true) } -public extension View { - func keyboardShortcut(_ shortcut: KeyboardShortcuts.Name) -> some View { - if let shortcut = shortcut.shortcut { - if let keyEquivalent = shortcut.toKeyEquivalent() { - return AnyView(keyboardShortcut(keyEquivalent, modifiers: shortcut.toEventModifiers())) - } - } - - return AnyView(self) - } -} - extension KeyboardShortcuts.Shortcut { func toKeyEquivalent() -> KeyEquivalent? { let carbonKeyCode = UInt16(carbonKeyCode) @@ -223,7 +211,7 @@ func importIscu(_ url: URL) { } } -func importFile(_ url: URL, completion: @escaping (Bool, Error?) -> Void) { +func importFile(_ url: URL, completion: @escaping (Bool, (any Error)?) -> Void) { do { let data = try Data(contentsOf: url) let decoder = JSONDecoder() @@ -260,7 +248,7 @@ struct Contributor: Codable { } } -let AppIcon: NSImage = { +@MainActor let AppIcon: NSImage = { let appIconImage = NSImage(named: "AppIcon") let ratio = (appIconImage?.size.height)! / (appIconImage?.size.width)! let newSize = NSSize(width: 18, height: 18 / ratio) @@ -271,7 +259,7 @@ let AppIcon: NSImage = { return resizedImage }() -let GlyphIcon: NSImage = { +@MainActor let GlyphIcon: NSImage = { let appIconImage = NSImage(named: "GlyphIcon")! let ratio = appIconImage.size.height / appIconImage.size.width let newSize = NSSize(width: 18, height: 18 / ratio) @@ -282,7 +270,7 @@ let GlyphIcon: NSImage = { return resizedImage }() -let ImgurIcon: NSImage = { +@MainActor let ImgurIcon: NSImage = { let appIconImage = NSImage(named: "Imgur") let ratio = (appIconImage?.size.height)! / (appIconImage?.size.width)! let newSize = NSSize(width: 18, height: 18 / ratio) @@ -293,7 +281,7 @@ let ImgurIcon: NSImage = { return resizedImage }() -let ToastIcon: NSImage = { +@MainActor let ToastIcon: NSImage = { let toastIconImage = NSImage(named: "AppIcon") let ratio = (toastIconImage?.size.height)! / (toastIconImage?.size.width)! let newSize = NSSize(width: 100, height: 100 / ratio) @@ -313,7 +301,7 @@ func icon(forAppWithName appName: String) -> NSImage? { let airdropIconPath = Bundle.path(forResource: "AirDrop", ofType: "icns", inDirectory: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources") -let airdropIcon = NSImage(contentsOfFile: airdropIconPath!) +@MainActor let airdropIcon = NSImage(contentsOfFile: airdropIconPath!) struct SharingPreferences: Codable, Defaults.Serializable { var airdrop: Bool = false @@ -323,7 +311,7 @@ struct SharingPreferences: Codable, Defaults.Serializable { } func shareBasedOnPreferences(_ fileURL: URL) { - @Default(.builtInShare) var preferences + let preferences = Defaults[.builtInShare] if preferences.airdrop { NSSharingService(named: .sendViaAirDrop)?.perform(withItems: [fileURL]) @@ -356,15 +344,12 @@ struct HistoryItem: Codable, Hashable, Defaults.Serializable { } func addToUploadHistory(_ item: HistoryItem) { - @Default(.uploadHistory) var uploadHistory - - // Add new history item at the beginning of the array - uploadHistory.insert(item, at: 0) - - // Limit the history size - if uploadHistory.count > 50 { - uploadHistory.removeLast() + var history = Defaults[.uploadHistory] + history.insert(item, at: 0) + if history.count > 50 { + history.removeLast() } + Defaults[.uploadHistory] = history } struct ExcludedAppsView: View { diff --git a/ishare/Util/CustomUploader.swift b/ishare/Util/CustomUploader.swift index 0d761a6..3cf462b 100644 --- a/ishare/Util/CustomUploader.swift +++ b/ishare/Util/CustomUploader.swift @@ -56,7 +56,7 @@ struct CustomUploader: Codable, Hashable, Equatable, CaseIterable, Identifiable, case deleteRequestType = "deleterequesttype" } - init(from decoder: Decoder) throws { + init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: DynamicCodingKey.self) id = try container.decodeDynamicIfPresent(UUID.self, forKey: DynamicCodingKey(stringValue: "id")!) ?? UUID() @@ -113,13 +113,13 @@ struct CustomUploader: Codable, Hashable, Equatable, CaseIterable, Identifiable, } if let headers { - guard headers as Codable is [String: String] else { + guard headers as (any Codable) is [String: String] else { return false } } if let formData { - guard formData as Codable is [String: String] else { + guard formData as (any Codable) is [String: String] else { return false } } diff --git a/ishare/Util/ToastPopover.swift b/ishare/Util/ToastPopover.swift index 44facb6..8e77f83 100644 --- a/ishare/Util/ToastPopover.swift +++ b/ishare/Util/ToastPopover.swift @@ -37,8 +37,10 @@ struct ToastPopoverView: View { } } -func showToast(fileURL: URL, completion: (() -> Void)? = nil) { +@MainActor +func showToast(fileURL: URL, completion: (@Sendable () -> Void)? = nil) { if fileURL.pathExtension == "mov" || fileURL.pathExtension == "mp4" { + let localCompletion = completion Task.detached(priority: .userInitiated) { let asset = AVURLAsset(url: fileURL) let imageGenerator = AVAssetImageGenerator(asset: asset) @@ -57,7 +59,7 @@ func showToast(fileURL: URL, completion: (() -> Void)? = nil) { await MainActor.run { let thumbnailImage = NSImage(cgImage: cgImage.image, size: CGSize(width: width, height: height)) - showThumbnailAndToast(fileURL: fileURL, thumbnailImage: thumbnailImage, completion: completion) + showThumbnailAndToast(fileURL: fileURL, thumbnailImage: thumbnailImage, completion: localCompletion) } } catch { print("Error generating thumbnail: \(error)") @@ -68,20 +70,19 @@ func showToast(fileURL: URL, completion: (() -> Void)? = nil) { } } -func showThumbnailAndToast(fileURL: URL, thumbnailImage: NSImage, completion: (() -> Void)?) { - @Default(.toastTimeout) var toastTimeout - +@MainActor +private func showThumbnailAndToast(fileURL: URL, thumbnailImage: NSImage, completion: (() -> Void)? = nil) { + let toastTimeout = Defaults[.toastTimeout] + let localCompletion = completion let toastWindow = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 250, height: 150), - styleMask: [.borderless, .nonactivatingPanel], + contentRect: NSRect(x: 0, y: 0, width: 300, height: 100), + styleMask: [.borderless], backing: .buffered, defer: false ) - - toastWindow.level = .floating - toastWindow.isOpaque = false toastWindow.backgroundColor = .clear - toastWindow.isMovableByWindowBackground = true + toastWindow.isOpaque = false + toastWindow.level = .floating toastWindow.contentView = NSHostingView( rootView: ToastPopoverView(thumbnailImage: thumbnailImage, fileURL: fileURL) ) @@ -105,7 +106,7 @@ func showThumbnailAndToast(fileURL: URL, thumbnailImage: NSImage, completion: (( toastWindow.animator().alphaValue = 0.0 }) { toastWindow.orderOut(nil) - completion?() + localCompletion?() } } } diff --git a/ishare/Views/MainMenuView.swift b/ishare/Views/MainMenuView.swift index fb32ea9..ec981e3 100644 --- a/ishare/Views/MainMenuView.swift +++ b/ishare/Views/MainMenuView.swift @@ -75,28 +75,34 @@ struct MainMenuView: View { VStack { Menu { Button { - captureScreen(type: .REGION) + Task { + await captureScreen(type: .REGION) + } } label: { Image(systemName: "uiwindow.split.2x1") Text("Capture Region") - }.keyboardShortcut(.captureRegion) + }.globalKeyboardShortcut(.captureRegion) Button { - captureScreen(type: .WINDOW) + Task { + await captureScreen(type: .WINDOW) + } } label: { Image(systemName: "macwindow.on.rectangle") Text("Capture Window") - }.keyboardShortcut(.captureWindow) + }.globalKeyboardShortcut(.captureWindow) ForEach(NSScreen.screens.indices, id: \.self) { index in let screen = NSScreen.screens[index] let screenName = screen.localizedName Button { - captureScreen(type: .SCREEN, display: index + 1) + Task { + await captureScreen(type: .SCREEN, display: index + 1) + } } label: { Image(systemName: "macwindow") Text("Capture \(screenName)") - }.keyboardShortcut(index == 0 ? .captureScreen : .noKeybind) + }.globalKeyboardShortcut(index == 0 ? .captureScreen : .noKeybind) } } label: { Image(systemName: "photo.on.rectangle.angled") @@ -109,7 +115,7 @@ struct MainMenuView: View { label: { Image(systemName: "menubar.dock.rectangle.badge.record") Text("Record") - }.keyboardShortcut(.recordScreen).disabled(AppDelegate.shared?.screenRecorder?.isRunning ?? false) + }.globalKeyboardShortcut(.recordScreen).disabled(AppDelegate.shared?.screenRecorder?.isRunning ?? false) Button { recordScreen(gif: true) @@ -117,7 +123,7 @@ struct MainMenuView: View { label: { Image(systemName: "photo.stack") Text("Record GIF") - }.keyboardShortcut(.recordGif).disabled(AppDelegate.shared?.screenRecorder?.isRunning ?? false) + }.globalKeyboardShortcut(.recordGif).disabled(AppDelegate.shared?.screenRecorder?.isRunning ?? false) } VStack { Menu { @@ -218,7 +224,7 @@ struct MainMenuView: View { } label: { Image(systemName: "clock.arrow.circlepath") Text("Open History Window") - }.keyboardShortcut(.openHistoryWindow) + }.globalKeyboardShortcut(.openHistoryWindow) Divider() diff --git a/ishare/Views/Settings/UploaderSettings.swift b/ishare/Views/Settings/UploaderSettings.swift index a526e59..bcb54b2 100644 --- a/ishare/Views/Settings/UploaderSettings.swift +++ b/ishare/Views/Settings/UploaderSettings.swift @@ -166,19 +166,17 @@ struct UploaderSettingsView: View { return } - let callback: ((Error?, URL?) -> Void) = { error, finalURL in - if let error { - print("Upload error: \(error)") - DispatchQueue.main.async { + let callback = { @Sendable (error: (any Error)?, finalURL: URL?) -> Void in + Task { @MainActor in + if let error { + print("Upload error: \(error)") let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "Upload Error" alert.informativeText = "An error occurred during the upload process." alert.runModal() - } - } else if let url = finalURL { - print("Final URL: \(url)") - DispatchQueue.main.async { + } else if let url = finalURL { + print("Final URL: \(url)") let alert = NSAlert() alert.alertStyle = .informational alert.messageText = "Upload Successful" @@ -552,7 +550,7 @@ struct ImportCustomUploaderView: View { struct ImportError: Identifiable { let id = UUID() - let error: Error + let error: any Error var localizedDescription: String { error.localizedDescription diff --git a/sharemenuext/ShareViewController.swift b/sharemenuext/ShareViewController.swift index cdbccc9..83d7fbc 100644 --- a/sharemenuext/ShareViewController.swift +++ b/sharemenuext/ShareViewController.swift @@ -9,6 +9,7 @@ import Cocoa import Social import UniformTypeIdentifiers +@MainActor class ShareViewController: SLComposeServiceViewController { override func loadView() { // Do nothing to avoid displaying any UI @@ -32,21 +33,29 @@ class ShareViewController: SLComposeServiceViewController { .webP ] - if let item = (extensionContext!.inputItems as? [NSExtensionItem])?.first, - let provider = item.attachments?.first(where: { provider in - supportedTypes.contains(where: { provider.hasItemConformingToTypeIdentifier($0.identifier) }) - }) - { - let typeIdentifier = supportedTypes.first { provider.hasItemConformingToTypeIdentifier($0.identifier) }?.identifier ?? UTType.data.identifier - provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in - var url: URL? + guard let extensionContext = extensionContext, + let item = (extensionContext.inputItems as? [NSExtensionItem])?.first, + let provider = item.attachments?.first(where: { provider in + supportedTypes.contains(where: { provider.hasItemConformingToTypeIdentifier($0.identifier) }) + }) + else { + extensionContext?.completeRequest(returningItems: nil) + return + } + let typeIdentifier = supportedTypes.first { provider.hasItemConformingToTypeIdentifier($0.identifier) }?.identifier ?? UTType.data.identifier + let localTypeIdentifier = typeIdentifier + + provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { [weak self] item, _ in + guard let item = item else { return } + + let processItem = { (item: (any NSSecureCoding)) -> URL? in if let urlItem = item as? URL { - url = urlItem + return urlItem } else if let data = item as? Data { guard let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as! String) else { NSLog("Failed to get shared container URL") - return + return nil } let tempDir = sharedContainerURL.appendingPathComponent("tmp", isDirectory: true) @@ -56,34 +65,40 @@ class ShareViewController: SLComposeServiceViewController { try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil) } catch { NSLog("Failed to create temporary directory in shared container: \(error)") - return + return nil } - let utType = UTType(typeIdentifier) + let utType = UTType(localTypeIdentifier) let fileExtension = utType?.preferredFilenameExtension ?? "dat" - let fileName = UUID().uuidString + "." + fileExtension let fileURL = tempDir.appendingPathComponent(fileName) do { try data.write(to: fileURL) - url = fileURL + return fileURL } catch { NSLog("Failed to write data to shared container URL: \(error)") + return nil } } - - if let url = url { - if let encodedURLString = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), - let shareURL = URL(string: "ishare://upload?file=\(encodedURLString)") - { + return nil + } + + if let url = processItem(item) { + if let encodedURLString = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let shareURL = URL(string: "ishare://upload?file=\(encodedURLString)") + { + Task { @MainActor in NSWorkspace.shared.open(shareURL) + self?.extensionContext?.completeRequest(returningItems: nil) } - self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) - } else { - NSLog("No valid URL found") + } + } else { + Task { @MainActor in + self?.extensionContext?.completeRequest(returningItems: nil) } } } } } + From aa21f000cc77b06b3169bafb2a3c53e36c6b74fd Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:20:32 +0100 Subject: [PATCH 3/7] chore: close #57 --- .github/ISSUE_TEMPLATE/bug_report.md | 17 ++++++---- ishare/App.swift | 17 ++++++++-- ishare/Capture/ImageCapture.swift | 6 ++++ ishare/Capture/ScreenRecorder.swift | 10 ++++-- ishare/Capture/VideoCapture.swift | 9 ++--- ishare/Http/Custom.swift | 7 ++-- ishare/Http/Imgur.swift | 3 +- ishare/Http/UploadManager.swift | 2 ++ ishare/Util/AppState.swift | 5 +++ ishare/Util/Constants.swift | 46 ++++++++------------------ sharemenuext/ShareViewController.swift | 7 +++- 11 files changed, 78 insertions(+), 51 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..3205926 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,6 +12,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +25,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/ishare/App.swift b/ishare/App.swift index d21c0b8..53a1a45 100644 --- a/ishare/App.swift +++ b/ishare/App.swift @@ -47,28 +47,36 @@ struct ishare: App { func application(_: NSApplication, open urls: [URL]) { if urls.first!.isFileURL { + NSLog("Attempting to import ISCU file from: %@", urls.first!.path) importIscu(urls.first!) } if let url = urls.first { + NSLog("Processing URL scheme: %@", url.absoluteString) let path = url.host let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems if path == "upload" { if let fileItem = queryItems?.first(where: { $0.name == "file" }) { - if let encodedFileURLString = fileItem.value, let decodedFileURLString = encodedFileURLString.removingPercentEncoding, let fileURL = URL(string: decodedFileURLString) { - print("Received file URL: \(fileURL.absoluteString)") + if let encodedFileURLString = fileItem.value, + let decodedFileURLString = encodedFileURLString.removingPercentEncoding, + let fileURL = URL(string: decodedFileURLString) { + NSLog("Processing upload request for file: %@", fileURL.absoluteString) @Default(.uploadType) var uploadType + NSLog("Using upload type: %@", String(describing: uploadType)) let localFileURL = fileURL uploadFile(fileURL: fileURL, uploadType: uploadType) { Task { @MainActor in + NSLog("Upload completed, showing toast notification") showToast(fileURL: localFileURL) { NSSound.beep() } } } + } else { + NSLog("Error: Failed to process file URL from query parameters") } } } @@ -76,13 +84,18 @@ struct ishare: App { } func applicationDidFinishLaunching(_: Notification) { + NSLog("Application finished launching") AppDelegate.shared = self Task { + NSLog("Initializing screen recorder") screenRecorder = ScreenRecorder() } + #if GITHUB_RELEASE + NSLog("Initializing updater controller for GitHub release") updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: self, userDriverDelegate: nil) + #endif } @MainActor diff --git a/ishare/Capture/ImageCapture.swift b/ishare/Capture/ImageCapture.swift index c94a6e8..60b74d2 100644 --- a/ishare/Capture/ImageCapture.swift +++ b/ishare/Capture/ImageCapture.swift @@ -26,6 +26,8 @@ enum FileType: String, CaseIterable, Identifiable, Defaults.Serializable { @MainActor func captureScreen(type: CaptureType, display: Int = 1) async { + NSLog("Starting screen capture with type: %@, display: %d", type.rawValue, display) + let capturePath = Defaults[.capturePath] let fileType = Defaults[.captureFileType] let fileName = Defaults[.captureFileName] @@ -45,14 +47,18 @@ func captureScreen(type: CaptureType, display: Int = 1) async { let task = Process() task.launchPath = captureBinary task.arguments = type == CaptureType.SCREEN ? [type.rawValue, fileType.rawValue, "-D", "\(display)", path] : [type.rawValue, fileType.rawValue, path] + + NSLog("Executing capture command: %@ %@", captureBinary, task.arguments?.joined(separator: " ") ?? "") task.launch() task.waitUntilExit() let fileURL = URL(fileURLWithPath: path) if !FileManager.default.fileExists(atPath: fileURL.path) { + NSLog("Error: Capture file not created at path: %@", path) return } + NSLog("Screen capture completed successfully") if copyToClipboard { let pasteboard = NSPasteboard.general diff --git a/ishare/Capture/ScreenRecorder.swift b/ishare/Capture/ScreenRecorder.swift index 4238c61..847a123 100644 --- a/ishare/Capture/ScreenRecorder.swift +++ b/ishare/Capture/ScreenRecorder.swift @@ -28,17 +28,23 @@ class ScreenRecorder: ObservableObject { var canRecord: Bool { get async { do { - // If the app doesn't have Screen Recording permission, this call generates an exception. + NSLog("Checking screen recording permissions") try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) + NSLog("Screen recording permissions granted") return true } catch { + NSLog("Screen recording permissions denied: %@", error.localizedDescription) return false } } } func start(_ fileURL: URL) async { - guard !isRunning else { return } + guard !isRunning else { + NSLog("Recording already in progress") + return + } + NSLog("Starting screen recording to: %@", fileURL.path) isRunning = true let pickerManager = ContentSharingPickerManager.shared diff --git a/ishare/Capture/VideoCapture.swift b/ishare/Capture/VideoCapture.swift index 5b5c801..0956955 100644 --- a/ishare/Capture/VideoCapture.swift +++ b/ishare/Capture/VideoCapture.swift @@ -16,6 +16,7 @@ import SwiftUI @MainActor func recordScreen(gif: Bool? = false) { + NSLog("Starting screen recording, gif mode: %@", String(describing: gif)) @Default(.openInFinder) var openInFinder @Default(.recordingPath) var recordingPath @Default(.recordingFileName) var fileName @@ -23,12 +24,10 @@ func recordScreen(gif: Bool? = false) { let timestamp = Int(Date().timeIntervalSince1970) let uniqueFilename = "\(fileName)-\(timestamp)" - - var path = "\(recordingPath)\(uniqueFilename).\(recordMP4 ? "mp4" : "mov")" - path = NSString(string: path).expandingTildeInPath + let path = NSString(string: "\(recordingPath)\(uniqueFilename).\(recordMP4 ? "mp4" : "mov")").expandingTildeInPath + NSLog("Recording to path: %@", path) let fileURL = URL(fileURLWithPath: path) - let screenRecorder = AppDelegate.shared.screenRecorder if gif ?? false { @@ -37,8 +36,10 @@ func recordScreen(gif: Bool? = false) { Task { if await (screenRecorder?.canRecord) != nil { + NSLog("Starting screen recording") await screenRecorder?.start(fileURL) } else { + NSLog("Screen recording permission denied") BezelNotification.show(messageText: "Missing permission", icon: ToastIcon) } } diff --git a/ishare/Http/Custom.swift b/ishare/Http/Custom.swift index 203b093..40473e7 100644 --- a/ishare/Http/Custom.swift +++ b/ishare/Http/Custom.swift @@ -20,15 +20,18 @@ enum CustomUploadError: Error { @MainActor func customUpload(fileURL: URL, specification: CustomUploader, callback: (@Sendable ((any Error)?, URL?) -> Void)? = nil, completion: @Sendable @escaping () -> Void) { - @Default(.captureFileType) var fileType + NSLog("Starting custom upload for file: %@", fileURL.path) + NSLog("Using uploader: %@", specification.name) guard specification.isValid() else { - print("Invalid specification") + NSLog("Error: Invalid uploader specification") completion() return } let url = URL(string: specification.requestURL)! + NSLog("Uploading to endpoint: %@", url.absoluteString) + var headers = HTTPHeaders(specification.headers ?? [:]) let fileName = fileURL.lastPathComponent headers.add(name: "x-file-name", value: fileName) diff --git a/ishare/Http/Imgur.swift b/ishare/Http/Imgur.swift index a23dbf8..8781da4 100644 --- a/ishare/Http/Imgur.swift +++ b/ishare/Http/Imgur.swift @@ -13,6 +13,7 @@ import Foundation import SwiftyJSON @MainActor func imgurUpload(_ fileURL: URL, completion: @Sendable @escaping () -> Void) { + NSLog("Starting Imgur upload for file: %@", fileURL.path) @Default(.imgurClientId) var imgurClientId let uploadManager = UploadManager.shared @@ -20,7 +21,7 @@ import SwiftyJSON let fileFormName = determineFileFormName(for: fileURL) let fileName = "ishare.\(fileURL.pathExtension)" - let mimeType = mimeTypeForPathExtension(fileURL.pathExtension) + NSLog("Using file form name: %@, filename: %@", fileFormName, fileName) AF.upload(multipartFormData: { multipartFormData in multipartFormData.append(fileURL, withName: fileFormName, fileName: fileName, mimeType: mimeType) diff --git a/ishare/Http/UploadManager.swift b/ishare/Http/UploadManager.swift index a5c2bb8..7acaaac 100644 --- a/ishare/Http/UploadManager.swift +++ b/ishare/Http/UploadManager.swift @@ -34,6 +34,7 @@ final class UploadManager: @unchecked Sendable { } func updateProgress(fraction: Double) { + NSLog("Upload progress: %.2f%%", fraction * 100) Task { @MainActor in self.progress.completedUnitCount = Int64(fraction * 100) self.hostingView?.rootView = CircularProgressView(progress: fraction) @@ -41,6 +42,7 @@ final class UploadManager: @unchecked Sendable { } func uploadCompleted() { + NSLog("Upload completed, removing status item") Task { @MainActor in if let item = self.statusItem { NSStatusBar.system.removeStatusItem(item) diff --git a/ishare/Util/AppState.swift b/ishare/Util/AppState.swift index 10e597b..d16fed7 100644 --- a/ishare/Util/AppState.swift +++ b/ishare/Util/AppState.swift @@ -21,16 +21,21 @@ final class AppState: ObservableObject { } func setupKeyboardShortcuts() { + NSLog("Setting up keyboard shortcuts") + KeyboardShortcuts.onKeyUp(for: .toggleMainMenu) { [weak self] in + NSLog("Toggle main menu shortcut triggered") self?.showMainMenu = true } KeyboardShortcuts.onKeyUp(for: .openHistoryWindow) { [weak self] in + NSLog("Open history window shortcut triggered") guard let self = self else { return } openHistoryWindow(uploadHistory: self.uploadHistory) } KeyboardShortcuts.onKeyUp(for: .captureRegion) { + NSLog("Capture region shortcut triggered") Task { @MainActor in await captureScreen(type: .REGION) } diff --git a/ishare/Util/Constants.swift b/ishare/Util/Constants.swift index e259c05..86ce937 100644 --- a/ishare/Util/Constants.swift +++ b/ishare/Util/Constants.swift @@ -150,23 +150,28 @@ func selectFolder(completion: @escaping (URL?) -> Void) { } func importIscu(_ url: URL) { + NSLog("Starting ISCU import process for file: %@", url.path) if let keyWindow = NSApplication.shared.keyWindow { let alert = NSAlert() alert.messageText = "Import ISCU" alert.informativeText = "Do you want to import this custom uploader?" alert.addButton(withTitle: "Import") alert.addButton(withTitle: "Cancel") + NSLog("Showing import confirmation dialog") alert.beginSheetModal(for: keyWindow) { response in if response == .alertFirstButtonReturn { + NSLog("User confirmed import") alert.window.orderOut(nil) importFile(url) { success, error in if success { + NSLog("ISCU import successful") let successAlert = NSAlert() successAlert.messageText = "Import Successful" successAlert.informativeText = "The custom uploader has been imported successfully." successAlert.addButton(withTitle: "OK") successAlert.runModal() } else if let error { + NSLog("ISCU import failed: %@", error.localizedDescription) let errorAlert = NSAlert() errorAlert.messageText = "Import Error" errorAlert.informativeText = error.localizedDescription @@ -174,40 +179,10 @@ func importIscu(_ url: URL) { errorAlert.runModal() } } + } else { + NSLog("User cancelled import") } } - } else { - let window = NSWindow(contentViewController: NSHostingController(rootView: EmptyView())) - window.makeKeyAndOrderFront(nil) - window.center() - - let alert = NSAlert() - alert.messageText = "Import ISCU" - alert.informativeText = "Do you want to import this custom uploader?" - alert.addButton(withTitle: "Import") - alert.addButton(withTitle: "Cancel") - alert.beginSheetModal(for: window) { response in - if response == .alertFirstButtonReturn { - alert.window.orderOut(nil) - importFile(url) { success, error in - if success { - let successAlert = NSAlert() - successAlert.messageText = "Import Successful" - successAlert.informativeText = "The custom uploader has been imported successfully." - successAlert.addButton(withTitle: "OK") - successAlert.runModal() - } else if let error { - let errorAlert = NSAlert() - errorAlert.messageText = "Import Error" - errorAlert.informativeText = error.localizedDescription - errorAlert.addButton(withTitle: "OK") - errorAlert.runModal() - } - } - } - - window.orderOut(nil) - } } } @@ -311,21 +286,26 @@ struct SharingPreferences: Codable, Defaults.Serializable { } func shareBasedOnPreferences(_ fileURL: URL) { + NSLog("Processing share preferences for file: %@", fileURL.path) let preferences = Defaults[.builtInShare] if preferences.airdrop { + NSLog("Sharing via AirDrop") NSSharingService(named: .sendViaAirDrop)?.perform(withItems: [fileURL]) } if preferences.photos { + NSLog("Adding to Photos") NSSharingService(named: .addToIPhoto)?.perform(withItems: [fileURL]) } if preferences.messages { + NSLog("Sharing via Messages") NSSharingService(named: .composeMessage)?.perform(withItems: [fileURL]) } if preferences.mail { + NSLog("Sharing via Mail") NSSharingService(named: .composeEmail)?.perform(withItems: [fileURL]) } } @@ -344,9 +324,11 @@ struct HistoryItem: Codable, Hashable, Defaults.Serializable { } func addToUploadHistory(_ item: HistoryItem) { + NSLog("Adding item to upload history: %@", item.fileUrl ?? "nil") var history = Defaults[.uploadHistory] history.insert(item, at: 0) if history.count > 50 { + NSLog("Upload history exceeded 50 items, removing oldest entry") history.removeLast() } Defaults[.uploadHistory] = history diff --git a/sharemenuext/ShareViewController.swift b/sharemenuext/ShareViewController.swift index 83d7fbc..3621767 100644 --- a/sharemenuext/ShareViewController.swift +++ b/sharemenuext/ShareViewController.swift @@ -22,6 +22,8 @@ class ShareViewController: SLComposeServiceViewController { } func sendFileToApp() { + NSLog("Share extension activated") + let supportedTypes: [UTType] = [ .quickTimeMovie, .mpeg4Movie, @@ -32,17 +34,20 @@ class ShareViewController: SLComposeServiceViewController { .gif, .webP ] - + guard let extensionContext = extensionContext, let item = (extensionContext.inputItems as? [NSExtensionItem])?.first, let provider = item.attachments?.first(where: { provider in supportedTypes.contains(where: { provider.hasItemConformingToTypeIdentifier($0.identifier) }) }) else { + NSLog("Error: No valid attachment found in share extension") extensionContext?.completeRequest(returningItems: nil) return } + NSLog("Processing shared item of type: %@", typeIdentifier) + let typeIdentifier = supportedTypes.first { provider.hasItemConformingToTypeIdentifier($0.identifier) }?.identifier ?? UTType.data.identifier let localTypeIdentifier = typeIdentifier From 351bc7b72f5b395a2266e3610b89b44f51200146 Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:30:51 +0100 Subject: [PATCH 4/7] feat: close #93 --- ishare/Capture/ImageCapture.swift | 60 +++++++++++++++++++++++++- ishare/Capture/VideoCapture.swift | 11 ++++- ishare/Http/Imgur.swift | 1 + ishare/Views/MainMenuView.swift | 2 +- sharemenuext/ShareViewController.swift | 4 +- 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/ishare/Capture/ImageCapture.swift b/ishare/Capture/ImageCapture.swift index 60b74d2..9cf8799 100644 --- a/ishare/Capture/ImageCapture.swift +++ b/ishare/Capture/ImageCapture.swift @@ -38,10 +38,12 @@ func captureScreen(type: CaptureType, display: Int = 1) async { let uploadType = Defaults[.uploadType] let saveToDisk = Defaults[.saveToDisk] + let suffix = await getCaptureNameSuffix(type: type, display: display) + let timestamp = Int(Date().timeIntervalSince1970) - let uniqueFilename = "\(fileName)-\(timestamp)" + let uniqueFilename = "\(fileName)-\(timestamp)\(suffix).\(fileType)" - var path = "\(capturePath)\(uniqueFilename).\(fileType)" + var path = "\(capturePath)\(uniqueFilename)" path = NSString(string: path).expandingTildeInPath let task = Process() @@ -107,3 +109,57 @@ func captureScreen(type: CaptureType, display: Int = 1) async { } shareBasedOnPreferences(fileURL) } + +@MainActor +private func getCaptureNameSuffix(type: CaptureType, display: Int) async -> String { + switch type { + case .WINDOW: + if let frontmostApp = NSWorkspace.shared.frontmostApplication { + let appName = frontmostApp.localizedName ?? "window" + return "-\(appName.lowercased())" + } + return "-window" + + case .SCREEN: + if let screen = NSScreen.screens[safe: display - 1] { + if let displayName = screen.localizedName { + return "-\(displayName.lowercased())" + } + return "-display-\(display)" + } + return "-screen" + + case .REGION: + if let frontmostApp = NSWorkspace.shared.frontmostApplication { + let appName = frontmostApp.localizedName ?? "region" + return "-\(appName.lowercased())" + } + return "-region" + } +} + +// Helper extension for safe array access +extension Collection { + subscript(safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} + +// Helper extension for NSScreen to get display name +extension NSScreen { + var localizedName: String? { + // Get the display's bounds to help identify it + let bounds = frame + let width = Int(bounds.width) + let height = Int(bounds.height) + + if let displayID = deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID { + // Check if this is the main display + if (CGDisplayIsMain(displayID) != 0) { + return "main-\(width)x\(height)" + } + return "display-\(width)x\(height)" + } + return nil + } +} diff --git a/ishare/Capture/VideoCapture.swift b/ishare/Capture/VideoCapture.swift index 0956955..9d2c895 100644 --- a/ishare/Capture/VideoCapture.swift +++ b/ishare/Capture/VideoCapture.swift @@ -22,10 +22,17 @@ func recordScreen(gif: Bool? = false) { @Default(.recordingFileName) var fileName @Default(.recordMP4) var recordMP4 + // Get the suffix based on frontmost application + let suffix = if let frontmostApp = NSWorkspace.shared.frontmostApplication { + "-\(frontmostApp.localizedName?.lowercased() ?? "screen")" + } else { + "-screen" + } + let timestamp = Int(Date().timeIntervalSince1970) - let uniqueFilename = "\(fileName)-\(timestamp)" + let uniqueFilename = "\(fileName)-\(timestamp)\(suffix)" let path = NSString(string: "\(recordingPath)\(uniqueFilename).\(recordMP4 ? "mp4" : "mov")").expandingTildeInPath - NSLog("Recording to path: %@", path) + NSLog("Recording to path: %@ with suffix: %@", path, suffix) let fileURL = URL(fileURLWithPath: path) let screenRecorder = AppDelegate.shared.screenRecorder diff --git a/ishare/Http/Imgur.swift b/ishare/Http/Imgur.swift index 8781da4..ae2be41 100644 --- a/ishare/Http/Imgur.swift +++ b/ishare/Http/Imgur.swift @@ -21,6 +21,7 @@ import SwiftyJSON let fileFormName = determineFileFormName(for: fileURL) let fileName = "ishare.\(fileURL.pathExtension)" + let mimeType = mimeTypeForPathExtension(fileURL.pathExtension) NSLog("Using file form name: %@, filename: %@", fileFormName, fileName) AF.upload(multipartFormData: { multipartFormData in diff --git a/ishare/Views/MainMenuView.swift b/ishare/Views/MainMenuView.swift index ec981e3..bbf1b6b 100644 --- a/ishare/Views/MainMenuView.swift +++ b/ishare/Views/MainMenuView.swift @@ -101,7 +101,7 @@ struct MainMenuView: View { } } label: { Image(systemName: "macwindow") - Text("Capture \(screenName)") + Text("Capture \(screenName ?? "Screen")") }.globalKeyboardShortcut(index == 0 ? .captureScreen : .noKeybind) } } label: { diff --git a/sharemenuext/ShareViewController.swift b/sharemenuext/ShareViewController.swift index 3621767..b52d87c 100644 --- a/sharemenuext/ShareViewController.swift +++ b/sharemenuext/ShareViewController.swift @@ -46,11 +46,11 @@ class ShareViewController: SLComposeServiceViewController { return } - NSLog("Processing shared item of type: %@", typeIdentifier) - let typeIdentifier = supportedTypes.first { provider.hasItemConformingToTypeIdentifier($0.identifier) }?.identifier ?? UTType.data.identifier let localTypeIdentifier = typeIdentifier + NSLog("Processing shared item of type: %@", typeIdentifier) + provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { [weak self] item, _ in guard let item = item else { return } From d72c3138ef17a8ee9abe549e24b98c7f1759e5db Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:53:25 +0100 Subject: [PATCH 5/7] fix: close #95 --- ishare/Util/ToastPopover.swift | 52 ++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/ishare/Util/ToastPopover.swift b/ishare/Util/ToastPopover.swift index 8e77f83..efa6682 100644 --- a/ishare/Util/ToastPopover.swift +++ b/ishare/Util/ToastPopover.swift @@ -9,31 +9,33 @@ struct ToastPopoverView: View { @State private var isDragging = false var body: some View { - Image(nsImage: thumbnailImage) - .resizable() - .aspectRatio(contentMode: .fit) - .background(Color(NSColor.windowBackgroundColor).opacity(0.9)) - .foregroundColor(Color(NSColor.labelColor)) - .cornerRadius(10) - .padding(.horizontal, 20) - .padding(.vertical, 10) - .animation(Animation.easeInOut(duration: 1.0), value: thumbnailImage) - .opacity(isDragging ? 0 : 1) - .onTapGesture { - if saveToDisk { - NSWorkspace.shared.selectFile(fileURL.path, inFileViewerRootedAtPath: "") + GeometryReader { geometry in + Image(nsImage: thumbnailImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: geometry.size.width - 40, maxHeight: geometry.size.height - 20) + .frame(width: geometry.size.width, height: geometry.size.height) + .background(Color(NSColor.windowBackgroundColor).opacity(0.9)) + .foregroundColor(Color(NSColor.labelColor)) + .cornerRadius(10) + .animation(Animation.easeInOut(duration: 1.0), value: thumbnailImage) + .opacity(isDragging ? 0 : 1) + .onTapGesture { + if saveToDisk { + NSWorkspace.shared.selectFile(fileURL.path, inFileViewerRootedAtPath: "") + } } - } - .onDrag { - isDragging = true - let itemProvider = NSItemProvider(object: fileURL as NSURL) - itemProvider.suggestedName = fileURL.lastPathComponent - return itemProvider - } - .onDrop(of: [UTType.url], isTargeted: nil) { _ -> Bool in - isDragging = false - return true - } + .onDrag { + isDragging = true + let itemProvider = NSItemProvider(object: fileURL as NSURL) + itemProvider.suggestedName = fileURL.lastPathComponent + return itemProvider + } + .onDrop(of: [UTType.url], isTargeted: nil) { _ -> Bool in + isDragging = false + return true + } + } } } @@ -75,7 +77,7 @@ private func showThumbnailAndToast(fileURL: URL, thumbnailImage: NSImage, comple let toastTimeout = Defaults[.toastTimeout] let localCompletion = completion let toastWindow = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 300, height: 100), + contentRect: NSRect(x: 0, y: 0, width: 340, height: 240), styleMask: [.borderless], backing: .buffered, defer: false From bc2bbe8e335ff31f716759db840e6cbd34310789 Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Fri, 29 Nov 2024 02:01:07 +0100 Subject: [PATCH 6/7] feat: resolve #90 --- ishare/Util/AppState.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ishare/Util/AppState.swift b/ishare/Util/AppState.swift index d16fed7..57f7c97 100644 --- a/ishare/Util/AppState.swift +++ b/ishare/Util/AppState.swift @@ -54,11 +54,25 @@ final class AppState: ObservableObject { } KeyboardShortcuts.onKeyUp(for: .recordScreen) { - recordScreen() + if let screenRecorder = AppDelegate.shared.screenRecorder, + screenRecorder.isRunning { + let pickerManager = ContentSharingPickerManager.shared + pickerManager.deactivatePicker() + AppDelegate.shared.stopRecording() + } else { + recordScreen() + } } KeyboardShortcuts.onKeyUp(for: .recordGif) { - recordScreen(gif: true) + if let screenRecorder = AppDelegate.shared.screenRecorder, + screenRecorder.isRunning { + let pickerManager = ContentSharingPickerManager.shared + pickerManager.deactivatePicker() + AppDelegate.shared.stopRecording() + } else { + recordScreen(gif: true) + } } } } From 21bc736b8baf84a51871825d00fd608467441bf4 Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Fri, 29 Nov 2024 02:34:37 +0100 Subject: [PATCH 7/7] feat: resolve #91 --- ishare/Localizable.xcstrings | 6 + ishare/Util/AppState.swift | 66 ++++++++--- ishare/Util/Constants.swift | 26 +++++ ishare/Views/SettingsMenuView.swift | 164 ++++++++++++++++------------ 4 files changed, 179 insertions(+), 83 deletions(-) diff --git a/ishare/Localizable.xcstrings b/ishare/Localizable.xcstrings index 3553aa5..25b123d 100644 --- a/ishare/Localizable.xcstrings +++ b/ishare/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + + }, "*required" : { }, @@ -84,6 +87,9 @@ }, "File prefix:" : { + }, + "Force Upload Modifier:" : { + }, "Format:" : { diff --git a/ishare/Util/AppState.swift b/ishare/Util/AppState.swift index 57f7c97..4affab6 100644 --- a/ishare/Util/AppState.swift +++ b/ishare/Util/AppState.swift @@ -21,19 +21,7 @@ final class AppState: ObservableObject { } func setupKeyboardShortcuts() { - NSLog("Setting up keyboard shortcuts") - - KeyboardShortcuts.onKeyUp(for: .toggleMainMenu) { [weak self] in - NSLog("Toggle main menu shortcut triggered") - self?.showMainMenu = true - } - - KeyboardShortcuts.onKeyUp(for: .openHistoryWindow) { [weak self] in - NSLog("Open history window shortcut triggered") - guard let self = self else { return } - openHistoryWindow(uploadHistory: self.uploadHistory) - } - + // Regular shortcuts KeyboardShortcuts.onKeyUp(for: .captureRegion) { NSLog("Capture region shortcut triggered") Task { @MainActor in @@ -74,5 +62,57 @@ final class AppState: ObservableObject { recordScreen(gif: true) } } + + // Force upload shortcuts + KeyboardShortcuts.onKeyUp(for: .captureRegionForceUpload) { + NSLog("Force upload capture region shortcut triggered") + Task { @MainActor in + Defaults[.uploadMedia] = true + await captureScreen(type: .REGION) + Defaults[.uploadMedia] = false + } + } + + KeyboardShortcuts.onKeyUp(for: .captureWindowForceUpload) { + Task { @MainActor in + Defaults[.uploadMedia] = true + await captureScreen(type: .WINDOW) + Defaults[.uploadMedia] = false + } + } + + KeyboardShortcuts.onKeyUp(for: .captureScreenForceUpload) { + Task { @MainActor in + Defaults[.uploadMedia] = true + await captureScreen(type: .SCREEN) + Defaults[.uploadMedia] = false + } + } + + KeyboardShortcuts.onKeyUp(for: .recordScreenForceUpload) { + if let screenRecorder = AppDelegate.shared.screenRecorder, + screenRecorder.isRunning { + let pickerManager = ContentSharingPickerManager.shared + pickerManager.deactivatePicker() + AppDelegate.shared.stopRecording() + } else { + Defaults[.uploadMedia] = true + recordScreen() + Defaults[.uploadMedia] = false + } + } + + KeyboardShortcuts.onKeyUp(for: .recordGifForceUpload) { + if let screenRecorder = AppDelegate.shared.screenRecorder, + screenRecorder.isRunning { + let pickerManager = ContentSharingPickerManager.shared + pickerManager.deactivatePicker() + AppDelegate.shared.stopRecording() + } else { + Defaults[.uploadMedia] = true + recordScreen(gif: true) + Defaults[.uploadMedia] = false + } + } } } diff --git a/ishare/Util/Constants.swift b/ishare/Util/Constants.swift index 86ce937..4c47340 100644 --- a/ishare/Util/Constants.swift +++ b/ishare/Util/Constants.swift @@ -25,6 +25,13 @@ extension KeyboardShortcuts.Name { static let recordScreen = Self("recordScreen", default: .init(.z, modifiers: [.control, .option])) static let recordGif = Self("recordGif", default: .init(.g, modifiers: [.control, .option])) static let openHistoryWindow = Self("openHistoryWindow", default: .init(.k, modifiers: [.command, .option])) + + // Force upload variants + static let captureRegionForceUpload = Self("captureRegionForceUpload", default: .init(.p, modifiers: [.shift, .option, .command])) + static let captureWindowForceUpload = Self("captureWindowForceUpload", default: .init(.p, modifiers: [.shift, .control, .option])) + static let captureScreenForceUpload = Self("captureScreenForceUpload", default: .init(.x, modifiers: [.shift, .option, .command])) + static let recordScreenForceUpload = Self("recordScreenForceUpload", default: .init(.z, modifiers: [.shift, .control, .option])) + static let recordGifForceUpload = Self("recordGifForceUpload", default: .init(.g, modifiers: [.shift, .control, .option])) } extension Defaults.Keys { @@ -53,6 +60,7 @@ extension Defaults.Keys { static let uploadHistory = Key<[HistoryItem]>("uploadHistory", default: [], iCloud: true) static let ignoredBundleIdentifiers = Key<[String]>("ignoredApps", default: [], iCloud: true) static let aussieMode = Key("aussieMode", default: false, iCloud: true) + static let forceUploadModifier = Key("forceUploadModifier", default: .shift) } extension KeyboardShortcuts.Shortcut { @@ -375,3 +383,21 @@ struct ExcludedAppsView: View { .padding() } } + +enum ForceUploadModifier: String, CaseIterable, Identifiable, Defaults.Serializable { + case shift = "⇧" + case control = "⌃" + case option = "⌥" + case command = "⌘" + + var id: Self { self } + + var modifierFlag: NSEvent.ModifierFlags { + switch self { + case .shift: return .shift + case .control: return .control + case .option: return .option + case .command: return .command + } + } +} diff --git a/ishare/Views/SettingsMenuView.swift b/ishare/Views/SettingsMenuView.swift index 3ba2e5e..f8089fd 100644 --- a/ishare/Views/SettingsMenuView.swift +++ b/ishare/Views/SettingsMenuView.swift @@ -56,11 +56,11 @@ struct SettingsMenuView: View { .padding() .frame(maxWidth: .infinity, alignment: .center) } - .frame(minWidth: 150, idealWidth: 200, maxWidth: 300, maxHeight: .infinity) + .frame(minWidth: 200, idealWidth: 200, maxWidth: 300, maxHeight: .infinity) GeneralSettingsView() } - .frame(minWidth: 600, maxWidth: 600, minHeight: 300, maxHeight: 300) + .frame(minWidth: 600, maxWidth: 600, minHeight: 450, maxHeight: 450) .navigationTitle("Settings") } } @@ -86,20 +86,15 @@ struct GeneralSettingsView: View { } var body: some View { - VStack(alignment: .leading) { - Spacer() - - HStack { + VStack(alignment: .leading, spacing: 30) { + HStack(spacing: 40) { VStack(alignment: .leading) { LaunchAtLogin.Toggle() Toggle("Land down under", isOn: $aussieMode) - }.padding(25) - - Spacer() + } - VStack { + VStack(alignment: .leading) { Text("Menu Bar Icon") - HStack { ForEach(MenuBarIcon.allCases, id: \.self) { choice in Button(action: { @@ -131,7 +126,8 @@ struct GeneralSettingsView: View { } } } - }.padding(25) + } + .padding(.top, 30) Spacer() @@ -140,39 +136,71 @@ struct GeneralSettingsView: View { Slider(value: $toastTimeout, in: 1 ... 10, step: 1) .frame(maxWidth: .infinity) } - .padding(.bottom) + .padding(.bottom, 30) } - .padding().rotationEffect(aussieMode ? .degrees(180) : .zero) + .padding(30) + .rotationEffect(aussieMode ? .degrees(180) : .zero) } } struct KeybindSettingsView: View { + @Default(.forceUploadModifier) var forceUploadModifier @Default(.aussieMode) var aussieMode var body: some View { - Spacer() - - Form { - KeyboardShortcuts.Recorder("Open Main Menu:", name: .toggleMainMenu) - KeyboardShortcuts.Recorder("Open History Window:", name: .openHistoryWindow) - KeyboardShortcuts.Recorder("Capture Region:", name: .captureRegion) - KeyboardShortcuts.Recorder("Capture Window:", name: .captureWindow) - KeyboardShortcuts.Recorder("Capture Screen:", name: .captureScreen) - KeyboardShortcuts.Recorder("Record Screen:", name: .recordScreen) - KeyboardShortcuts.Recorder("Record GIF:", name: .recordGif) - } - - Spacer() - - Button(action: { - KeyboardShortcuts.reset([.toggleMainMenu, .openHistoryWindow, .captureRegion, .captureWindow, .captureScreen, .recordScreen, .recordGif]) - BezelNotification.show(messageText: "Reset keybinds", icon: ToastIcon) - }) { - Text("Reset All Keybinds") - .foregroundColor(.red) - .frame(maxWidth: .infinity) + VStack(spacing: 20) { + Form { + Section { + VStack(spacing: 10) { + KeyboardShortcuts.Recorder("Open Main Menu:", name: .toggleMainMenu) + KeyboardShortcuts.Recorder("Open History Window:", name: .openHistoryWindow) + KeyboardShortcuts.Recorder("Capture Region:", name: .captureRegion) + KeyboardShortcuts.Recorder("Capture Window:", name: .captureWindow) + KeyboardShortcuts.Recorder("Capture Screen:", name: .captureScreen) + KeyboardShortcuts.Recorder("Record Screen:", name: .recordScreen) + KeyboardShortcuts.Recorder("Record GIF:", name: .recordGif) + + Divider() + .padding(.vertical, 5) + + HStack { + Text("Force Upload Modifier:") + Picker("", selection: $forceUploadModifier) { + ForEach(ForceUploadModifier.allCases) { modifier in + Text(modifier.rawValue) + .tag(modifier) + } + } + .frame(width: 100) + } + } + .padding(.vertical, 5) + } header: { + Text("Keybinds") + .font(.headline) + .padding(.bottom, 5) + } + } + .formStyle(.grouped) + + Button(action: { + KeyboardShortcuts.reset([ + .toggleMainMenu, .openHistoryWindow, + .captureRegion, .captureWindow, .captureScreen, + .recordScreen, .recordGif, + .captureRegionForceUpload, .captureWindowForceUpload, .captureScreenForceUpload, + .recordScreenForceUpload, .recordGifForceUpload + ]) + BezelNotification.show(messageText: "Reset keybinds", icon: ToastIcon) + }) { + Text("Reset All Keybinds") + .foregroundColor(.red) + .frame(maxWidth: .infinity) + } + .padding(.horizontal) + .rotationEffect(aussieMode ? .degrees(180) : .zero) } - .padding().rotationEffect(aussieMode ? .degrees(180) : .zero) + .padding() } } @@ -183,8 +211,8 @@ struct CaptureSettingsView: View { @Default(.aussieMode) var aussieMode var body: some View { - VStack(alignment: .leading) { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 30) { + VStack(alignment: .leading, spacing: 15) { Text("Image path:").font(.headline) HStack { TextField(text: $capturePath) {} @@ -198,9 +226,9 @@ struct CaptureSettingsView: View { Image(systemName: "folder.fill") }.help("Pick a folder") } - }.padding() + } - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 15) { Text("File prefix:").font(.headline) HStack { TextField(String(), text: $fileName) @@ -210,18 +238,20 @@ struct CaptureSettingsView: View { Image(systemName: "arrow.clockwise") }.help("Set to default") } - }.padding() + } - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 15) { Text("Format:").font(.headline) Picker("Format:", selection: $fileType) { ForEach(FileType.allCases, id: \.self) { Text($0.rawValue.uppercased()) } - }.labelsHidden() - }.padding() - - }.padding().rotationEffect(aussieMode ? .degrees(180) : .zero) + } + .labelsHidden() + } + } + .padding(30) + .rotationEffect(aussieMode ? .degrees(180) : .zero) } } @@ -236,20 +266,18 @@ struct RecordingSettingsView: View { @State private var isExcludedAppSheetPresented = false var body: some View { - VStack { - Spacer() - - HStack(spacing: 25) { - VStack(alignment: .leading) { - Toggle("Record .mp4 instead of .mov", isOn: $recordMP4) - Toggle("Use HEVC", isOn: $useHEVC) - Toggle("Use HDR", isOn: $useHDR) + VStack(alignment: .leading, spacing: 30) { + VStack(alignment: .leading, spacing: 15) { + HStack(spacing: 30) { + VStack(alignment: .leading) { + Toggle("Record .mp4 instead of .mov", isOn: $recordMP4) + Toggle("Use HEVC", isOn: $useHEVC) + Toggle("Use HDR", isOn: $useHDR) + } } - }.padding(.horizontal) - - Spacer() + } - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 15) { Text("Video path:").font(.headline) HStack { TextField(text: $recordingPath) {} @@ -263,11 +291,9 @@ struct RecordingSettingsView: View { Image(systemName: "folder.fill") }.help("Pick a folder") } - }.padding(.horizontal) - - Spacer() + } - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 15) { Text("File prefix:").font(.headline) HStack { TextField(String(), text: $fileName) @@ -277,17 +303,15 @@ struct RecordingSettingsView: View { Image(systemName: "arrow.clockwise") }.help("Set to default") } - }.padding(.horizontal) - - Spacer() + } Button("Excluded applications") { isExcludedAppSheetPresented.toggle() - }.padding() - .sheet(isPresented: $isExcludedAppSheetPresented) { - ExcludedAppsView().frame(maxHeight: 500) - } - }.rotationEffect(aussieMode ? .degrees(180) : .zero) + } + .padding(.top) + } + .padding(30) + .rotationEffect(aussieMode ? .degrees(180) : .zero) } }