From 406be77eeb9ec3b0b227be034a461a371b5f2381 Mon Sep 17 00:00:00 2001 From: "sameerasw.com" Date: Thu, 3 Sep 2026 19:03:02 +0530 Subject: [PATCH 01/14] New translations en.json (Italian) [ci skip] --- airsync-mac/Localization/it.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/airsync-mac/Localization/it.json b/airsync-mac/Localization/it.json index cd85fab8..027ed677 100644 --- a/airsync-mac/Localization/it.json +++ b/airsync-mac/Localization/it.json @@ -201,11 +201,11 @@ "connection.wiredAdb": "ADB cablato", "connection.selectDevice": "Seleziona dispositivo", "connection.wireless": "Wireless", - "settings.connectionMode": "Connection Mode", - "connectionMode.network": "Network", - "connectionMode.dynamic": "Dynamic", - "connectionMode.nearby": "Nearby", - "settings.connectionMode.network.info": "Uses local area network only. Bluetooth LE is disabled.", - "settings.connectionMode.dynamic.info": "Uses Wi-Fi and Bluetooth LE with automatic fallback and switching.", - "settings.connectionMode.nearby.info": "Uses Bluetooth LE only. Local network discovery is disabled." + "settings.connectionMode": "Modalità di connessione", + "connectionMode.network": "Rete", + "connectionMode.dynamic": "Dinamico", + "connectionMode.nearby": "Nelle vicinanze", + "settings.connectionMode.network.info": "Utilizza solo la rete locale. Bluetooth LE è disabilitato.", + "settings.connectionMode.dynamic.info": "Utilizza Wi-Fi e Bluetooth LE con fallback e cambio automatico.", + "settings.connectionMode.nearby.info": "Utilizza solo Bluetooth LE. La ricerca della rete locale è disabilitata." } From f97360254f15c7d34eda1ab3a4c2d2b0f4327ec8 Mon Sep 17 00:00:00 2001 From: hkay-dev <14947763+hkay-dev@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:28:40 -0700 Subject: [PATCH 02/14] Add macOS Share menu handoff to Quick Share --- AirSync.xcodeproj/project.pbxproj | 171 ++++++++++++++++ AirSyncShareExtension.entitlements | 14 ++ AirSyncShareExtension/Info.plist | 33 ++++ AirSyncShareExtension/ShareInputLoader.swift | 183 ++++++++++++++++++ .../ShareViewController.swift | 106 ++++++++++ README.md | 7 + airsync-mac/Core/AppDelegate.swift | 2 +- 7 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 AirSyncShareExtension.entitlements create mode 100644 AirSyncShareExtension/Info.plist create mode 100644 AirSyncShareExtension/ShareInputLoader.swift create mode 100644 AirSyncShareExtension/ShareViewController.swift diff --git a/AirSync.xcodeproj/project.pbxproj b/AirSync.xcodeproj/project.pbxproj index 92c1d201..d429f8f1 100644 --- a/AirSync.xcodeproj/project.pbxproj +++ b/AirSync.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A17000000000000000000001 /* AirSyncShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A17000000000000000000004 /* AirSyncShareExtension.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B94971602F62509A00F59BC9 /* BigInt in Frameworks */ = {isa = PBXBuildFile; productRef = B949715F2F62509A00F59BC9 /* BigInt */; }; B94971632F6250DE00F59BC9 /* ASN1 in Frameworks */ = {isa = PBXBuildFile; productRef = B94971622F6250DE00F59BC9 /* ASN1 */; }; B94971662F62514500F59BC9 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = B94971652F62514500F59BC9 /* SwiftECC */; }; @@ -26,6 +27,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + A17000000000000000000002 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B9673B4F2E35A2A1006D284A /* Project object */; + proxyType = 1; + remoteGlobalIDString = A17000000000000000000008; + remoteInfo = AirSyncShareExtension; + }; B99D64073059A795003D951A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B9673B4F2E35A2A1006D284A /* Project object */; @@ -43,6 +51,7 @@ dstSubfolderSpec = 13; files = ( B99D64093059A795003D951A /* AirSyncWidgetExtension.appex in Embed Foundation Extensions */, + A17000000000000000000001 /* AirSyncShareExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -50,6 +59,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + A17000000000000000000004 /* AirSyncShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AirSyncShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + A17000000000000000000010 /* AirSyncShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AirSyncShareExtension.entitlements; sourceTree = ""; }; B9673B572E35A2A1006D284A /* AirSync.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AirSync.app; sourceTree = BUILT_PRODUCTS_DIR; }; B99B04C52F641E1400FF3E2D /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; B99D63F83059A794003D951A /* AirSyncWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AirSyncWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -61,6 +72,13 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + A17000000000000000000005 /* Exceptions for "AirSyncShareExtension" folder in "AirSyncShareExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = A17000000000000000000008 /* AirSyncShareExtension */; + }; B933ED362E3BEC21007393CB /* Exceptions for "airsync-mac" folder in "AirSync" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -90,6 +108,14 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + A17000000000000000000006 /* AirSyncShareExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + A17000000000000000000005 /* Exceptions for "AirSyncShareExtension" folder in "AirSyncShareExtension" target */, + ); + path = AirSyncShareExtension; + sourceTree = ""; + }; B9673B592E35A2A1006D284A /* airsync-mac */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -109,6 +135,13 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + A17000000000000000000007 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B9673B542E35A2A1006D284A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -142,10 +175,12 @@ B9673B4E2E35A2A1006D284A = { isa = PBXGroup; children = ( + A17000000000000000000010 /* AirSyncShareExtension.entitlements */, B99D64153059A9FC003D951A /* AirSyncWidgetExtension.entitlements */, B99D64143059A9B3003D951A /* AirSync.entitlements */, B9673B592E35A2A1006D284A /* airsync-mac */, B99D63FD3059A794003D951A /* AirSyncWidget */, + A17000000000000000000006 /* AirSyncShareExtension */, B9D263192F60D97900628704 /* Frameworks */, B9673B582E35A2A1006D284A /* Products */, B99B04C52F641E1400FF3E2D /* AppIcon.icon */, @@ -158,6 +193,7 @@ children = ( B9673B572E35A2A1006D284A /* AirSync.app */, B99D63F83059A794003D951A /* AirSyncWidgetExtension.appex */, + A17000000000000000000004 /* AirSyncShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -174,6 +210,28 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + A17000000000000000000008 /* AirSyncShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = A1700000000000000000000F /* Build configuration list for PBXNativeTarget "AirSyncShareExtension" */; + buildPhases = ( + A1700000000000000000000A /* Sources */, + A17000000000000000000007 /* Frameworks */, + A17000000000000000000009 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + A17000000000000000000006 /* AirSyncShareExtension */, + ); + name = AirSyncShareExtension; + packageProductDependencies = ( + ); + productName = AirSyncShareExtension; + productReference = A17000000000000000000004 /* AirSyncShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; B9673B562E35A2A1006D284A /* AirSync */ = { isa = PBXNativeTarget; buildConfigurationList = B9673B622E35A2A3006D284A /* Build configuration list for PBXNativeTarget "AirSync" */; @@ -187,6 +245,7 @@ ); dependencies = ( B99D64083059A795003D951A /* PBXTargetDependency */, + A1700000000000000000000B /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( B9673B592E35A2A1006D284A /* airsync-mac */, @@ -241,6 +300,9 @@ LastSwiftUpdateCheck = 2700; LastUpgradeCheck = 2620; TargetAttributes = { + A17000000000000000000008 = { + CreatedOnToolsVersion = 27.0; + }; B9673B562E35A2A1006D284A = { CreatedOnToolsVersion = 26.0; }; @@ -277,11 +339,19 @@ targets = ( B9673B562E35A2A1006D284A /* AirSync */, B99D63F73059A794003D951A /* AirSyncWidgetExtension */, + A17000000000000000000008 /* AirSyncShareExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + A17000000000000000000009 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B9673B552E35A2A1006D284A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -301,6 +371,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + A1700000000000000000000A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B9673B532E35A2A1006D284A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -318,6 +395,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + A1700000000000000000000B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A17000000000000000000008 /* AirSyncShareExtension */; + targetProxy = A17000000000000000000002 /* PBXContainerItemProxy */; + }; B99D64083059A795003D951A /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B99D63F73059A794003D951A /* AirSyncWidgetExtension */; @@ -326,6 +408,85 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + A1700000000000000000000C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 32; + DEVELOPMENT_TEAM = WCB4HTANA6; + ENABLE_APP_SANDBOX = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirSyncShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirSync; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 4.1.0; + PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + A1700000000000000000000D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 32; + DEVELOPMENT_TEAM = WCB4HTANA6; + ENABLE_APP_SANDBOX = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirSyncShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirSync; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 4.1.0; + PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + A1700000000000000000000E /* Self Compiled */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 32; + DEVELOPMENT_TEAM = WCB4HTANA6; + ENABLE_APP_SANDBOX = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirSyncShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirSync; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 4.1.0; + PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = "Self Compiled"; + }; 11F6093B2EA2A88900AB6BDC /* Self Compiled */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = B9673B592E35A2A1006D284A /* airsync-mac */; @@ -780,6 +941,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + A1700000000000000000000F /* Build configuration list for PBXNativeTarget "AirSyncShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1700000000000000000000C /* Debug */, + A1700000000000000000000D /* Release */, + A1700000000000000000000E /* Self Compiled */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; B9673B522E35A2A1006D284A /* Build configuration list for PBXProject "AirSync" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AirSyncShareExtension.entitlements b/AirSyncShareExtension.entitlements new file mode 100644 index 00000000..f835e023 --- /dev/null +++ b/AirSyncShareExtension.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.sameerasw.airsync-mac.widget + + com.apple.security.files.user-selected.read-only + + + diff --git a/AirSyncShareExtension/Info.plist b/AirSyncShareExtension/Info.plist new file mode 100644 index 00000000..856434a2 --- /dev/null +++ b/AirSyncShareExtension/Info.plist @@ -0,0 +1,33 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationDictionaryVersion + 2 + NSExtensionActivationSupportsAttachmentsWithMaxCount + 999 + NSExtensionActivationSupportsFileWithMaxCount + 999 + NSExtensionActivationSupportsImageWithMaxCount + 999 + NSExtensionActivationSupportsMovieWithMaxCount + 999 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 999 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareViewController + + + diff --git a/AirSyncShareExtension/ShareInputLoader.swift b/AirSyncShareExtension/ShareInputLoader.swift new file mode 100644 index 00000000..cb0491a5 --- /dev/null +++ b/AirSyncShareExtension/ShareInputLoader.swift @@ -0,0 +1,183 @@ +import Foundation +import UniformTypeIdentifiers + +struct ShareInputLoader { + enum InputError: LocalizedError { + case empty + case unsupported + case folder + + var errorDescription: String? { + switch self { + case .empty: "There is nothing to send." + case .unsupported: "AirSync cannot read one of the shared items." + case .folder: "AirSync cannot send folders yet." + } + } + } + + private let requestDirectory: URL + + init(groupContainer: URL) { + requestDirectory = groupContainer + .appendingPathComponent("Library/Caches/AirSyncShares", isDirectory: true) + .appendingPathComponent(UUID().uuidString, isDirectory: true) + } + + func prepare(_ items: [NSExtensionItem]) async throws -> [URL] { + removeOldRequests() + do { + var urls: [URL] = [] + for item in items { + let attachments = item.attachments ?? [] + if attachments.isEmpty { + if let text = item.attributedContentText?.string, !text.isEmpty { + urls.append(try stage(Data(text.utf8), name: "Shared Text.txt")) + } + } else { + for provider in attachments { + urls.append(try await prepare(provider)) + } + } + } + guard !urls.isEmpty else { throw InputError.empty } + return urls + } catch { + discard() + throw error + } + } + + func discard() { + try? FileManager.default.removeItem(at: requestDirectory) + } + + private func prepare(_ provider: NSItemProvider) async throws -> URL { + if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier), + let url = try? await loadURL(provider) { + if url.isFileURL { + try checkForFolder(url) + return url + } + return try stage(Data(url.absoluteString.utf8), name: "Shared Link.txt") + } + + let fileType = provider.registeredTypeIdentifiers.first { + guard let type = UTType($0) else { return false } + return type.conforms(to: .data) && !type.conforms(to: .text) && !type.conforms(to: .url) + } + if let fileType { + do { + return try await stageFile(provider, typeIdentifier: fileType) + } catch InputError.folder { + throw InputError.folder + } catch { + let data = try await loadData(provider, typeIdentifier: fileType) + return try stage(data, name: fileName(provider.suggestedName, typeIdentifier: fileType)) + } + } + + if let textType = provider.registeredTypeIdentifiers.first(where: { + UTType($0)?.conforms(to: .text) == true + }) { + let data = try await loadData(provider, typeIdentifier: textType) + return try stage(data, name: fileName(provider.suggestedName, typeIdentifier: textType, fallback: "Shared Text")) + } + throw InputError.unsupported + } + + private func loadURL(_ provider: NSItemProvider) async throws -> URL { + try await withCheckedThrowingContinuation { continuation in + _ = provider.loadObject(ofClass: NSURL.self) { url, error in + if let error { + continuation.resume(throwing: error) + } else if let url = url as? NSURL { + continuation.resume(returning: url as URL) + } else { + continuation.resume(throwing: InputError.unsupported) + } + } + } + } + + private func loadData(_ provider: NSItemProvider, typeIdentifier: String) async throws -> Data { + try await withCheckedThrowingContinuation { continuation in + _ = provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { data, error in + if let error { + continuation.resume(throwing: error) + } else if let data { + continuation.resume(returning: data) + } else { + continuation.resume(throwing: InputError.unsupported) + } + } + } + } + + private func stageFile(_ provider: NSItemProvider, typeIdentifier: String) async throws -> URL { + let suggestedName = provider.suggestedName + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + _ = provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { source, error in + if let error { + continuation.resume(throwing: error) + return + } + guard let source else { + continuation.resume(throwing: InputError.unsupported) + return + } + do { + try checkForFolder(source) + let name = fileName(suggestedName, typeIdentifier: typeIdentifier, fallback: source.lastPathComponent) + let destination = try destination(for: name) + try FileManager.default.copyItem(at: source, to: destination) + continuation.resume(returning: destination) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + private func stage(_ data: Data, name: String) throws -> URL { + let destination = try destination(for: name) + try data.write(to: destination, options: .atomic) + return destination + } + + private func destination(for name: String) throws -> URL { + let itemDirectory = requestDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: itemDirectory, withIntermediateDirectories: true) + return itemDirectory.appendingPathComponent(name) + } + + private func fileName(_ suggestedName: String?, typeIdentifier: String, fallback: String = "Shared Item") -> String { + let suggested = suggestedName ?? fallback + let name = (suggested as NSString).lastPathComponent + let safeName = name.isEmpty || name == "." || name == ".." ? fallback : name + guard (safeName as NSString).pathExtension.isEmpty, + let ext = UTType(typeIdentifier)?.preferredFilenameExtension else { return safeName } + return "\(safeName).\(ext)" + } + + private func checkForFolder(_ url: URL) throws { + if url.hasDirectoryPath || (try? url.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true { + throw InputError.folder + } + } + + private func removeOldRequests() { + let root = requestDirectory.deletingLastPathComponent() + guard let children = try? FileManager.default.contentsOfDirectory( + at: root, + includingPropertiesForKeys: [.contentModificationDateKey] + ) else { return } + let cutoff = Date().addingTimeInterval(-24 * 60 * 60) + for child in children where child != requestDirectory { + if let values = try? child.resourceValues(forKeys: [.contentModificationDateKey]), + let date = values.contentModificationDate, date < cutoff { + try? FileManager.default.removeItem(at: child) + } + } + } +} diff --git a/AirSyncShareExtension/ShareViewController.swift b/AirSyncShareExtension/ShareViewController.swift new file mode 100644 index 00000000..4f2085b1 --- /dev/null +++ b/AirSyncShareExtension/ShareViewController.swift @@ -0,0 +1,106 @@ +import AppKit + +final class ShareViewController: NSViewController { + private lazy var detail = NSTextField(wrappingLabelWithString: "AirSync will send this to your connected device or let you pick one.") + private lazy var sendButton = NSButton(title: "Send with AirSync", target: self, action: #selector(send)) + private lazy var cancelButton = NSButton(title: "Cancel", target: self, action: #selector(cancel)) + private var sendTask: Task? + + init() { + super.init(nibName: nil, bundle: nil) + preferredContentSize = NSSize(width: 360, height: 150) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError() + } + + override func loadView() { + let title = NSTextField(wrappingLabelWithString: "Send with AirSync") + title.font = .systemFont(ofSize: 17, weight: .semibold) + detail.textColor = .secondaryLabelColor + sendButton.keyEquivalent = "\r" + cancelButton.keyEquivalent = "\u{1b}" + + let buttons = NSStackView(views: [sendButton, cancelButton]) + buttons.orientation = .horizontal + buttons.spacing = 8 + + let stack = NSStackView(views: [title, detail, buttons]) + stack.orientation = .vertical + stack.alignment = .leading + stack.spacing = 12 + stack.translatesAutoresizingMaskIntoConstraints = false + + let content = NSView(frame: NSRect(origin: .zero, size: preferredContentSize)) + content.addSubview(stack) + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20), + stack.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20), + stack.centerYAnchor.constraint(equalTo: content.centerYAnchor), + ]) + view = content + } + + @objc private func send() { + guard sendTask == nil else { return } + sendButton.isEnabled = false + detail.stringValue = "Preparing this share for AirSync..." + sendTask = Task { @MainActor in + await handOff() + } + } + + private func handOff() async { + guard let items = extensionContext?.inputItems as? [NSExtensionItem], + let group = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.sameerasw.airsync-mac.widget" + ) else { + showError("AirSync could not access the shared items.") + return + } + + let loader = ShareInputLoader(groupContainer: group) + var openingStarted = false + do { + let urls = try await loader.prepare(items) + try Task.checkCancellation() + detail.stringValue = "Opening AirSync..." + let appURL = Bundle.main.bundleURL + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + let configuration = NSWorkspace.OpenConfiguration() + configuration.allowsRunningApplicationSubstitution = false + openingStarted = true + _ = try await NSWorkspace.shared.open( + urls, + withApplicationAt: appURL, + configuration: configuration + ) + try Task.checkCancellation() + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } catch is CancellationError { + if !openingStarted { loader.discard() } + } catch { + if !openingStarted { loader.discard() } + if !Task.isCancelled { showError(error.localizedDescription) } + } + } + + private func showError(_ message: String) { + detail.stringValue = message + sendButton.isEnabled = true + cancelButton.isEnabled = true + sendTask = nil + } + + @objc private func cancel() { + guard cancelButton.isEnabled else { return } + sendTask?.cancel() + extensionContext?.cancelRequest( + withError: NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError) + ) + } +} diff --git a/README.md b/README.md index 3e732434..7285264e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,13 @@ ## Building Locally In XCode, select the `AirSync Self Compiled` Scheme. Then at the menu bar, select `Product -> Archive`. Once this process is finished, the organizer window will pop up. Select `Distribute`, then `Custom`. Select `Copy App`, then select the folder for output. `AirSync.app` will be located in that folder. +## Share menu +The macOS Share extension hands files to AirSync's existing Quick Share transfer. It uses the connected device when one is available, or opens device selection otherwise. Images and videos supplied as temporary data are copied to the existing app group's cache first. Text and web links are sent as text files. Cached copies older than 24 hours are removed on a later share. + +The current file sender does not support folders. macOS and each sharing app control menu placement, so AirSync cannot force a position above AirDrop or appear for every item. + +For release signing, enable the existing `group.sameerasw.airsync-mac.widget` app group for the new Share extension identifier in the Apple Developer account. Sign the app and its extensions with the same team. + ## Contributors <3 diff --git a/airsync-mac/Core/AppDelegate.swift b/airsync-mac/Core/AppDelegate.swift index 5ddde499..c1143a37 100644 --- a/airsync-mac/Core/AppDelegate.swift +++ b/airsync-mac/Core/AppDelegate.swift @@ -54,7 +54,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { func application(_ application: NSApplication, open urls: [URL]) { if !urls.isEmpty { QuickShareManager.shared.transferURLs = urls - QuickShareManager.shared.startDiscovery(autoTargetName: nil) + QuickShareManager.shared.startDiscovery(autoTargetName: AppState.shared.device?.name) AppState.shared.showingQuickShareTransfer = true } } From c0f6c578961dd7fb745c4ee6a088efe2deccc50a Mon Sep 17 00:00:00 2001 From: "sameerasw.com" Date: Wed, 7 Oct 2026 07:20:01 +0530 Subject: [PATCH 03/14] New translations en.json (English) [ci skip] --- airsync-mac/Localization/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airsync-mac/Localization/en.json b/airsync-mac/Localization/en.json index c27f3e8a..a4edc632 100644 --- a/airsync-mac/Localization/en.json +++ b/airsync-mac/Localization/en.json @@ -1,9 +1,9 @@ { - "app.name": "AirSync", + "app.name": "Airsync", "menu.mirror": "Mirror", "menu.browseFiles": "Browse Files", "menu.about": "About AirSync", - "menu.checkUpdates": "Check for Updates…", + "menu.checkUpdates": "Check for Updates...", "menu.quit": "Quit", "button.connect": "Connect", "home.title": "AirSync", From bdd28a13ffabc91feb19d1deee7a9a5328443279 Mon Sep 17 00:00:00 2001 From: "sameerasw.com" Date: Wed, 7 Oct 2026 08:42:27 +0530 Subject: [PATCH 04/14] New translations en.json (English) [ci skip] --- airsync-mac/Localization/en.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/airsync-mac/Localization/en.json b/airsync-mac/Localization/en.json index a4edc632..7334f9d6 100644 --- a/airsync-mac/Localization/en.json +++ b/airsync-mac/Localization/en.json @@ -22,26 +22,26 @@ "settings.sync": "Sync", "settings.notifications": "Notifications & Alerts", "settings.notifications.apps": "Apps", - "settings.notifications.sync": "Notifications Sync", + "settings.notifications.sync": "Notification Sync", "settings.notifications.dismiss": "Sync notification dismissals", "settings.notifications.system": "System Notifications", "settings.notifications.sound.default": "Default", "settings.notifications.grant": "Grant Permission", "settings.notifications.ai.title": "Apple Intelligence", "settings.notifications.ai.disableAll": "Disable all AI features", - "settings.notifications.ai.showToolbarButton": "Summarize notifications button in toolbar", + "settings.notifications.ai.showToolbarButton": "Summarize notification button in toolbar", "settings.notifications.ai.includeSilent": "Include silent notifications", "settings.notifications.ai.menubarSummary": "Menubar pop-up summary", "settings.notifications.ai.autoMenubarSummary": "Automatic menubar summary", "settings.notifications.ai.unavailable.title": "Apple Intelligence Unavailable", "settings.notifications.ai.unavailable.body": "These features require macOS 26.0 or later to run local language models.", "settings.notifications.calls": "Call Alerts", - "settings.notifications.callAlert": "Call alert", + "settings.notifications.callAlert": "Call Alert", "settings.notifications.ring": "Ring for calls", - "settings.notifications.apps.connect": "Connect your Android to see the list of apps", + "settings.notifications.apps.connect": "Connect your android to see the list of apps", "settings.notifications.apps.none": "No apps found", "settings.notifications.app.settings": "%@ Notification Settings", - "settings.notifications.app.noSettings": "No settings yet", + "settings.notifications.app.noSettings": "No settings yet.", "settings.notifications.app.title": "App notifications", "settings.notifications.app.priority": "Priority", "settings.notifications.app.priority.alert": "Alert", @@ -57,7 +57,7 @@ "transfers.empty": "Nothing transferred yet", "plus.title": "AirSync+", "plus.why": "Why Plus?", - "plus.why.body": "Keeps me inspired to continue and maybe even to publish to the Apple app store and Google Play Store. Think of it as a little donation to keep this project alive and evolving.\nThat said, I know not everyone who wants the full experience can afford it. If that’s you, please don’t hesitate to reach out. 😊\n\nThe source code is available on GitHub, and you're more than welcome to build with all Plus features free — for personal use which also opens for contributions which is a win-win!.\nAs a thank-you for supporting the app, AirSync+ unlocks some nice extras: media controls, synced widgets, low battery alerts, wireless ADB, file browser, and more to come as I keep adding new features.\n\nEnjoy the app!\n(っ◕‿◕)っ", + "plus.why.body": "Keeps me inspired to continue and maybe even to publish to the Apple app store and Google Play Store. Think of it as a little donation to keep this project alive and evolving.\nThat said, I know not everyone who wants the full experience can afford it. If that's you, please don't hesitate to reach out. 😊\n\nThe source code is available on Github, and you're more than welcome to build with all Plus features free — for personal use which also opens for contributions which is a win-win!.\nAs a thank-you for supporting the app, AirSync+ unlocks some nice extras: media controls, synced widgets, low battery alerts, wireless ADB, file browser, and more to come as I keep adding new features.\n\nEnjoy the app!\n(っ◕‿◕)っ", "license.enterKey": "Enter license key", "status.battery": "Battery", "status.volume": "Volume", @@ -92,7 +92,7 @@ "quickshare.receiving": "Receiving...", "quickshare.connecting": "Connecting...", "quickshare.confirm_pin": "Confirm PIN on your device", - "quickshare.finished": "Transfer Finished!", + "quickshare.finished": "Transfer Finished", "quickshare.failed": "Transfer Failed", "quickshare.pin_code": "PIN: %@", "quickshare.device_sending_files": "%1$@ is sending %2$@", @@ -132,7 +132,7 @@ "settings.mirroring.swapCmdCtrl": "Swap ⌘ and ⌃ (macOS familiar)", "settings.mirroring.showMirrorControls": "Show navigation buttons (⌘B)", "quickshare.copy": "Copy to clipboard", - "settings.menubar.showInMenubar": "Show in menubar", + "settings.menubar.showInMenubar": "Show in Menubar", "settings.menubar.showInMenubar.disabledInfo": "Menu bar item cannot be hidden when Dock icon is hidden to keep AirSync accessible.", "settings.menubar.showIcon": "Show menu bar icon", "settings.menubar.showText": "Show menu bar Text", From ed6a57421a7aa80ffec830da453f6208b5656e72 Mon Sep 17 00:00:00 2001 From: "sameerasw.com" Date: Wed, 7 Oct 2026 20:55:10 +0530 Subject: [PATCH 05/14] New translations en.json (English) [ci skip] --- airsync-mac/Localization/en.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/airsync-mac/Localization/en.json b/airsync-mac/Localization/en.json index 7334f9d6..e6af275d 100644 --- a/airsync-mac/Localization/en.json +++ b/airsync-mac/Localization/en.json @@ -41,7 +41,7 @@ "settings.notifications.apps.connect": "Connect your android to see the list of apps", "settings.notifications.apps.none": "No apps found", "settings.notifications.app.settings": "%@ Notification Settings", - "settings.notifications.app.noSettings": "No settings yet.", + "settings.notifications.app.noSettings": "No settings yet", "settings.notifications.app.title": "App notifications", "settings.notifications.app.priority": "Priority", "settings.notifications.app.priority.alert": "Alert", @@ -72,7 +72,7 @@ "quickshare.settings.discoverable": "Discoverable as %@", "quickshare.accept": "Accept", "quickshare.decline": "Decline", - "quickshare.deviceSending": "%1$@ is sending %2$@", + "quickshare.deviceSending": "%1$@ Is sending %2$@", "quickshare.error.protocol": "Protocol error", "quickshare.error.crypto": "Encryption error", "quickshare.transferError": "Transfer error with %@", @@ -95,29 +95,29 @@ "quickshare.finished": "Transfer Finished", "quickshare.failed": "Transfer Failed", "quickshare.pin_code": "PIN: %@", - "quickshare.device_sending_files": "%1$@ is sending %2$@", - "quickshare.n_files": "%d files", + "quickshare.device_sending_files": "%1$@ Is sending %2$@", + "quickshare.n_files": "%d Files", "quickshare.drop.send_to": "Send to %@", "quickshare.drop.pick_device": "Pick a device to send", "settings.quickshare": "Quick Share and Files", "settings.fileAccess.title": "File Access", "settings.fileAccess.enabled": "Enable file access", - "settings.fileAccess.description": "Mount your Android device storage as a local drive in macOS Finder automatically.", + "settings.fileAccess.description": "Mount your Android device storage as a local drive in macOS Finder automatically", "settings.mirroring.scrcpy.title": "scrcpy Mirror", "settings.mirroring.scrcpy.description": "scrcpy is a reliable integration built-in to AirSync", "settings.mirroring.native.title": "Native Mirror (BETA)", "settings.mirroring.native.description": "As close as you can get to iPhone Mirroring XD. Powered by scrcpy. WIP", "settings.mirroring.defaultMode": "Android Mirror Mode", "settings.mirroring.desktop.defaultMode": "Desktop Mirror Mode", - "settings.mirroring.native.info": "Native Android Mirroring powered by scrcpy to feel just like iPhone Mirroring. WIP. You can always right click the mirror button for other options.", - "settings.mirroring.scrcpy.info": "Integration with scrcpy to provide reliable Android Mirroring. You can always right click the mirror button for other options.", + "settings.mirroring.native.info": "Native Android Mirroring powered by scrcpy to feel just like iPhone Mirroring. WIP. You can always right-click the mirror button for other options", + "settings.mirroring.scrcpy.info": "Integration with scrcpy to provide reliable Andrdoid Mirroring. You can always right-click the mirror button for other options", "settings.mirroring.appMirroring": "App Mirroring", "settings.mirroring.enableAppMirroring": "Enable App Mirroring", "settings.mirroring.videoBitrate": "Video bitrate", "settings.mirroring.bitrateFormat": "%d Mbps", - "settings.mirroring.maxSize": "Max size", + "settings.mirroring.maxSize": "Max Size", "settings.mirroring.stayOnTop": "Stay on top", - "settings.mirroring.stayAwake": "Stay awake (charging)", + "settings.mirroring.stayAwake": "Stay aware (charging)", "settings.mirroring.blankDisplay": "Blank display", "settings.mirroring.noAudio": "No audio", "settings.mirroring.continueApp": "Continue app after closing", @@ -181,8 +181,8 @@ "settings.autoStartAtLogin": "Auto start at login", "settings.newDevice": "New device?", "settings.pairing.howToPair": "How to pair?", - "settings.pairing.instructions": "In order to pair your device, follow these steps:\n\n1. Go to Android device's Settings -> About phone\n2. Tap on \"Build number\" 7 times to unlcok developer options\n3. Go to Settings -> System -> Developer options\n4. Look for \"USB debugging\" and enable\n5. Look for \"Wireless debugging\", enable and go in\n6. Tap on \"Pair device with QR code\" option and scan the above QR code to authenticate the device. If prompted, Select \"Always allow on this network\" if prompted.\n\nDone! You are ready to connect.", - "settings.pairing.troubleshooting": "Troubleshooting", + "settings.pairing.instructions": "In order to pair your device, follow these steps:\n\n1. Go to Android device's Settings -> About phone\n2. Tap on \"Build number\" 7 times to unlock developer options\n3. Go to Settings -> System - > Developer options\n4. Look for \"USB debugging\" and enable\n5. Look for \"Wireless debugging\", enable and go in\n6. Tap on \"Pair device with QR code\" option and scan the above QR code to authenticate the device. If prompted, Select \"Always allow on this network\" if prompted.\n\nDone! You are ready to connect.", + "settings.pairing.troubleshooting": "TroubleShooting", "settings.pairing.troubleshooting.text": "If pairing is failing, please check the following:\n\n• Ensure both devices are connected to the exact same Wi-Fi network.\n• Check if you have an active VPN on your Mac or Android device, which can block local network traffic (mDNS).\n• Turn Wireless Debugging off and back on in Android Developer options.\n• Check if your router blocks Multicast or local network discovery (AP Isolation).", "whatsnew.settings.title": "Welcome to new settings!", "whatsnew.settings.message": "Explore all your AirSync settings reorganized in a sleek, convenient new layout.", From 0c2061dd514d45576121f60efe25b38441a737e9 Mon Sep 17 00:00:00 2001 From: "sameerasw.com" Date: Wed, 7 Oct 2026 22:21:53 +0530 Subject: [PATCH 06/14] New translations en.json (English) [ci skip] --- airsync-mac/Localization/en.json | 38 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/airsync-mac/Localization/en.json b/airsync-mac/Localization/en.json index e6af275d..c27f3e8a 100644 --- a/airsync-mac/Localization/en.json +++ b/airsync-mac/Localization/en.json @@ -1,9 +1,9 @@ { - "app.name": "Airsync", + "app.name": "AirSync", "menu.mirror": "Mirror", "menu.browseFiles": "Browse Files", "menu.about": "About AirSync", - "menu.checkUpdates": "Check for Updates...", + "menu.checkUpdates": "Check for Updates…", "menu.quit": "Quit", "button.connect": "Connect", "home.title": "AirSync", @@ -22,23 +22,23 @@ "settings.sync": "Sync", "settings.notifications": "Notifications & Alerts", "settings.notifications.apps": "Apps", - "settings.notifications.sync": "Notification Sync", + "settings.notifications.sync": "Notifications Sync", "settings.notifications.dismiss": "Sync notification dismissals", "settings.notifications.system": "System Notifications", "settings.notifications.sound.default": "Default", "settings.notifications.grant": "Grant Permission", "settings.notifications.ai.title": "Apple Intelligence", "settings.notifications.ai.disableAll": "Disable all AI features", - "settings.notifications.ai.showToolbarButton": "Summarize notification button in toolbar", + "settings.notifications.ai.showToolbarButton": "Summarize notifications button in toolbar", "settings.notifications.ai.includeSilent": "Include silent notifications", "settings.notifications.ai.menubarSummary": "Menubar pop-up summary", "settings.notifications.ai.autoMenubarSummary": "Automatic menubar summary", "settings.notifications.ai.unavailable.title": "Apple Intelligence Unavailable", "settings.notifications.ai.unavailable.body": "These features require macOS 26.0 or later to run local language models.", "settings.notifications.calls": "Call Alerts", - "settings.notifications.callAlert": "Call Alert", + "settings.notifications.callAlert": "Call alert", "settings.notifications.ring": "Ring for calls", - "settings.notifications.apps.connect": "Connect your android to see the list of apps", + "settings.notifications.apps.connect": "Connect your Android to see the list of apps", "settings.notifications.apps.none": "No apps found", "settings.notifications.app.settings": "%@ Notification Settings", "settings.notifications.app.noSettings": "No settings yet", @@ -57,7 +57,7 @@ "transfers.empty": "Nothing transferred yet", "plus.title": "AirSync+", "plus.why": "Why Plus?", - "plus.why.body": "Keeps me inspired to continue and maybe even to publish to the Apple app store and Google Play Store. Think of it as a little donation to keep this project alive and evolving.\nThat said, I know not everyone who wants the full experience can afford it. If that's you, please don't hesitate to reach out. 😊\n\nThe source code is available on Github, and you're more than welcome to build with all Plus features free — for personal use which also opens for contributions which is a win-win!.\nAs a thank-you for supporting the app, AirSync+ unlocks some nice extras: media controls, synced widgets, low battery alerts, wireless ADB, file browser, and more to come as I keep adding new features.\n\nEnjoy the app!\n(っ◕‿◕)っ", + "plus.why.body": "Keeps me inspired to continue and maybe even to publish to the Apple app store and Google Play Store. Think of it as a little donation to keep this project alive and evolving.\nThat said, I know not everyone who wants the full experience can afford it. If that’s you, please don’t hesitate to reach out. 😊\n\nThe source code is available on GitHub, and you're more than welcome to build with all Plus features free — for personal use which also opens for contributions which is a win-win!.\nAs a thank-you for supporting the app, AirSync+ unlocks some nice extras: media controls, synced widgets, low battery alerts, wireless ADB, file browser, and more to come as I keep adding new features.\n\nEnjoy the app!\n(っ◕‿◕)っ", "license.enterKey": "Enter license key", "status.battery": "Battery", "status.volume": "Volume", @@ -72,7 +72,7 @@ "quickshare.settings.discoverable": "Discoverable as %@", "quickshare.accept": "Accept", "quickshare.decline": "Decline", - "quickshare.deviceSending": "%1$@ Is sending %2$@", + "quickshare.deviceSending": "%1$@ is sending %2$@", "quickshare.error.protocol": "Protocol error", "quickshare.error.crypto": "Encryption error", "quickshare.transferError": "Transfer error with %@", @@ -92,32 +92,32 @@ "quickshare.receiving": "Receiving...", "quickshare.connecting": "Connecting...", "quickshare.confirm_pin": "Confirm PIN on your device", - "quickshare.finished": "Transfer Finished", + "quickshare.finished": "Transfer Finished!", "quickshare.failed": "Transfer Failed", "quickshare.pin_code": "PIN: %@", - "quickshare.device_sending_files": "%1$@ Is sending %2$@", - "quickshare.n_files": "%d Files", + "quickshare.device_sending_files": "%1$@ is sending %2$@", + "quickshare.n_files": "%d files", "quickshare.drop.send_to": "Send to %@", "quickshare.drop.pick_device": "Pick a device to send", "settings.quickshare": "Quick Share and Files", "settings.fileAccess.title": "File Access", "settings.fileAccess.enabled": "Enable file access", - "settings.fileAccess.description": "Mount your Android device storage as a local drive in macOS Finder automatically", + "settings.fileAccess.description": "Mount your Android device storage as a local drive in macOS Finder automatically.", "settings.mirroring.scrcpy.title": "scrcpy Mirror", "settings.mirroring.scrcpy.description": "scrcpy is a reliable integration built-in to AirSync", "settings.mirroring.native.title": "Native Mirror (BETA)", "settings.mirroring.native.description": "As close as you can get to iPhone Mirroring XD. Powered by scrcpy. WIP", "settings.mirroring.defaultMode": "Android Mirror Mode", "settings.mirroring.desktop.defaultMode": "Desktop Mirror Mode", - "settings.mirroring.native.info": "Native Android Mirroring powered by scrcpy to feel just like iPhone Mirroring. WIP. You can always right-click the mirror button for other options", - "settings.mirroring.scrcpy.info": "Integration with scrcpy to provide reliable Andrdoid Mirroring. You can always right-click the mirror button for other options", + "settings.mirroring.native.info": "Native Android Mirroring powered by scrcpy to feel just like iPhone Mirroring. WIP. You can always right click the mirror button for other options.", + "settings.mirroring.scrcpy.info": "Integration with scrcpy to provide reliable Android Mirroring. You can always right click the mirror button for other options.", "settings.mirroring.appMirroring": "App Mirroring", "settings.mirroring.enableAppMirroring": "Enable App Mirroring", "settings.mirroring.videoBitrate": "Video bitrate", "settings.mirroring.bitrateFormat": "%d Mbps", - "settings.mirroring.maxSize": "Max Size", + "settings.mirroring.maxSize": "Max size", "settings.mirroring.stayOnTop": "Stay on top", - "settings.mirroring.stayAwake": "Stay aware (charging)", + "settings.mirroring.stayAwake": "Stay awake (charging)", "settings.mirroring.blankDisplay": "Blank display", "settings.mirroring.noAudio": "No audio", "settings.mirroring.continueApp": "Continue app after closing", @@ -132,7 +132,7 @@ "settings.mirroring.swapCmdCtrl": "Swap ⌘ and ⌃ (macOS familiar)", "settings.mirroring.showMirrorControls": "Show navigation buttons (⌘B)", "quickshare.copy": "Copy to clipboard", - "settings.menubar.showInMenubar": "Show in Menubar", + "settings.menubar.showInMenubar": "Show in menubar", "settings.menubar.showInMenubar.disabledInfo": "Menu bar item cannot be hidden when Dock icon is hidden to keep AirSync accessible.", "settings.menubar.showIcon": "Show menu bar icon", "settings.menubar.showText": "Show menu bar Text", @@ -181,8 +181,8 @@ "settings.autoStartAtLogin": "Auto start at login", "settings.newDevice": "New device?", "settings.pairing.howToPair": "How to pair?", - "settings.pairing.instructions": "In order to pair your device, follow these steps:\n\n1. Go to Android device's Settings -> About phone\n2. Tap on \"Build number\" 7 times to unlock developer options\n3. Go to Settings -> System - > Developer options\n4. Look for \"USB debugging\" and enable\n5. Look for \"Wireless debugging\", enable and go in\n6. Tap on \"Pair device with QR code\" option and scan the above QR code to authenticate the device. If prompted, Select \"Always allow on this network\" if prompted.\n\nDone! You are ready to connect.", - "settings.pairing.troubleshooting": "TroubleShooting", + "settings.pairing.instructions": "In order to pair your device, follow these steps:\n\n1. Go to Android device's Settings -> About phone\n2. Tap on \"Build number\" 7 times to unlcok developer options\n3. Go to Settings -> System -> Developer options\n4. Look for \"USB debugging\" and enable\n5. Look for \"Wireless debugging\", enable and go in\n6. Tap on \"Pair device with QR code\" option and scan the above QR code to authenticate the device. If prompted, Select \"Always allow on this network\" if prompted.\n\nDone! You are ready to connect.", + "settings.pairing.troubleshooting": "Troubleshooting", "settings.pairing.troubleshooting.text": "If pairing is failing, please check the following:\n\n• Ensure both devices are connected to the exact same Wi-Fi network.\n• Check if you have an active VPN on your Mac or Android device, which can block local network traffic (mDNS).\n• Turn Wireless Debugging off and back on in Android Developer options.\n• Check if your router blocks Multicast or local network discovery (AP Isolation).", "whatsnew.settings.title": "Welcome to new settings!", "whatsnew.settings.message": "Explore all your AirSync settings reorganized in a sleek, convenient new layout.", From 1f445beb98dec412f347522c6bb0854391c9d64e Mon Sep 17 00:00:00 2001 From: "sameerasw.com" Date: Wed, 7 Oct 2026 22:21:55 +0530 Subject: [PATCH 07/14] New translations en.json (French) [ci skip] --- airsync-mac/Localization/fr.json | 96 ++++++++++++++++---------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/airsync-mac/Localization/fr.json b/airsync-mac/Localization/fr.json index d795e937..e8581ffa 100644 --- a/airsync-mac/Localization/fr.json +++ b/airsync-mac/Localization/fr.json @@ -1,11 +1,11 @@ { "app.name": "AirSync", "menu.mirror": "Miroir", - "menu.browseFiles": "Browse Files", + "menu.browseFiles": "Parcourir les fichiers", "menu.about": "À propos de Airsync", "menu.checkUpdates": "Recherche de mise à jour…", "menu.quit": "Quitter", - "button.connect": "Connect", + "button.connect": "Connecter", "home.title": "AirSync", "home.sidebar.ready": "Prêt", "onboarding.title": "Bienvenue sur AirSync", @@ -18,40 +18,40 @@ "apps.tab": "Applis", "transfers.tab": "Transferts", "settings.tab": "Paramètres", - "settings.myMac": "My Mac", - "settings.sync": "Sync", - "settings.notifications": "Notifications & Alerts", - "settings.notifications.apps": "Apps", + "settings.myMac": "Mon Mac", + "settings.sync": "Synchronisation", + "settings.notifications": "Notifications et alertes", + "settings.notifications.apps": "Applications", "settings.notifications.sync": "Notifications Sync", - "settings.notifications.dismiss": "Sync notification dismissals", - "settings.notifications.system": "System Notifications", - "settings.notifications.sound.default": "Default", - "settings.notifications.grant": "Grant Permission", - "settings.notifications.ai.title": "Apple Intelligence", - "settings.notifications.ai.disableAll": "Disable all AI features", - "settings.notifications.ai.showToolbarButton": "Summarize notifications button in toolbar", - "settings.notifications.ai.includeSilent": "Include silent notifications", - "settings.notifications.ai.menubarSummary": "Menubar pop-up summary", - "settings.notifications.ai.autoMenubarSummary": "Automatic menubar summary", - "settings.notifications.ai.unavailable.title": "Apple Intelligence Unavailable", - "settings.notifications.ai.unavailable.body": "These features require macOS 26.0 or later to run local language models.", - "settings.notifications.calls": "Call Alerts", - "settings.notifications.callAlert": "Call alert", - "settings.notifications.ring": "Ring for calls", - "settings.notifications.apps.connect": "Connect your Android to see the list of apps", - "settings.notifications.apps.none": "No apps found", - "settings.notifications.app.settings": "%@ Notification Settings", - "settings.notifications.app.noSettings": "No settings yet", - "settings.notifications.app.title": "App notifications", - "settings.notifications.app.priority": "Priority", - "settings.notifications.app.priority.alert": "Alert", - "settings.notifications.app.priority.silent": "Silent", - "settings.notifications.close": "Close", - "settings.notifications.mediaPlayback": "Media Playback", - "settings.mirroring": "Mirroring", - "settings.menubar": "Menubar", - "settings.appleIntelligence": "Apple Intelligence", - "settings.appearance": "Appearance & more", + "settings.notifications.dismiss": "Synchroniser les rejets de notification", + "settings.notifications.system": "Notifications système", + "settings.notifications.sound.default": "Défaut", + "settings.notifications.grant": "Accorder la permission", + "settings.notifications.ai.title": "Intelligence Apple", + "settings.notifications.ai.disableAll": "Désactiver toutes les fonctionnalités AI", + "settings.notifications.ai.showToolbarButton": "Résumer les notifications dans la barre d'outils", + "settings.notifications.ai.includeSilent": "Inclure les notifications silencieuses", + "settings.notifications.ai.menubarSummary": "Récapitulatif de la barre de menu", + "settings.notifications.ai.autoMenubarSummary": "Résumé automatique de la barre de menu", + "settings.notifications.ai.unavailable.title": "Intelligence Apple indisponible", + "settings.notifications.ai.unavailable.body": "Ces fonctionnalités nécessitent macOS 26.0 ou ultérieur pour exécuter des modèles de langue locale.", + "settings.notifications.calls": "Alertes d'appel", + "settings.notifications.callAlert": "Alertes d'appel", + "settings.notifications.ring": "Sonner les appels", + "settings.notifications.apps.connect": "Connectez votre Android pour voir la liste des applications", + "settings.notifications.apps.none": "Aucune application trouvée", + "settings.notifications.app.settings": "Paramètres de notification %@", + "settings.notifications.app.noSettings": "Aucun paramètre pour l'instant.", + "settings.notifications.app.title": "Notifications d'applications", + "settings.notifications.app.priority": "Priorité", + "settings.notifications.app.priority.alert": "Alerte", + "settings.notifications.app.priority.silent": "Silencieuse", + "settings.notifications.close": "Fermer", + "settings.notifications.mediaPlayback": "Lecture multimédia", + "settings.mirroring": "Miroir", + "settings.menubar": "Barre de menu", + "settings.appleIntelligence": "Intelligence Apple", + "settings.appearance": "Apparence & plus", "settings.airsyncPlus": "AirSync+", "qr.tab": "Scanner le QR code", "transfers.empty": "Rien de transféré pour l'instant", @@ -64,7 +64,7 @@ "media.play": "Jouer", "media.pause": "Pause", "settings.wiredAdb": "Support d'ADB filaire", - "settings.alwaysKillAdbBeforeConnect": "Always kill-server before connect", + "settings.alwaysKillAdbBeforeConnect": "Toujours tuer le serveur avant la connexion", "quickshare.title": "Quick Share", "quickshare.button": "Quick Share", "quickshare.settings.title": "Paramètres de Quick Share", @@ -77,11 +77,11 @@ "quickshare.error.crypto": "Erreur de chiffrement", "quickshare.transferError": "Erreur du transfert avec %@", "quickshare.settings.autoAccept": "Accepter automatiquement de mon appareil", - "quickshare.settings.popupSharedImages": "Shared file pop-ups", - "quickshare.settings.maxPopups": "Max pop-up cards", - "quickshare.settings.popupSide": "Pop up position", - "quickshare.settings.side.left": "Left", - "quickshare.settings.side.right": "Right", + "quickshare.settings.popupSharedImages": "Popups de fichiers partagés", + "quickshare.settings.maxPopups": "Nombre maximum de cartes pop-up", + "quickshare.settings.popupSide": "Position du pop-up", + "quickshare.settings.side.left": "Gauche", + "quickshare.settings.side.right": "Droite", "quickshare.waiting_for": "En attente de %1$@...", "quickshare.select_device": "Sélectionnez un appareil", "quickshare.searching": "Recherche d'appareils à proximité...", @@ -90,7 +90,7 @@ "quickshare.done": "Confirmer", "quickshare.sending": "Envoi...", "quickshare.receiving": "Réception...", - "quickshare.connecting": "Connecting...", + "quickshare.connecting": "Connexion...", "quickshare.confirm_pin": "Confirmez le PIN sur votre appareil", "quickshare.finished": "Transfert terminé !", "quickshare.failed": "Transfert échoué", @@ -99,12 +99,12 @@ "quickshare.n_files": "%d fichiers", "quickshare.drop.send_to": "Envoyer à %@", "quickshare.drop.pick_device": "Choisissez un appareil auquel envoyer", - "settings.quickshare": "Quick Share and Files", - "settings.fileAccess.title": "File Access", - "settings.fileAccess.enabled": "Enable file access", - "settings.fileAccess.description": "Mount your Android device storage as a local drive in macOS Finder automatically.", - "settings.mirroring.scrcpy.title": "scrcpy Mirror", - "settings.mirroring.scrcpy.description": "scrcpy is a reliable integration built-in to AirSync", + "settings.quickshare": "Partage rapide et fichiers", + "settings.fileAccess.title": "Accès au fichier", + "settings.fileAccess.enabled": "Activer l'accès aux fichiers", + "settings.fileAccess.description": "Montez automatiquement le stockage de votre appareil Android en tant que lecteur local dans le moteur de recherche macOS.", + "settings.mirroring.scrcpy.title": "Miroir scrcpy", + "settings.mirroring.scrcpy.description": "scrcpy est une intégration fiable intégrée à AirSync", "settings.mirroring.native.title": "Native Mirror (BETA)", "settings.mirroring.native.description": "As close as you can get to iPhone Mirroring XD. Powered by scrcpy. WIP", "settings.mirroring.defaultMode": "Android Mirror Mode", From d8a99c12c070272b8a90f7564424f484441e4bdc Mon Sep 17 00:00:00 2001 From: hkay-dev <14947763+hkay-dev@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:28:40 -0700 Subject: [PATCH 08/14] Add macOS Share menu handoff to Quick Share --- AirSync.xcodeproj/project.pbxproj | 171 ++++++++++++++++ AirSyncShareExtension.entitlements | 14 ++ AirSyncShareExtension/Info.plist | 33 ++++ AirSyncShareExtension/ShareInputLoader.swift | 183 ++++++++++++++++++ .../ShareViewController.swift | 106 ++++++++++ README.md | 7 + airsync-mac/Core/AppDelegate.swift | 2 +- 7 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 AirSyncShareExtension.entitlements create mode 100644 AirSyncShareExtension/Info.plist create mode 100644 AirSyncShareExtension/ShareInputLoader.swift create mode 100644 AirSyncShareExtension/ShareViewController.swift diff --git a/AirSync.xcodeproj/project.pbxproj b/AirSync.xcodeproj/project.pbxproj index 92c1d201..d429f8f1 100644 --- a/AirSync.xcodeproj/project.pbxproj +++ b/AirSync.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A17000000000000000000001 /* AirSyncShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A17000000000000000000004 /* AirSyncShareExtension.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B94971602F62509A00F59BC9 /* BigInt in Frameworks */ = {isa = PBXBuildFile; productRef = B949715F2F62509A00F59BC9 /* BigInt */; }; B94971632F6250DE00F59BC9 /* ASN1 in Frameworks */ = {isa = PBXBuildFile; productRef = B94971622F6250DE00F59BC9 /* ASN1 */; }; B94971662F62514500F59BC9 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = B94971652F62514500F59BC9 /* SwiftECC */; }; @@ -26,6 +27,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + A17000000000000000000002 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B9673B4F2E35A2A1006D284A /* Project object */; + proxyType = 1; + remoteGlobalIDString = A17000000000000000000008; + remoteInfo = AirSyncShareExtension; + }; B99D64073059A795003D951A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B9673B4F2E35A2A1006D284A /* Project object */; @@ -43,6 +51,7 @@ dstSubfolderSpec = 13; files = ( B99D64093059A795003D951A /* AirSyncWidgetExtension.appex in Embed Foundation Extensions */, + A17000000000000000000001 /* AirSyncShareExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -50,6 +59,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + A17000000000000000000004 /* AirSyncShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AirSyncShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + A17000000000000000000010 /* AirSyncShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AirSyncShareExtension.entitlements; sourceTree = ""; }; B9673B572E35A2A1006D284A /* AirSync.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AirSync.app; sourceTree = BUILT_PRODUCTS_DIR; }; B99B04C52F641E1400FF3E2D /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; B99D63F83059A794003D951A /* AirSyncWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AirSyncWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -61,6 +72,13 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + A17000000000000000000005 /* Exceptions for "AirSyncShareExtension" folder in "AirSyncShareExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = A17000000000000000000008 /* AirSyncShareExtension */; + }; B933ED362E3BEC21007393CB /* Exceptions for "airsync-mac" folder in "AirSync" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -90,6 +108,14 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + A17000000000000000000006 /* AirSyncShareExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + A17000000000000000000005 /* Exceptions for "AirSyncShareExtension" folder in "AirSyncShareExtension" target */, + ); + path = AirSyncShareExtension; + sourceTree = ""; + }; B9673B592E35A2A1006D284A /* airsync-mac */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -109,6 +135,13 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + A17000000000000000000007 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B9673B542E35A2A1006D284A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -142,10 +175,12 @@ B9673B4E2E35A2A1006D284A = { isa = PBXGroup; children = ( + A17000000000000000000010 /* AirSyncShareExtension.entitlements */, B99D64153059A9FC003D951A /* AirSyncWidgetExtension.entitlements */, B99D64143059A9B3003D951A /* AirSync.entitlements */, B9673B592E35A2A1006D284A /* airsync-mac */, B99D63FD3059A794003D951A /* AirSyncWidget */, + A17000000000000000000006 /* AirSyncShareExtension */, B9D263192F60D97900628704 /* Frameworks */, B9673B582E35A2A1006D284A /* Products */, B99B04C52F641E1400FF3E2D /* AppIcon.icon */, @@ -158,6 +193,7 @@ children = ( B9673B572E35A2A1006D284A /* AirSync.app */, B99D63F83059A794003D951A /* AirSyncWidgetExtension.appex */, + A17000000000000000000004 /* AirSyncShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -174,6 +210,28 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + A17000000000000000000008 /* AirSyncShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = A1700000000000000000000F /* Build configuration list for PBXNativeTarget "AirSyncShareExtension" */; + buildPhases = ( + A1700000000000000000000A /* Sources */, + A17000000000000000000007 /* Frameworks */, + A17000000000000000000009 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + A17000000000000000000006 /* AirSyncShareExtension */, + ); + name = AirSyncShareExtension; + packageProductDependencies = ( + ); + productName = AirSyncShareExtension; + productReference = A17000000000000000000004 /* AirSyncShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; B9673B562E35A2A1006D284A /* AirSync */ = { isa = PBXNativeTarget; buildConfigurationList = B9673B622E35A2A3006D284A /* Build configuration list for PBXNativeTarget "AirSync" */; @@ -187,6 +245,7 @@ ); dependencies = ( B99D64083059A795003D951A /* PBXTargetDependency */, + A1700000000000000000000B /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( B9673B592E35A2A1006D284A /* airsync-mac */, @@ -241,6 +300,9 @@ LastSwiftUpdateCheck = 2700; LastUpgradeCheck = 2620; TargetAttributes = { + A17000000000000000000008 = { + CreatedOnToolsVersion = 27.0; + }; B9673B562E35A2A1006D284A = { CreatedOnToolsVersion = 26.0; }; @@ -277,11 +339,19 @@ targets = ( B9673B562E35A2A1006D284A /* AirSync */, B99D63F73059A794003D951A /* AirSyncWidgetExtension */, + A17000000000000000000008 /* AirSyncShareExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + A17000000000000000000009 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B9673B552E35A2A1006D284A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -301,6 +371,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + A1700000000000000000000A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B9673B532E35A2A1006D284A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -318,6 +395,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + A1700000000000000000000B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A17000000000000000000008 /* AirSyncShareExtension */; + targetProxy = A17000000000000000000002 /* PBXContainerItemProxy */; + }; B99D64083059A795003D951A /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B99D63F73059A794003D951A /* AirSyncWidgetExtension */; @@ -326,6 +408,85 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + A1700000000000000000000C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 32; + DEVELOPMENT_TEAM = WCB4HTANA6; + ENABLE_APP_SANDBOX = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirSyncShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirSync; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 4.1.0; + PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + A1700000000000000000000D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 32; + DEVELOPMENT_TEAM = WCB4HTANA6; + ENABLE_APP_SANDBOX = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirSyncShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirSync; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 4.1.0; + PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + A1700000000000000000000E /* Self Compiled */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 32; + DEVELOPMENT_TEAM = WCB4HTANA6; + ENABLE_APP_SANDBOX = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirSyncShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirSync; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 4.1.0; + PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = "Self Compiled"; + }; 11F6093B2EA2A88900AB6BDC /* Self Compiled */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = B9673B592E35A2A1006D284A /* airsync-mac */; @@ -780,6 +941,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + A1700000000000000000000F /* Build configuration list for PBXNativeTarget "AirSyncShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1700000000000000000000C /* Debug */, + A1700000000000000000000D /* Release */, + A1700000000000000000000E /* Self Compiled */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; B9673B522E35A2A1006D284A /* Build configuration list for PBXProject "AirSync" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/AirSyncShareExtension.entitlements b/AirSyncShareExtension.entitlements new file mode 100644 index 00000000..f835e023 --- /dev/null +++ b/AirSyncShareExtension.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.sameerasw.airsync-mac.widget + + com.apple.security.files.user-selected.read-only + + + diff --git a/AirSyncShareExtension/Info.plist b/AirSyncShareExtension/Info.plist new file mode 100644 index 00000000..856434a2 --- /dev/null +++ b/AirSyncShareExtension/Info.plist @@ -0,0 +1,33 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationDictionaryVersion + 2 + NSExtensionActivationSupportsAttachmentsWithMaxCount + 999 + NSExtensionActivationSupportsFileWithMaxCount + 999 + NSExtensionActivationSupportsImageWithMaxCount + 999 + NSExtensionActivationSupportsMovieWithMaxCount + 999 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 999 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareViewController + + + diff --git a/AirSyncShareExtension/ShareInputLoader.swift b/AirSyncShareExtension/ShareInputLoader.swift new file mode 100644 index 00000000..cb0491a5 --- /dev/null +++ b/AirSyncShareExtension/ShareInputLoader.swift @@ -0,0 +1,183 @@ +import Foundation +import UniformTypeIdentifiers + +struct ShareInputLoader { + enum InputError: LocalizedError { + case empty + case unsupported + case folder + + var errorDescription: String? { + switch self { + case .empty: "There is nothing to send." + case .unsupported: "AirSync cannot read one of the shared items." + case .folder: "AirSync cannot send folders yet." + } + } + } + + private let requestDirectory: URL + + init(groupContainer: URL) { + requestDirectory = groupContainer + .appendingPathComponent("Library/Caches/AirSyncShares", isDirectory: true) + .appendingPathComponent(UUID().uuidString, isDirectory: true) + } + + func prepare(_ items: [NSExtensionItem]) async throws -> [URL] { + removeOldRequests() + do { + var urls: [URL] = [] + for item in items { + let attachments = item.attachments ?? [] + if attachments.isEmpty { + if let text = item.attributedContentText?.string, !text.isEmpty { + urls.append(try stage(Data(text.utf8), name: "Shared Text.txt")) + } + } else { + for provider in attachments { + urls.append(try await prepare(provider)) + } + } + } + guard !urls.isEmpty else { throw InputError.empty } + return urls + } catch { + discard() + throw error + } + } + + func discard() { + try? FileManager.default.removeItem(at: requestDirectory) + } + + private func prepare(_ provider: NSItemProvider) async throws -> URL { + if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier), + let url = try? await loadURL(provider) { + if url.isFileURL { + try checkForFolder(url) + return url + } + return try stage(Data(url.absoluteString.utf8), name: "Shared Link.txt") + } + + let fileType = provider.registeredTypeIdentifiers.first { + guard let type = UTType($0) else { return false } + return type.conforms(to: .data) && !type.conforms(to: .text) && !type.conforms(to: .url) + } + if let fileType { + do { + return try await stageFile(provider, typeIdentifier: fileType) + } catch InputError.folder { + throw InputError.folder + } catch { + let data = try await loadData(provider, typeIdentifier: fileType) + return try stage(data, name: fileName(provider.suggestedName, typeIdentifier: fileType)) + } + } + + if let textType = provider.registeredTypeIdentifiers.first(where: { + UTType($0)?.conforms(to: .text) == true + }) { + let data = try await loadData(provider, typeIdentifier: textType) + return try stage(data, name: fileName(provider.suggestedName, typeIdentifier: textType, fallback: "Shared Text")) + } + throw InputError.unsupported + } + + private func loadURL(_ provider: NSItemProvider) async throws -> URL { + try await withCheckedThrowingContinuation { continuation in + _ = provider.loadObject(ofClass: NSURL.self) { url, error in + if let error { + continuation.resume(throwing: error) + } else if let url = url as? NSURL { + continuation.resume(returning: url as URL) + } else { + continuation.resume(throwing: InputError.unsupported) + } + } + } + } + + private func loadData(_ provider: NSItemProvider, typeIdentifier: String) async throws -> Data { + try await withCheckedThrowingContinuation { continuation in + _ = provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { data, error in + if let error { + continuation.resume(throwing: error) + } else if let data { + continuation.resume(returning: data) + } else { + continuation.resume(throwing: InputError.unsupported) + } + } + } + } + + private func stageFile(_ provider: NSItemProvider, typeIdentifier: String) async throws -> URL { + let suggestedName = provider.suggestedName + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + _ = provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { source, error in + if let error { + continuation.resume(throwing: error) + return + } + guard let source else { + continuation.resume(throwing: InputError.unsupported) + return + } + do { + try checkForFolder(source) + let name = fileName(suggestedName, typeIdentifier: typeIdentifier, fallback: source.lastPathComponent) + let destination = try destination(for: name) + try FileManager.default.copyItem(at: source, to: destination) + continuation.resume(returning: destination) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + private func stage(_ data: Data, name: String) throws -> URL { + let destination = try destination(for: name) + try data.write(to: destination, options: .atomic) + return destination + } + + private func destination(for name: String) throws -> URL { + let itemDirectory = requestDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: itemDirectory, withIntermediateDirectories: true) + return itemDirectory.appendingPathComponent(name) + } + + private func fileName(_ suggestedName: String?, typeIdentifier: String, fallback: String = "Shared Item") -> String { + let suggested = suggestedName ?? fallback + let name = (suggested as NSString).lastPathComponent + let safeName = name.isEmpty || name == "." || name == ".." ? fallback : name + guard (safeName as NSString).pathExtension.isEmpty, + let ext = UTType(typeIdentifier)?.preferredFilenameExtension else { return safeName } + return "\(safeName).\(ext)" + } + + private func checkForFolder(_ url: URL) throws { + if url.hasDirectoryPath || (try? url.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true { + throw InputError.folder + } + } + + private func removeOldRequests() { + let root = requestDirectory.deletingLastPathComponent() + guard let children = try? FileManager.default.contentsOfDirectory( + at: root, + includingPropertiesForKeys: [.contentModificationDateKey] + ) else { return } + let cutoff = Date().addingTimeInterval(-24 * 60 * 60) + for child in children where child != requestDirectory { + if let values = try? child.resourceValues(forKeys: [.contentModificationDateKey]), + let date = values.contentModificationDate, date < cutoff { + try? FileManager.default.removeItem(at: child) + } + } + } +} diff --git a/AirSyncShareExtension/ShareViewController.swift b/AirSyncShareExtension/ShareViewController.swift new file mode 100644 index 00000000..4f2085b1 --- /dev/null +++ b/AirSyncShareExtension/ShareViewController.swift @@ -0,0 +1,106 @@ +import AppKit + +final class ShareViewController: NSViewController { + private lazy var detail = NSTextField(wrappingLabelWithString: "AirSync will send this to your connected device or let you pick one.") + private lazy var sendButton = NSButton(title: "Send with AirSync", target: self, action: #selector(send)) + private lazy var cancelButton = NSButton(title: "Cancel", target: self, action: #selector(cancel)) + private var sendTask: Task? + + init() { + super.init(nibName: nil, bundle: nil) + preferredContentSize = NSSize(width: 360, height: 150) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError() + } + + override func loadView() { + let title = NSTextField(wrappingLabelWithString: "Send with AirSync") + title.font = .systemFont(ofSize: 17, weight: .semibold) + detail.textColor = .secondaryLabelColor + sendButton.keyEquivalent = "\r" + cancelButton.keyEquivalent = "\u{1b}" + + let buttons = NSStackView(views: [sendButton, cancelButton]) + buttons.orientation = .horizontal + buttons.spacing = 8 + + let stack = NSStackView(views: [title, detail, buttons]) + stack.orientation = .vertical + stack.alignment = .leading + stack.spacing = 12 + stack.translatesAutoresizingMaskIntoConstraints = false + + let content = NSView(frame: NSRect(origin: .zero, size: preferredContentSize)) + content.addSubview(stack) + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20), + stack.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20), + stack.centerYAnchor.constraint(equalTo: content.centerYAnchor), + ]) + view = content + } + + @objc private func send() { + guard sendTask == nil else { return } + sendButton.isEnabled = false + detail.stringValue = "Preparing this share for AirSync..." + sendTask = Task { @MainActor in + await handOff() + } + } + + private func handOff() async { + guard let items = extensionContext?.inputItems as? [NSExtensionItem], + let group = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.sameerasw.airsync-mac.widget" + ) else { + showError("AirSync could not access the shared items.") + return + } + + let loader = ShareInputLoader(groupContainer: group) + var openingStarted = false + do { + let urls = try await loader.prepare(items) + try Task.checkCancellation() + detail.stringValue = "Opening AirSync..." + let appURL = Bundle.main.bundleURL + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + let configuration = NSWorkspace.OpenConfiguration() + configuration.allowsRunningApplicationSubstitution = false + openingStarted = true + _ = try await NSWorkspace.shared.open( + urls, + withApplicationAt: appURL, + configuration: configuration + ) + try Task.checkCancellation() + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } catch is CancellationError { + if !openingStarted { loader.discard() } + } catch { + if !openingStarted { loader.discard() } + if !Task.isCancelled { showError(error.localizedDescription) } + } + } + + private func showError(_ message: String) { + detail.stringValue = message + sendButton.isEnabled = true + cancelButton.isEnabled = true + sendTask = nil + } + + @objc private func cancel() { + guard cancelButton.isEnabled else { return } + sendTask?.cancel() + extensionContext?.cancelRequest( + withError: NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError) + ) + } +} diff --git a/README.md b/README.md index 7a787859..bf94078a 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,13 @@ ## Building Locally In XCode, select the `AirSync Self Compiled` Scheme. Then at the menu bar, select `Product -> Archive`. Once this process is finished, the organizer window will pop up. Select `Distribute`, then `Custom`. Select `Copy App`, then select the folder for output. `AirSync.app` will be located in that folder. +## Share menu +The macOS Share extension hands files to AirSync's existing Quick Share transfer. It uses the connected device when one is available, or opens device selection otherwise. Images and videos supplied as temporary data are copied to the existing app group's cache first. Text and web links are sent as text files. Cached copies older than 24 hours are removed on a later share. + +The current file sender does not support folders. macOS and each sharing app control menu placement, so AirSync cannot force a position above AirDrop or appear for every item. + +For release signing, enable the existing `group.sameerasw.airsync-mac.widget` app group for the new Share extension identifier in the Apple Developer account. Sign the app and its extensions with the same team. + ## Contributors <3 diff --git a/airsync-mac/Core/AppDelegate.swift b/airsync-mac/Core/AppDelegate.swift index 5ddde499..c1143a37 100644 --- a/airsync-mac/Core/AppDelegate.swift +++ b/airsync-mac/Core/AppDelegate.swift @@ -54,7 +54,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { func application(_ application: NSApplication, open urls: [URL]) { if !urls.isEmpty { QuickShareManager.shared.transferURLs = urls - QuickShareManager.shared.startDiscovery(autoTargetName: nil) + QuickShareManager.shared.startDiscovery(autoTargetName: AppState.shared.device?.name) AppState.shared.showingQuickShareTransfer = true } } From 1ac467c096b1aea59dcd70dc1800bc6a60dfe30d Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 13 Oct 2026 22:56:26 +0530 Subject: [PATCH 09/14] Show item previews and auto-send from the Share sheet Co-Authored-By: Claude Sonnet 5.5 --- .../SharePreviewLoader.swift | 182 ++++++++++++++++++ AirSyncShareExtension/ShareView.swift | 159 +++++++++++++++ .../ShareViewController.swift | 86 +++++---- 3 files changed, 392 insertions(+), 35 deletions(-) create mode 100644 AirSyncShareExtension/SharePreviewLoader.swift create mode 100644 AirSyncShareExtension/ShareView.swift diff --git a/AirSyncShareExtension/SharePreviewLoader.swift b/AirSyncShareExtension/SharePreviewLoader.swift new file mode 100644 index 00000000..44b1489f --- /dev/null +++ b/AirSyncShareExtension/SharePreviewLoader.swift @@ -0,0 +1,182 @@ +import AppKit +import QuickLookThumbnailing +import UniformTypeIdentifiers + +struct SharePreviewItem: Identifiable { + let id = UUID() + let name: String + let detail: String + let icon: NSImage + let thumbnail: NSImage? + let byteCount: Int64? + let isFolder: Bool +} + +@MainActor +struct SharePreviewLoader { + static let thumbnailByteLimit: Int64 = 25 * 1024 * 1024 + private let thumbnailSize = CGSize(width: 96, height: 96) + + func load(_ items: [NSExtensionItem]) async -> [SharePreviewItem] { + var previews: [SharePreviewItem] = [] + for item in items { + let attachments = item.attachments ?? [] + if attachments.isEmpty { + if let text = item.attributedContentText?.string, !text.isEmpty { + previews.append(textPreview(text, name: "Shared Text")) + } + } else { + for provider in attachments { + if let preview = await preview(provider) { + previews.append(preview) + } + } + } + } + return previews + } + + private func preview(_ provider: NSItemProvider) async -> SharePreviewItem? { + if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier), + let url = try? await loadURL(provider) { + return url.isFileURL ? await filePreview(url) : linkPreview(url) + } + + let identifiers = provider.registeredTypeIdentifiers + if let imageType = identifiers.first(where: { UTType($0)?.conforms(to: .image) == true }) { + return await imagePreview(provider, typeIdentifier: imageType) + } + if let fileType = identifiers.first(where: { + guard let type = UTType($0) else { return false } + return type.conforms(to: .data) && !type.conforms(to: .text) && !type.conforms(to: .url) + }) { + let type = UTType(fileType) + return SharePreviewItem( + name: provider.suggestedName ?? "Shared Item", + detail: type?.localizedDescription ?? "File", + icon: type.map { NSWorkspace.shared.icon(for: $0) } ?? NSImage(named: NSImage.multipleDocumentsName) ?? NSImage(), + thumbnail: nil, + byteCount: nil, + isFolder: false + ) + } + if let textType = identifiers.first(where: { UTType($0)?.conforms(to: .text) == true }), + let data = try? await loadData(provider, typeIdentifier: textType), + let text = String(data: data, encoding: .utf8) { + return textPreview(text, name: provider.suggestedName ?? "Shared Text") + } + return nil + } + + private func filePreview(_ url: URL) async -> SharePreviewItem { + let values = try? url.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey, .localizedTypeDescriptionKey]) + let isFolder = values?.isDirectory == true || url.hasDirectoryPath + let size = values?.fileSize.map(Int64.init) + let kind = values?.localizedTypeDescription ?? "File" + let icon = NSWorkspace.shared.icon(forFile: url.path) + + var thumbnail: NSImage? + if !isFolder, let size, size <= Self.thumbnailByteLimit { + thumbnail = await quickLookThumbnail(url) + } + return SharePreviewItem( + name: url.lastPathComponent, + detail: isFolder ? "Folder" : [kind, size.map(Self.format)].compactMap { $0 }.joined(separator: " · "), + icon: icon, + thumbnail: thumbnail, + byteCount: isFolder ? nil : size, + isFolder: isFolder + ) + } + + private func imagePreview(_ provider: NSItemProvider, typeIdentifier: String) async -> SharePreviewItem? { + guard let data = try? await loadData(provider, typeIdentifier: typeIdentifier) else { return nil } + let size = Int64(data.count) + let type = UTType(typeIdentifier) + return SharePreviewItem( + name: provider.suggestedName ?? "Shared Image", + detail: [type?.localizedDescription ?? "Image", Self.format(size)].joined(separator: " · "), + icon: type.map { NSWorkspace.shared.icon(for: $0) } ?? NSImage(), + thumbnail: size <= Self.thumbnailByteLimit ? downsampled(data) : nil, + byteCount: size, + isFolder: false + ) + } + + private func linkPreview(_ url: URL) -> SharePreviewItem { + SharePreviewItem( + name: url.host ?? url.absoluteString, + detail: url.absoluteString, + icon: NSImage(systemSymbolName: "link", accessibilityDescription: nil) ?? NSImage(), + thumbnail: nil, + byteCount: Int64(url.absoluteString.utf8.count), + isFolder: false + ) + } + + private func textPreview(_ text: String, name: String) -> SharePreviewItem { + let snippet = text.trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "\n", with: " ") + return SharePreviewItem( + name: name, + detail: String(snippet.prefix(120)), + icon: NSImage(systemSymbolName: "text.alignleft", accessibilityDescription: nil) ?? NSImage(), + thumbnail: nil, + byteCount: Int64(text.utf8.count), + isFolder: false + ) + } + + private func quickLookThumbnail(_ url: URL) async -> NSImage? { + let request = QLThumbnailGenerator.Request( + fileAt: url, + size: thumbnailSize, + scale: NSScreen.main?.backingScaleFactor ?? 2, + representationTypes: .thumbnail + ) + return try? await QLThumbnailGenerator.shared.generateBestRepresentation(for: request).nsImage + } + + private func downsampled(_ data: Data) -> NSImage? { + guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil } + let options: [CFString: Any] = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: thumbnailSize.width * 2, + ] + guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else { return nil } + return NSImage(cgImage: image, size: .zero) + } + + private func loadURL(_ provider: NSItemProvider) async throws -> URL { + try await withCheckedThrowingContinuation { continuation in + _ = provider.loadObject(ofClass: NSURL.self) { url, error in + if let error { + continuation.resume(throwing: error) + } else if let url = url as? NSURL { + continuation.resume(returning: url as URL) + } else { + continuation.resume(throwing: CocoaError(.fileReadUnknown)) + } + } + } + } + + private func loadData(_ provider: NSItemProvider, typeIdentifier: String) async throws -> Data { + try await withCheckedThrowingContinuation { continuation in + _ = provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { data, error in + if let error { + continuation.resume(throwing: error) + } else if let data { + continuation.resume(returning: data) + } else { + continuation.resume(throwing: CocoaError(.fileReadUnknown)) + } + } + } + } + + nonisolated static func format(_ bytes: Int64) -> String { + ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file) + } +} diff --git a/AirSyncShareExtension/ShareView.swift b/AirSyncShareExtension/ShareView.swift new file mode 100644 index 00000000..a4e4e2f6 --- /dev/null +++ b/AirSyncShareExtension/ShareView.swift @@ -0,0 +1,159 @@ +import AppKit +import SwiftUI + +@MainActor +final class ShareModel: ObservableObject { + enum Phase: Equatable { + case working(String) + case failed(String) + } + + @Published var items: [SharePreviewItem] = [] + @Published var isLoadingPreview = true + @Published var phase: Phase = .working("Preparing…") + + var listHeight: CGFloat { + let rows = CGFloat(min(items.count, 3)) + return rows * 60 + max(rows - 1, 0) * 8 + } + + var totalHeight: CGFloat { + let base: CGFloat = 215 + return items.isEmpty ? base : base + listHeight + 14 + } + + var summary: String { + if isLoadingPreview { return "Checking items…" } + guard !items.isEmpty else { return "Nothing to send" } + let total = items.compactMap(\.byteCount).reduce(0, +) + let count = items.count == 1 ? "1 item" : "\(items.count) items" + return total > 0 ? "\(count) · \(SharePreviewLoader.format(total))" : count + } +} + +struct ShareView: View { + @ObservedObject var model: ShareModel + let cancel: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + header + if !model.items.isEmpty { itemList } + status + buttons + } + .padding(20) + .frame(width: 380, height: model.totalHeight, alignment: .topLeading) + } + + private var header: some View { + VStack(alignment: .leading, spacing: 3) { + Text("Send with AirSync") + .font(.title3.weight(.semibold)) + Text(model.summary) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + + private var itemList: some View { + ScrollView { + VStack(spacing: 8) { + ForEach(model.items) { item in + ShareItemRow(item: item) + } + } + } + .frame(height: model.listHeight) + } + + @ViewBuilder + private var status: some View { + switch model.phase { + case .working(let message): + HStack(spacing: 8) { + ProgressView().controlSize(.small) + Text(message) + } + .font(.callout) + .foregroundStyle(.secondary) + case .failed(let message): + Label(message, systemImage: "exclamationmark.triangle.fill") + .font(.callout) + .foregroundStyle(.red) + } + } + + private var buttons: some View { + HStack { + Spacer() + Button(isFailed ? "Close" : "Cancel", action: cancel) + .keyboardShortcut(.cancelAction) + .modifier(GlassButtonStyle(prominent: false)) + } + .controlSize(.large) + } + + private var isFailed: Bool { + if case .failed = model.phase { return true } + return false + } +} + +private struct ShareItemRow: View { + let item: SharePreviewItem + + var body: some View { + HStack(spacing: 12) { + Group { + if let thumbnail = item.thumbnail { + Image(nsImage: thumbnail).resizable().scaledToFill() + } else { + Image(nsImage: item.icon).resizable().scaledToFit().padding(6) + } + } + .frame(width: 44, height: 44) + .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + + VStack(alignment: .leading, spacing: 2) { + Text(item.name).lineLimit(1).truncationMode(.middle) + Text(item.detail) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + } + Spacer(minLength: 0) + } + .padding(8) + .modifier(GlassCard()) + } +} + +private struct GlassCard: ViewModifier { + func body(content: Content) -> some View { + if #available(macOS 26, *) { + content.glassEffect(.regular, in: .rect(cornerRadius: 14)) + } else { + content.background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + } +} + +private struct GlassButtonStyle: ViewModifier { + let prominent: Bool + + func body(content: Content) -> some View { + if #available(macOS 26, *) { + if prominent { + content.buttonStyle(.glassProminent) + } else { + content.buttonStyle(.glass) + } + } else if prominent { + content.buttonStyle(.borderedProminent) + } else { + content.buttonStyle(.bordered) + } + } +} diff --git a/AirSyncShareExtension/ShareViewController.swift b/AirSyncShareExtension/ShareViewController.swift index 4f2085b1..e315915d 100644 --- a/AirSyncShareExtension/ShareViewController.swift +++ b/AirSyncShareExtension/ShareViewController.swift @@ -1,14 +1,13 @@ import AppKit +import SwiftUI final class ShareViewController: NSViewController { - private lazy var detail = NSTextField(wrappingLabelWithString: "AirSync will send this to your connected device or let you pick one.") - private lazy var sendButton = NSButton(title: "Send with AirSync", target: self, action: #selector(send)) - private lazy var cancelButton = NSButton(title: "Cancel", target: self, action: #selector(cancel)) + private let model = ShareModel() private var sendTask: Task? init() { super.init(nibName: nil, bundle: nil) - preferredContentSize = NSSize(width: 360, height: 150) + preferredContentSize = NSSize(width: 380, height: 215) } @available(*, unavailable) @@ -17,36 +16,56 @@ final class ShareViewController: NSViewController { } override func loadView() { - let title = NSTextField(wrappingLabelWithString: "Send with AirSync") - title.font = .systemFont(ofSize: 17, weight: .semibold) - detail.textColor = .secondaryLabelColor - sendButton.keyEquivalent = "\r" - cancelButton.keyEquivalent = "\u{1b}" - - let buttons = NSStackView(views: [sendButton, cancelButton]) - buttons.orientation = .horizontal - buttons.spacing = 8 + let host = NSHostingView(rootView: ShareView( + model: model, + cancel: { [weak self] in self?.cancel() } + )) + host.translatesAutoresizingMaskIntoConstraints = false + let container = NSView(frame: NSRect(origin: .zero, size: preferredContentSize)) + if #available(macOS 26, *) { + let glass = NSGlassEffectView() + glass.contentView = host + glass.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(glass) + NSLayoutConstraint.activate([ + glass.topAnchor.constraint(equalTo: container.topAnchor), + glass.bottomAnchor.constraint(equalTo: container.bottomAnchor), + glass.leadingAnchor.constraint(equalTo: container.leadingAnchor), + glass.trailingAnchor.constraint(equalTo: container.trailingAnchor), + ]) + } else { + container.addSubview(host) + NSLayoutConstraint.activate([ + host.topAnchor.constraint(equalTo: container.topAnchor), + host.bottomAnchor.constraint(equalTo: container.bottomAnchor), + host.leadingAnchor.constraint(equalTo: container.leadingAnchor), + host.trailingAnchor.constraint(equalTo: container.trailingAnchor), + ]) + } + view = container + } - let stack = NSStackView(views: [title, detail, buttons]) - stack.orientation = .vertical - stack.alignment = .leading - stack.spacing = 12 - stack.translatesAutoresizingMaskIntoConstraints = false + override func viewDidLoad() { + super.viewDidLoad() + guard let items = extensionContext?.inputItems as? [NSExtensionItem] else { + model.isLoadingPreview = false + return + } + send() + Task { @MainActor in + model.items = await SharePreviewLoader().load(items) + model.isLoadingPreview = false + resizeToFit() + } + } - let content = NSView(frame: NSRect(origin: .zero, size: preferredContentSize)) - content.addSubview(stack) - NSLayoutConstraint.activate([ - stack.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20), - stack.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20), - stack.centerYAnchor.constraint(equalTo: content.centerYAnchor), - ]) - view = content + private func resizeToFit() { + preferredContentSize = NSSize(width: 380, height: model.totalHeight) } - @objc private func send() { + private func send() { guard sendTask == nil else { return } - sendButton.isEnabled = false - detail.stringValue = "Preparing this share for AirSync..." + model.phase = .working("Preparing this share for AirSync…") sendTask = Task { @MainActor in await handOff() } @@ -66,7 +85,7 @@ final class ShareViewController: NSViewController { do { let urls = try await loader.prepare(items) try Task.checkCancellation() - detail.stringValue = "Opening AirSync..." + model.phase = .working("Opening AirSync…") let appURL = Bundle.main.bundleURL .deletingLastPathComponent() .deletingLastPathComponent() @@ -90,14 +109,11 @@ final class ShareViewController: NSViewController { } private func showError(_ message: String) { - detail.stringValue = message - sendButton.isEnabled = true - cancelButton.isEnabled = true + model.phase = .failed(message) sendTask = nil } - @objc private func cancel() { - guard cancelButton.isEnabled else { return } + private func cancel() { sendTask?.cancel() extensionContext?.cancelRequest( withError: NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError) From b49cb305d53e855d918da9242d72a5b9f500af1b Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 13 Oct 2026 22:57:04 +0530 Subject: [PATCH 10/14] Clean up staged share files after transfer and harden the Share extension Co-Authored-By: Claude Sonnet 5.5 --- AirSyncShareExtension/ShareViewController.swift | 4 ++++ README.md | 2 +- airsync-mac/Core/QuickShare/QuickShareManager.swift | 9 +++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/AirSyncShareExtension/ShareViewController.swift b/AirSyncShareExtension/ShareViewController.swift index e315915d..1abf8d17 100644 --- a/AirSyncShareExtension/ShareViewController.swift +++ b/AirSyncShareExtension/ShareViewController.swift @@ -72,6 +72,7 @@ final class ShareViewController: NSViewController { } private func handOff() async { + defer { sendTask = nil } guard let items = extensionContext?.inputItems as? [NSExtensionItem], let group = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: "group.sameerasw.airsync-mac.widget" @@ -90,6 +91,9 @@ final class ShareViewController: NSViewController { .deletingLastPathComponent() .deletingLastPathComponent() .deletingLastPathComponent() + guard appURL.pathExtension == "app" else { + throw CocoaError(.fileNoSuchFile) + } let configuration = NSWorkspace.OpenConfiguration() configuration.allowsRunningApplicationSubstitution = false openingStarted = true diff --git a/README.md b/README.md index bf94078a..66a7fa00 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ In XCode, select the `AirSync Self Compiled` Scheme. Then at the menu bar, selec ## Share menu The macOS Share extension hands files to AirSync's existing Quick Share transfer. It uses the connected device when one is available, or opens device selection otherwise. Images and videos supplied as temporary data are copied to the existing app group's cache first. Text and web links are sent as text files. Cached copies older than 24 hours are removed on a later share. -The current file sender does not support folders. macOS and each sharing app control menu placement, so AirSync cannot force a position above AirDrop or appear for every item. +The current file sender does not support folders. Enable the extension in System Settings → Extensions → Sharing. macOS and each sharing app control where AirSync appears in the Share menu, so its position cannot be forced. For release signing, enable the existing `group.sameerasw.airsync-mac.widget` app group for the new Share extension identifier in the Apple Developer account. Sign the app and its extensions with the same team. diff --git a/airsync-mac/Core/QuickShare/QuickShareManager.swift b/airsync-mac/Core/QuickShare/QuickShareManager.swift index 2ad2de02..bfb14493 100644 --- a/airsync-mac/Core/QuickShare/QuickShareManager.swift +++ b/airsync-mac/Core/QuickShare/QuickShareManager.swift @@ -126,7 +126,14 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha } } + private func removeStagedShareFiles() { + for url in transferURLs where url.pathComponents.contains("AirSyncShares") { + try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) + } + } + public func cancelActiveTransfer() { + removeStagedShareFiles() switch transferState { case .connecting(let id), .awaitingPin(_, let id), .sending(let id): NearbyConnectionManager.shared.cancelOutgoingTransfer(id: id) @@ -333,6 +340,7 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha public func connectionFailed(with error: Error) { transferState = .failed(error.localizedDescription) + removeStagedShareFiles() DispatchQueue.main.asyncAfter(deadline: .now() + 3) { AppState.shared.showingQuickShareTransfer = false self.transferState = .idle @@ -351,6 +359,7 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha public func transferFinished() { transferState = .finished + removeStagedShareFiles() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { AppState.shared.showingQuickShareTransfer = false self.transferState = .idle From 39590363df7ee5c725975c26875af14ba172d79c Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 27 Oct 2026 18:55:58 +0530 Subject: [PATCH 11/14] fix: re-opening app on share extension --- AirSync.xcodeproj/project.pbxproj | 200 +++++++++++------- .../ShareViewController.swift | 14 +- 2 files changed, 132 insertions(+), 82 deletions(-) diff --git a/AirSync.xcodeproj/project.pbxproj b/AirSync.xcodeproj/project.pbxproj index d429f8f1..92d7e0de 100644 --- a/AirSync.xcodeproj/project.pbxproj +++ b/AirSync.xcodeproj/project.pbxproj @@ -408,85 +408,6 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - A1700000000000000000000C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; - DEVELOPMENT_TEAM = WCB4HTANA6; - ENABLE_APP_SANDBOX = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = AirSyncShareExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = AirSync; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; - PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - A1700000000000000000000D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; - DEVELOPMENT_TEAM = WCB4HTANA6; - ENABLE_APP_SANDBOX = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = AirSyncShareExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = AirSync; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; - PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - A1700000000000000000000E /* Self Compiled */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; - DEVELOPMENT_TEAM = WCB4HTANA6; - ENABLE_APP_SANDBOX = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = AirSyncShareExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = AirSync; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; - PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = "Self Compiled"; - }; 11F6093B2EA2A88900AB6BDC /* Self Compiled */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = B9673B592E35A2A1006D284A /* airsync-mac */; @@ -602,6 +523,127 @@ }; name = "Self Compiled"; }; + A1700000000000000000000C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + AUTOMATION_APPLE_EVENTS = NO; + CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 32; + DEVELOPMENT_TEAM = WCB4HTANA6; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirSyncShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirSync; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 4.1.0; + PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; + RUNTIME_EXCEPTION_ALLOW_JIT = NO; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + A1700000000000000000000D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + AUTOMATION_APPLE_EVENTS = NO; + CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 32; + DEVELOPMENT_TEAM = WCB4HTANA6; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirSyncShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirSync; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 4.1.0; + PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; + RUNTIME_EXCEPTION_ALLOW_JIT = NO; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + A1700000000000000000000E /* Self Compiled */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + AUTOMATION_APPLE_EVENTS = NO; + CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 32; + DEVELOPMENT_TEAM = WCB4HTANA6; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AirSyncShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AirSync; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 14.5; + MARKETING_VERSION = 4.1.0; + PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; + RUNTIME_EXCEPTION_ALLOW_JIT = NO; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = "Self Compiled"; + }; B9673B602E35A2A3006D284A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = B9673B592E35A2A1006D284A /* airsync-mac */; diff --git a/AirSyncShareExtension/ShareViewController.swift b/AirSyncShareExtension/ShareViewController.swift index 1abf8d17..30e9f814 100644 --- a/AirSyncShareExtension/ShareViewController.swift +++ b/AirSyncShareExtension/ShareViewController.swift @@ -87,15 +87,16 @@ final class ShareViewController: NSViewController { let urls = try await loader.prepare(items) try Task.checkCancellation() model.phase = .working("Opening AirSync…") - let appURL = Bundle.main.bundleURL + let containingAppURL = Bundle.main.bundleURL .deletingLastPathComponent() .deletingLastPathComponent() .deletingLastPathComponent() - guard appURL.pathExtension == "app" else { + guard containingAppURL.pathExtension == "app" else { throw CocoaError(.fileNoSuchFile) } + let appURL = runningAppURL(matching: containingAppURL) ?? containingAppURL let configuration = NSWorkspace.OpenConfiguration() - configuration.allowsRunningApplicationSubstitution = false + configuration.allowsRunningApplicationSubstitution = true openingStarted = true _ = try await NSWorkspace.shared.open( urls, @@ -112,6 +113,13 @@ final class ShareViewController: NSViewController { } } + private func runningAppURL(matching containingAppURL: URL) -> URL? { + guard let bundleID = Bundle(url: containingAppURL)?.bundleIdentifier else { return nil } + return NSRunningApplication.runningApplications(withBundleIdentifier: bundleID) + .first { !$0.isTerminated }? + .bundleURL + } + private func showError(_ message: String) { model.phase = .failed(message) sendTask = nil From a30bf3ec0895f7fd833f80d16c573d7fe0a5197b Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 27 Oct 2026 19:32:17 +0530 Subject: [PATCH 12/14] feat: add Quick Share connection timeout handling, endpoint retry logic, and pong message type --- .../Core/QuickShare/NearbyConnection.swift | 47 +++++++- .../QuickShare/NearbyConnectionManager.swift | 5 +- .../QuickShare/OutboundNearbyConnection.swift | 6 + .../Core/QuickShare/QuickShareManager.swift | 103 ++++++++++++++---- .../WebSocket/WebSocketServer+Handlers.swift | 2 +- .../Core/WebSocket/WebSocketServer.swift | 1 + airsync-mac/Model/Message.swift | 1 + 7 files changed, 137 insertions(+), 28 deletions(-) diff --git a/airsync-mac/Core/QuickShare/NearbyConnection.swift b/airsync-mac/Core/QuickShare/NearbyConnection.swift index 041ce34c..644a50ee 100644 --- a/airsync-mac/Core/QuickShare/NearbyConnection.swift +++ b/airsync-mac/Core/QuickShare/NearbyConnection.swift @@ -55,24 +55,59 @@ public class NearbyConnection{ nonisolated deinit {} + private var socketReady=false + + /// Outbound connections fail instead of waiting forever on a stale endpoint. + internal var openTimeout:TimeInterval? { nil } + func start(){ print("[nearby] Starting connection to \(connection.endpoint)") connection.stateUpdateHandler={ [weak self] state in Task { @MainActor in guard let self=self else { return } - if case .ready = state { + switch state { + case .ready: + self.socketReady=true self.connectionReady() self.receiveFrameAsync() - } else if case .failed(let err) = state { - self.lastError=err - print("[nearby] Error opening socket: \(err)") - self.handleConnectionClosure() + case .waiting(let err) where self.openTimeout != nil && !self.socketReady: + self.failToOpen(err) + case .failed(let err): + if self.socketReady { + self.lastError=err + print("[nearby] Socket failed: \(err)") + self.handleConnectionClosure() + } else { + self.failToOpen(err) + } + default: + break } } } connection.start(queue: NearbyConnection.dispatchQueue) + if let timeout=openTimeout { + Task { @MainActor [weak self] in + try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + guard let self=self, !self.socketReady else { return } + self.failToOpen(NWError.posix(.ETIMEDOUT)) + } + } } - + + private func failToOpen(_ error:Error){ + guard !connectionClosed else { return } + connectionClosed=true + lastError=error + print("[nearby] Error opening socket: \(error)") + connection.cancel() + connectionDidFailToOpen() + } + + internal func connectionDidFailToOpen(){ + handleConnectionClosure() + } + func connectionReady(){} internal func handleConnectionClosure(){ diff --git a/airsync-mac/Core/QuickShare/NearbyConnectionManager.swift b/airsync-mac/Core/QuickShare/NearbyConnectionManager.swift index 31443def..b3ea5bbd 100644 --- a/airsync-mac/Core/QuickShare/NearbyConnectionManager.swift +++ b/airsync-mac/Core/QuickShare/NearbyConnectionManager.swift @@ -470,7 +470,10 @@ public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNear } public func startOutgoingTransfer(deviceID:String, delegate:ShareExtensionDelegate, urls:[URL]){ - guard let info=foundServices[deviceID] else {return} + guard let info=foundServices[deviceID] else { + delegate.connectionFailed(with: NearbyError.protocolError("Device is no longer available")) + return + } let tcp=NWProtocolTCP.Options.init() tcp.noDelay=true let nwconn=NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp)) diff --git a/airsync-mac/Core/QuickShare/OutboundNearbyConnection.swift b/airsync-mac/Core/QuickShare/OutboundNearbyConnection.swift index 2f0ba01f..5d1d7b81 100644 --- a/airsync-mac/Core/QuickShare/OutboundNearbyConnection.swift +++ b/airsync-mac/Core/QuickShare/OutboundNearbyConnection.swift @@ -66,6 +66,12 @@ public class OutboundNearbyConnection:NearbyConnection{ try? sendDisconnectionAndDisconnect() } + override var openTimeout:TimeInterval? { 5 } + + override func connectionDidFailToOpen() { + delegate?.outboundConnection(connection: self, failedWithError: lastError ?? NearbyError.inputOutput) + } + override func connectionReady() { super.connectionReady() do{ diff --git a/airsync-mac/Core/QuickShare/QuickShareManager.swift b/airsync-mac/Core/QuickShare/QuickShareManager.swift index bfb14493..4ed9864a 100644 --- a/airsync-mac/Core/QuickShare/QuickShareManager.swift +++ b/airsync-mac/Core/QuickShare/QuickShareManager.swift @@ -49,6 +49,13 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha } private var activeIncomingTransfers: [String: QuickShareTransferInfo] = [:] + + // Android advertises a fresh endpoint each session and stale mDNS records can linger, + // so one phone may show up under several endpoint IDs. Newest is last. + private var endpointsByName: [String: [RemoteDeviceInfo]] = [:] + private var sendTargetName: String? + private var triedEndpointIDs: Set = [] + private var autoTargetTask: Task? override private init() { super.init() @@ -103,6 +110,10 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha public func startDiscovery(autoTargetName: String? = nil) { discoveredDevices.removeAll() + endpointsByName.removeAll() + resetSendTarget() + autoTargetTask?.cancel() + autoTargetTask = nil self.autoTargetDeviceName = autoTargetName transferState = .discovering NearbyConnectionManager.shared.addShareExtensionDelegate(self) @@ -116,6 +127,10 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha NearbyConnectionManager.shared.stopDeviceDiscovery() NearbyConnectionManager.shared.removeShareExtensionDelegate(self) discoveredDevices.removeAll() + endpointsByName.removeAll() + resetSendTarget() + autoTargetTask?.cancel() + autoTargetTask = nil self.autoTargetDeviceName = nil switch transferState { @@ -133,6 +148,7 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha } public func cancelActiveTransfer() { + resetSendTarget() removeStagedShareFiles() switch transferState { case .connecting(let id), .awaitingPin(_, let id), .sending(let id): @@ -156,18 +172,32 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha } public func sendFiles(urls: [URL], to device: RemoteDeviceInfo) { + autoTargetTask?.cancel() + autoTargetTask = nil + sendTargetName = device.name + triedEndpointIDs = [] + transferURLs = urls + transferProgress = 0 + connect(to: device) + } + + private func resetSendTarget() { + sendTargetName = nil + triedEndpointIDs = [] + } + + /// Endpoints can appear mid-attempt (the phone re-advertises after our trigger), so look them up fresh. + private func nextUntriedEndpoint() -> RemoteDeviceInfo? { + guard let name = sendTargetName else { return nil } + return endpointsByName[name]?.last { $0.id.map { !triedEndpointIDs.contains($0) } ?? false } + } + + private func connect(to device: RemoteDeviceInfo) { guard let deviceID = device.id else { return } + triedEndpointIDs.insert(deviceID) + print("[quickshare] Connecting to '\(device.name)' via endpoint \(deviceID)") transferState = .connecting(deviceID) - transferProgress = 0 - - // Trigger Quick Share mode on Android if it's the connected device - if let connectedDevice = AppState.shared.device, - connectedDevice.name == device.name { - print("[quickshare] Target device matches connected device, sending WebSocket trigger") - WebSocketServer.shared.sendQuickShareTrigger() - } - - NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: deviceID, delegate: self, urls: urls) + NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: deviceID, delegate: self, urls: transferURLs) } public func generateQrCodeKey() -> String { @@ -303,20 +333,47 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha // MARK: - ShareExtensionDelegate (Outgoing) public func addDevice(device: RemoteDeviceInfo) { - if !discoveredDevices.contains(where: { $0.id == device.id }) { - discoveredDevices.append(device) - - // If auto-targeting is active and name matches, start transfer - if let targetName = autoTargetDeviceName, device.name == targetName { - print("[quickshare] Auto-targeting found device '\(device.name)', starting transfer") - self.autoTargetDeviceName = nil // Clear so it doesn't trigger again - sendFiles(urls: self.transferURLs, to: device) + var endpoints = endpointsByName[device.name] ?? [] + endpoints.removeAll { $0.id == device.id } + endpoints.append(device) + endpointsByName[device.name] = endpoints + showNewestEndpoint(named: device.name) + + // Wait briefly so a stale cached record doesn't win over the live one + if let targetName = autoTargetDeviceName, device.name == targetName, autoTargetTask == nil { + print("[quickshare] Auto-targeting found device '\(device.name)', starting transfer") + autoTargetTask = Task { @MainActor [weak self] in + try? await Task.sleep(nanoseconds: 1_200_000_000) + guard let self, !Task.isCancelled, self.transferState == .discovering, + let newest = self.endpointsByName[targetName]?.last else { return } + self.autoTargetDeviceName = nil + self.autoTargetTask = nil + self.sendFiles(urls: self.transferURLs, to: newest) } } } - + public func removeDevice(id: String) { - discoveredDevices.removeAll(where: { $0.id == id }) + guard let name = endpointsByName.first(where: { $0.value.contains { $0.id == id } })?.key else { return } + endpointsByName[name]?.removeAll { $0.id == id } + if endpointsByName[name]?.isEmpty == true { + endpointsByName[name] = nil + } + showNewestEndpoint(named: name) + } + + private func showNewestEndpoint(named name: String) { + let index = discoveredDevices.firstIndex { $0.name == name } + switch (index, endpointsByName[name]?.last) { + case let (index?, newest?): + discoveredDevices[index] = newest + case let (nil, newest?): + discoveredDevices.append(newest) + case let (index?, nil): + discoveredDevices.remove(at: index) + case (nil, nil): + break + } } public func startTransferWithQrCode(device: RemoteDeviceInfo) { @@ -339,6 +396,12 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha } public func connectionFailed(with error: Error) { + if case .connecting(let failedID) = transferState, let next = nextUntriedEndpoint() { + print("[quickshare] Endpoint \(failedID) unreachable, trying \(next.id ?? "?")") + connect(to: next) + return + } + resetSendTarget() transferState = .failed(error.localizedDescription) removeStagedShareFiles() DispatchQueue.main.asyncAfter(deadline: .now() + 3) { diff --git a/airsync-mac/Core/WebSocket/WebSocketServer+Handlers.swift b/airsync-mac/Core/WebSocket/WebSocketServer+Handlers.swift index c7421878..4f32f33a 100644 --- a/airsync-mac/Core/WebSocket/WebSocketServer+Handlers.swift +++ b/airsync-mac/Core/WebSocket/WebSocketServer+Handlers.swift @@ -68,7 +68,7 @@ extension WebSocketServer { handleBrowseData(message) case .userDisconnected: handleUserDisconnected() - case .volumeControl, .macVolume, .toggleAppNotif, .browseLs, .wakeUpRequest, .macMediaControlResponse, .macInfo, .callControl: + case .volumeControl, .macVolume, .toggleAppNotif, .browseLs, .wakeUpRequest, .macMediaControlResponse, .macInfo, .callControl, .pong: // Outgoing or unexpected messages break } diff --git a/airsync-mac/Core/WebSocket/WebSocketServer.swift b/airsync-mac/Core/WebSocket/WebSocketServer.swift index 0d8a67d9..28f81eb3 100644 --- a/airsync-mac/Core/WebSocket/WebSocketServer.swift +++ b/airsync-mac/Core/WebSocket/WebSocketServer.swift @@ -185,6 +185,7 @@ class WebSocketServer: ObservableObject { do { let message = try self.jsonDecoder.decode(Message.self, from: data) + if message.type == .pong { return } if message.type == .status { // Pong / keepalive check if let dict = message.data.value as? [String: Any], diff --git a/airsync-mac/Model/Message.swift b/airsync-mac/Model/Message.swift index addbeb1c..39f6bce8 100644 --- a/airsync-mac/Model/Message.swift +++ b/airsync-mac/Model/Message.swift @@ -35,6 +35,7 @@ enum MessageType: String, Codable { case wakeUpRequest // sent by Android right before it closes the socket for a user-initiated disconnect case userDisconnected + case pong // remote control (Mac) case remoteControl case volumeControl // outgoing from Mac (legacy/other direction) From b794e74c8b7c76d5800666268c3367fc58096b3e Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 27 Oct 2026 19:35:00 +0530 Subject: [PATCH 13/14] fix: prioritize reliable endpoints and track unreachable devices in QuickShareManager --- .../Core/QuickShare/QuickShareManager.swift | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/airsync-mac/Core/QuickShare/QuickShareManager.swift b/airsync-mac/Core/QuickShare/QuickShareManager.swift index 4ed9864a..68297471 100644 --- a/airsync-mac/Core/QuickShare/QuickShareManager.swift +++ b/airsync-mac/Core/QuickShare/QuickShareManager.swift @@ -56,6 +56,9 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha private var sendTargetName: String? private var triedEndpointIDs: Set = [] private var autoTargetTask: Task? + // Kept across sends: a stale record keeps getting replayed from the mDNS cache until it expires + private var lastGoodEndpointByName: [String: String] = [:] + private var unreachableEndpointIDs: Set = [] override private init() { super.init() @@ -189,7 +192,21 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha /// Endpoints can appear mid-attempt (the phone re-advertises after our trigger), so look them up fresh. private func nextUntriedEndpoint() -> RemoteDeviceInfo? { guard let name = sendTargetName else { return nil } - return endpointsByName[name]?.last { $0.id.map { !triedEndpointIDs.contains($0) } ?? false } + let untried = (endpointsByName[name] ?? []).filter { $0.id.map { !triedEndpointIDs.contains($0) } ?? false } + return preferredEndpoint(in: untried, named: name) ?? untried.last + } + + /// The endpoint that last worked, else the newest one not known to be dead. + private func preferredEndpoint(in endpoints: [RemoteDeviceInfo], named name: String) -> RemoteDeviceInfo? { + if let good = lastGoodEndpointByName[name], let match = endpoints.first(where: { $0.id == good }) { + return match + } + return endpoints.last { $0.id.map { !unreachableEndpointIDs.contains($0) } ?? false } + } + + private func bestEndpoint(named name: String) -> RemoteDeviceInfo? { + let endpoints = endpointsByName[name] ?? [] + return preferredEndpoint(in: endpoints, named: name) ?? endpoints.last } private func connect(to device: RemoteDeviceInfo) { @@ -337,18 +354,25 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha endpoints.removeAll { $0.id == device.id } endpoints.append(device) endpointsByName[device.name] = endpoints - showNewestEndpoint(named: device.name) + showBestEndpoint(named: device.name) // Wait briefly so a stale cached record doesn't win over the live one if let targetName = autoTargetDeviceName, device.name == targetName, autoTargetTask == nil { print("[quickshare] Auto-targeting found device '\(device.name)', starting transfer") autoTargetTask = Task { @MainActor [weak self] in try? await Task.sleep(nanoseconds: 1_200_000_000) + // If only known-dead records are visible so far, give the live one a few seconds to show up + var waited = 0 + while let self, !Task.isCancelled, waited < 16, + self.preferredEndpoint(in: self.endpointsByName[targetName] ?? [], named: targetName) == nil { + try? await Task.sleep(nanoseconds: 300_000_000) + waited += 1 + } guard let self, !Task.isCancelled, self.transferState == .discovering, - let newest = self.endpointsByName[targetName]?.last else { return } + let target = self.bestEndpoint(named: targetName) else { return } self.autoTargetDeviceName = nil self.autoTargetTask = nil - self.sendFiles(urls: self.transferURLs, to: newest) + self.sendFiles(urls: self.transferURLs, to: target) } } } @@ -359,12 +383,12 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha if endpointsByName[name]?.isEmpty == true { endpointsByName[name] = nil } - showNewestEndpoint(named: name) + showBestEndpoint(named: name) } - private func showNewestEndpoint(named name: String) { + private func showBestEndpoint(named name: String) { let index = discoveredDevices.firstIndex { $0.name == name } - switch (index, endpointsByName[name]?.last) { + switch (index, bestEndpoint(named: name)) { case let (index?, newest?): discoveredDevices[index] = newest case let (nil, newest?): @@ -382,6 +406,10 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha public func connectionWasEstablished(pinCode: String) { let deviceID = caseDiscoveryID() ?? "unknown" + if let name = sendTargetName { + lastGoodEndpointByName[name] = deviceID + unreachableEndpointIDs.remove(deviceID) + } lastPinCode = pinCode transferState = .awaitingPin(pinCode, deviceID) } @@ -396,6 +424,13 @@ public class QuickShareManager: NSObject, ObservableObject, MainAppDelegate, Sha } public func connectionFailed(with error: Error) { + if case .connecting(let failedID) = transferState { + unreachableEndpointIDs.insert(failedID) + if let name = sendTargetName, lastGoodEndpointByName[name] == failedID { + lastGoodEndpointByName[name] = nil + } + showBestEndpoint(named: sendTargetName ?? "") + } if case .connecting(let failedID) = transferState, let next = nextUntriedEndpoint() { print("[quickshare] Endpoint \(failedID) unreachable, trying \(next.id ?? "?")") connect(to: next) From dec21bc7295a60c61ba609a53408e94b360f83b0 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Tue, 27 Oct 2026 19:48:26 +0530 Subject: [PATCH 14/14] refactor: remove unused share extension build configurations and bump project version to 4.2.0 --- AirSync.xcodeproj/project.pbxproj | 115 +++++------------------------- 1 file changed, 18 insertions(+), 97 deletions(-) diff --git a/AirSync.xcodeproj/project.pbxproj b/AirSync.xcodeproj/project.pbxproj index 503b7dce..228fa847 100644 --- a/AirSync.xcodeproj/project.pbxproj +++ b/AirSync.xcodeproj/project.pbxproj @@ -408,85 +408,6 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - A1700000000000000000000C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; - DEVELOPMENT_TEAM = WCB4HTANA6; - ENABLE_APP_SANDBOX = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = AirSyncShareExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = AirSync; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; - PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - A1700000000000000000000D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; - DEVELOPMENT_TEAM = WCB4HTANA6; - ENABLE_APP_SANDBOX = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = AirSyncShareExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = AirSync; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; - PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - A1700000000000000000000E /* Self Compiled */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; - DEVELOPMENT_TEAM = WCB4HTANA6; - ENABLE_APP_SANDBOX = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = AirSyncShareExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = AirSync; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@executable_path/../../../../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; - PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = "Self Compiled"; - }; 11F6093B2EA2A88900AB6BDC /* Self Compiled */ = { isa = XCBuildConfiguration; baseConfigurationReferenceAnchor = B9673B592E35A2A1006D284A /* airsync-mac */; @@ -561,7 +482,7 @@ CODE_SIGN_ENTITLEMENTS = AirSync.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = WCB4HTANA6; ENABLE_APP_SANDBOX = NO; @@ -581,7 +502,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac"; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -609,7 +530,7 @@ AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = WCB4HTANA6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -628,7 +549,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; PRODUCT_NAME = "$(TARGET_NAME)"; RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; @@ -650,7 +571,7 @@ AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = WCB4HTANA6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -669,7 +590,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; PRODUCT_NAME = "$(TARGET_NAME)"; RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; @@ -690,7 +611,7 @@ AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = AirSyncShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = WCB4HTANA6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -709,7 +630,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.share"; PRODUCT_NAME = "$(TARGET_NAME)"; RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; @@ -863,7 +784,7 @@ CODE_SIGN_ENTITLEMENTS = AirSync.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = WCB4HTANA6; ENABLE_APP_SANDBOX = NO; @@ -883,7 +804,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac"; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -916,7 +837,7 @@ CODE_SIGN_ENTITLEMENTS = AirSync.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = WCB4HTANA6; ENABLE_APP_SANDBOX = NO; @@ -936,7 +857,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.5; - MARKETING_VERSION = 4.1.0; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac"; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -964,7 +885,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = AirSyncWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = WCB4HTANA6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -978,7 +899,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 27.0; - MARKETING_VERSION = 4.0.1; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.widget"; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -998,7 +919,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = AirSyncWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = WCB4HTANA6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -1012,7 +933,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 27.0; - MARKETING_VERSION = 4.0.1; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.widget"; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -1032,7 +953,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = AirSyncWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = WCB4HTANA6; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -1046,7 +967,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 27.0; - MARKETING_VERSION = 4.0.1; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = "sameerasw.airsync-mac.widget"; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES;