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)
}
}