From 6ec3902788eb8cd52cdb38e3fbc29af18748b5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6ttger?= Date: Sun, 17 Aug 2025 11:46:47 +0200 Subject: [PATCH] WIP: Add abiity to receive using iOS app --- .../Connection/NearbyConnectionManager.swift | 21 +-- .../Connection/OutboundNearbyConnection.swift | 2 +- NearbyShare/Model/DataModel.swift | 2 +- NearbyShare/Util/Helpers.swift | 5 + QuickDrop.xcodeproj/project.pbxproj | 27 ++- .../xcshareddata/swiftpm/Package.resolved | 13 +- QuickDrop/AppDelegate.swift | 156 ++-------------- QuickDrop/Localizable.xcstrings | 175 ++++++++++++++---- QuickDrop/Util/ErrorAlertHandler.swift | 12 +- QuickDropiOS/ContentView.swift | 32 ++-- QuickDropiOS/ReceiveModel.swift | 150 ++++++++++++++- QuickDropiOS/SendModel.swift | 90 +++++++++ QuickDropiOS/ShareViewModel.swift | 2 +- 13 files changed, 473 insertions(+), 214 deletions(-) create mode 100644 QuickDropiOS/SendModel.swift diff --git a/NearbyShare/Connection/NearbyConnectionManager.swift b/NearbyShare/Connection/NearbyConnectionManager.swift index 15edd10..32aa484 100644 --- a/NearbyShare/Connection/NearbyConnectionManager.swift +++ b/NearbyShare/Connection/NearbyConnectionManager.swift @@ -7,6 +7,7 @@ #if os(iOS) import UIKit +import RNDeviceName #endif import Foundation @@ -31,12 +32,19 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb private var qrCodeAdvertisingToken: Data? private var qrCodeNameEncryptionKey: SymmetricKey? + public let deviceInfo: EndpointInfo public let endpointID: [UInt8] = generateEndpointID() public var mainAppDelegate: (any InboundAppDelegate)? public static let shared = NearbyConnectionManager() override init() { +#if os(macOS) + self.deviceInfo = EndpointInfo(name: Host.current().localizedName ?? "Mac", deviceType: .computer) +#else + self.deviceInfo = EndpointInfo(name: UIDevice.current.marketingName, deviceType: .phone) +#endif + tcpListener = try! NWListener(using: NWParameters(tls: .none)) super.init() } @@ -104,7 +112,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb let service = NetService(domain: "", type: serviceType, name: name, port: port) service.delegate = self service.setTXTRecord(NetService.data(fromTXTRecord: [ - "n": getEndpointInfo().serialize().urlSafeBase64EncodedString().data(using: .utf8)!, + "n": deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!, ])) service.publish() return service @@ -112,17 +120,6 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb } - func getEndpointInfo() -> EndpointInfo { -#if os(macOS) - let endpointInfo = EndpointInfo(name: Host.current().localizedName ?? "Mac", deviceType: .computer) -#else - let endpointInfo = EndpointInfo(name: UIDevice.current.name, deviceType: .phone) -#endif - - return endpointInfo - } - - func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection _: InboundNearbyConnection) { mainAppDelegate?.obtainUserConsent(for: transfer, from: device) } diff --git a/NearbyShare/Connection/OutboundNearbyConnection.swift b/NearbyShare/Connection/OutboundNearbyConnection.swift index 6ac0fbf..54fdf89 100644 --- a/NearbyShare/Connection/OutboundNearbyConnection.swift +++ b/NearbyShare/Connection/OutboundNearbyConnection.swift @@ -167,7 +167,7 @@ class OutboundNearbyConnection: NearbyConnection { private func sendConnectionRequest() throws { - let endpointInfo = NearbyConnectionManager.shared.getEndpointInfo() + let endpointInfo = NearbyConnectionManager.shared.deviceInfo var frame = Location_Nearby_Connections_OfflineFrame() frame.version = .v1 diff --git a/NearbyShare/Model/DataModel.swift b/NearbyShare/Model/DataModel.swift index 7aad1cd..842a68a 100644 --- a/NearbyShare/Model/DataModel.swift +++ b/NearbyShare/Model/DataModel.swift @@ -129,7 +129,7 @@ public struct FileMetadata { } -struct EndpointInfo { +public struct EndpointInfo { var name: String? let deviceType: RemoteDeviceInfo.DeviceType let qrCodeData: Data? diff --git a/NearbyShare/Util/Helpers.swift b/NearbyShare/Util/Helpers.swift index 2b1fb23..a0eed2e 100644 --- a/NearbyShare/Util/Helpers.swift +++ b/NearbyShare/Util/Helpers.swift @@ -105,8 +105,13 @@ public enum UserDefaultsKeys: String, CaseIterable { public func isPlusVersion() -> Bool { + #if os(macOS) // Enable full functionality if app distributed directly return DistributionDetector.isDirectDistributionEnabled || UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue) + #else + // iOS + return true + #endif } diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index 9e1230e..457d786 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -105,10 +105,12 @@ EB979F942E50CA23008B8C5C /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; }; EB979F952E50CA23008B8C5C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8E2E50CA23008B8C5C /* ContentView.swift */; }; EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8F2E50CA23008B8C5C /* FilePicker.swift */; }; - EB979F972E50CA23008B8C5C /* ShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F902E50CA23008B8C5C /* ShareViewModel.swift */; }; + EB979F972E50CA23008B8C5C /* SendModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F902E50CA23008B8C5C /* SendModel.swift */; }; EB979F982E50CA23008B8C5C /* QuickDropiOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */; }; EB979F9A2E50CAA6008B8C5C /* PieProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F992E50CAA6008B8C5C /* PieProgressView.swift */; }; EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A00E2E511120008B8C5C /* ReceiveModel.swift */; }; + EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A00E2E511120008B8C5C /* ReceiveModel.swift */; }; + EB97A0132E51D6B2008B8C5C /* RNDeviceName in Frameworks */ = {isa = PBXBuildFile; productRef = EB97A0122E51D6B2008B8C5C /* RNDeviceName */; }; EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; }; EB9925812E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; }; EB9925822E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; }; @@ -239,7 +241,7 @@ EB979F8D2E50CA23008B8C5C /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; EB979F8E2E50CA23008B8C5C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; EB979F8F2E50CA23008B8C5C /* FilePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePicker.swift; sourceTree = ""; }; - EB979F902E50CA23008B8C5C /* ShareViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewModel.swift; sourceTree = ""; }; + EB979F902E50CA23008B8C5C /* SendModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendModel.swift; sourceTree = ""; }; EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickDropiOS.entitlements; sourceTree = ""; }; EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickDropiOSApp.swift; sourceTree = ""; }; EB979F992E50CAA6008B8C5C /* PieProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PieProgressView.swift; sourceTree = ""; }; @@ -287,6 +289,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EB97A0132E51D6B2008B8C5C /* RNDeviceName in Frameworks */, EB5C37F02E377C3F0052EFD7 /* SwiftECC in Frameworks */, EB886E582E4E66420052B7D9 /* LUI in Frameworks */, EB5C37EE2E377C3F0052EFD7 /* SwiftProtobuf in Frameworks */, @@ -438,7 +441,7 @@ EB979F8D2E50CA23008B8C5C /* Configuration.swift */, EB979F8E2E50CA23008B8C5C /* ContentView.swift */, EB979F8F2E50CA23008B8C5C /* FilePicker.swift */, - EB979F902E50CA23008B8C5C /* ShareViewModel.swift */, + EB979F902E50CA23008B8C5C /* SendModel.swift */, EB97A00E2E511120008B8C5C /* ReceiveModel.swift */, EB979F992E50CAA6008B8C5C /* PieProgressView.swift */, EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */, @@ -578,6 +581,7 @@ EB5C37EF2E377C3F0052EFD7 /* SwiftECC */, EB9925882E45F13500357055 /* QRCode */, EB886E572E4E66420052B7D9 /* LUI */, + EB97A0122E51D6B2008B8C5C /* RNDeviceName */, ); productName = QuickDropiOS; productReference = EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */; @@ -636,6 +640,7 @@ EB2F9BFC2DA05F48004010A2 /* XCRemoteSwiftPackageReference "BezelNotification" */, EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */, EB886E562E4E66420052B7D9 /* XCRemoteSwiftPackageReference "LUI" */, + EB97A0112E51D6B2008B8C5C /* XCRemoteSwiftPackageReference "RNDeviceName" */, ); productRefGroup = 69DA9A0F29E0BF5100A442DA /* Products */; projectDirPath = ""; @@ -747,6 +752,7 @@ EB5C37C22E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */, EB5C37C32E377A780052EFD7 /* wire_format.pb.swift in Sources */, EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */, + EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */, EBC05C782E33B58100EECC18 /* ActiveNetworkInterface.swift in Sources */, EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */, EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */, @@ -803,7 +809,7 @@ EB979F952E50CA23008B8C5C /* ContentView.swift in Sources */, EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */, EB979F9A2E50CAA6008B8C5C /* PieProgressView.swift in Sources */, - EB979F972E50CA23008B8C5C /* ShareViewModel.swift in Sources */, + EB979F972E50CA23008B8C5C /* SendModel.swift in Sources */, EB979F982E50CA23008B8C5C /* QuickDropiOSApp.swift in Sources */, EB5C38402E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */, EB5C37FF2E377CE70052EFD7 /* wire_format.pb.swift in Sources */, @@ -1264,6 +1270,14 @@ kind = branch; }; }; + EB97A0112E51D6B2008B8C5C /* XCRemoteSwiftPackageReference "RNDeviceName" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/souzainf3/RNDeviceName"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.1.0; + }; + }; EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/dagronf/qrcode.git"; @@ -1315,6 +1329,11 @@ package = EB886E562E4E66420052B7D9 /* XCRemoteSwiftPackageReference "LUI" */; productName = LUI; }; + EB97A0122E51D6B2008B8C5C /* RNDeviceName */ = { + isa = XCSwiftPackageProductDependency; + package = EB97A0112E51D6B2008B8C5C /* XCRemoteSwiftPackageReference "RNDeviceName" */; + productName = RNDeviceName; + }; EB9925842E45EF0900357055 /* QRCode */ = { isa = XCSwiftPackageProductDependency; package = EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */; diff --git a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e5087f9..799e8bd 100644 --- a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "053324b22e9d13d48c4bf9290d746eb2c1a2e80e5900f9dde545bb150c5eff6a", + "originHash" : "6596056df2297c97fbb8a90f8b23b4feec22b8d0fbdb59f154e1c543f10c631c", "pins" : [ { "identity" : "applewatchscreensize", @@ -43,7 +43,7 @@ "location" : "https://github.com/leonboe1/LUI.git", "state" : { "branch" : "main", - "revision" : "49e61e6ff710a1548e352d4179fceec785849b1b" + "revision" : "b4a9766db0f328c8251b1fa8bed2472c783d7128" } }, { @@ -55,6 +55,15 @@ "version" : "26.10.0" } }, + { + "identity" : "rndevicename", + "kind" : "remoteSourceControl", + "location" : "https://github.com/souzainf3/RNDeviceName", + "state" : { + "revision" : "68ada02a7135381425b01778aeb8719ce3bf8f62", + "version" : "2.1.0" + } + }, { "identity" : "swift-cross-kit-types", "kind" : "remoteSourceControl", diff --git a/QuickDrop/AppDelegate.swift b/QuickDrop/AppDelegate.swift index 30ed0b3..a9fe868 100644 --- a/QuickDrop/AppDelegate.swift +++ b/QuickDrop/AppDelegate.swift @@ -14,7 +14,7 @@ import SwiftUI import UserNotifications @main -class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, NSWindowDelegate, InboundAppDelegate { +class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, NSWindowDelegate { private var statusItem: NSStatusItem? @@ -26,6 +26,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele private var errorAlertHandler = ErrorAlertHandler.shared private var iapManager: IAPManager? + private var receiveModel: ReceiveModel? var showsFirewallAlert = false var visibleItem: NSMenuItem? = nil @@ -64,9 +65,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele statusItem?.menu = menu statusItem?.behavior = .removalAllowed - NearbyConnectionManager.shared.mainAppDelegate = self - NearbyConnectionManager.shared.becomeVisible() - iapManager = IAPManager.sharedInstance iapManager?.startObserving() @@ -111,6 +109,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele let queue2 = DispatchQueue(label: "NetworkConnectionMonitor") hasConnectionMonitor.start(queue: queue2) + + receiveModel = ReceiveModel(controlPlusScreen: { shouldOpen in + if shouldOpen { + self.openPlusScreen() + } + else { + if let window = self.plusWindow { + log("[AppDelegate] Closing plus screen because of error") + window.close() + } + } + }) } @@ -231,8 +241,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele if let window = plusWindow, window.isVisible { NSApp.activate(ignoringOtherApps: true) window.makeKeyAndOrderFront(nil) + log("Plus screen already open, bringing to front") return } + + log("Opening Plus screen") // Create the welcome screen SwiftUI view let plusView = PlusView(closeView: { @@ -306,141 +319,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele } - // MARK: - Alerts and Notifications - - - func showCopiedToClipboardAlert() { - DispatchQueue.main.async { - BezelNotification.show(messageText: "InsertedIntoClipboard".localized(), icon: .clipboard) - } - } - - - // MARK: - Transfer Handling - - public func continueTransmission(accept: Bool, transferID: String, storeInTemp: Bool = false) { - NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: accept, storeInTemp: storeInTemp) - } - - - func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo) { - - NSApp.activate(ignoringOtherApps: true) - AudioManager.playIncomingFileSound() - - let acceptAutomatically = UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue) - - let fileStr: String - - if let textTitle = transfer.textDescription { - fileStr = textTitle - } else if transfer.files.count == 1 { - fileStr = transfer.files[0].name - } else { - fileStr = String.localizedStringWithFormat("NFiles".localized(), transfer.files.count) - } - - - let mainMessage: String - let name = device.name ?? "Android" - - switch transfer.type { - case .file: - mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [name, fileStr]) - - case .text: - mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [name, fileStr]) - - case .url: - mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [name, fileStr]) - } - - - let pinCodeMessage = String(format: "PinCode".localized(), arguments: [transfer.pinCode ?? "?"]) - let transferID = transfer.id - - if acceptAutomatically { - pressAcceptButton(transferID: transfer.id) - - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in - if granted { - log("User granted notification permissions") - - let notificationContent = UNMutableNotificationContent() - notificationContent.title = "QuickDrop" - notificationContent.body = mainMessage - notificationContent.sound = nil - let notificationId = UUID().uuidString - - let notificationReq = UNNotificationRequest(identifier: notificationId, content: notificationContent, trigger: nil) - UNUserNotificationCenter.current().add(notificationReq) - - DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId]) - } - } - } - } else { - let alert = NSAlert() - alert.alertStyle = .informational - - alert.messageText = "QuickDrop - \(pinCodeMessage)" - alert.informativeText = mainMessage - alert.addButton(withTitle: "Accept".localized()) - alert.addButton(withTitle: "Decline".localized()) - - let result = alert.runModal() - - if result == .alertFirstButtonReturn { - pressAcceptButton(transferID: transferID) - } else if result == .alertSecondButtonReturn { - continueTransmission(accept: false, transferID: transferID) - } - } - } - - - private func pressAcceptButton(transferID: String) { - if isFileTransferRestricted() { - continueTransmission(accept: true, transferID: transferID, storeInTemp: true) - log("Showing plus screen...") - - openPlusScreen() - } else { - continueTransmission(accept: true, transferID: transferID) - } - } - - - func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?) { - - if let error = error { - - if let plusWindow = plusWindow { - log("Closing plus screen because of error") - plusWindow.close() - } - - ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error) - } else { - - if wasPlainTextTransfer { - showCopiedToClipboardAlert() - } - - let currentCount = incomingTransmissionCount() - if currentCount == 0 { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - SKStoreReviewController.requestReview() - } - } - - UserDefaults.standard.set(currentCount + 1, forKey: UserDefaultsKeys.transmissionCount.rawValue) - log("Successful transmission. Current count: \(currentCount)") - } - } - - // MARK: - Helper Functions private func performDeviceToDeviceCheck() { diff --git a/QuickDrop/Localizable.xcstrings b/QuickDrop/Localizable.xcstrings index cd83d37..361d57c 100644 --- a/QuickDrop/Localizable.xcstrings +++ b/QuickDrop/Localizable.xcstrings @@ -2892,103 +2892,103 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يرغب %1$@ في إدراج \"%2$@\" في حافظة جهاز Mac الخاص بك." + "value" : "%1$@ يريد إدراج \"%2$@\" في الحافظة الخاصة بك." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ möchte \"%2$@\" in die Zwischenablage deines Macs einfügen." + "value" : "%1$@ möchte \"%2$@\" in Deine Zwischenablage einfügen." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to insert \"%2$@\" to your Mac's clipboard." + "value" : "%1$@ wants to insert \"%2$@\" to your clipboard." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quiere insertar \"%2$@\" en el portapapeles de tu Mac." + "value" : "%1$@ quiere insertar \"%2$@\" en tu portapapeles." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ souhaite insérer \"%2$@\" dans le presse-papiers de votre Mac." + "value" : "%1$@ souhaite insérer \"%2$@\" dans votre presse-papiers." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ be szeretné illeszteni a(z) \"%2$@\" szöveget a Mac-ed vágólapjára." + "value" : "%1$@ be szeretné illeszteni a(z) \"%2$@\" elemet a vágólapodra." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vuole inserire \"%2$@\" negli appunti del tuo Mac." + "value" : "%1$@ vuole inserire \"%2$@\" negli appunti." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ が \"%2$@\" をあなたの Mac のクリップボードに挿入しようとしています。" + "value" : "%1$@があなたのクリップボードに「%2$@」を挿入しようとしています。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@이(가) \"%2$@\"을(를) Mac의 클립보드에 삽입하려고 합니다." + "value" : "%1$@님이 클립보드에 \"%2$@\"을(를) 삽입하려고 합니다." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wil \"%2$@\" in het klembord van je Mac plaatsen." + "value" : "%1$@ wil \"%2$@\" naar uw klembord kopiëren." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ deseja inserir \"%2$@\" na área de transferência do seu Mac." + "value" : "%1$@ deseja inserir \"%2$@\" na sua área de transferência." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ pretende inserir \"%2$@\" na área de transferência do seu Mac." + "value" : "%1$@ quer inserir \"%2$@\" na sua área de transferência." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ dorește să introducă \"%2$@\" în clipboard-ul Mac-ului tău." + "value" : "%1$@ dorește să insereze \"%2$@\" în clipboard-ul tău." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хочет вставить \"%2$@\" в буфер обмена вашего Mac." + "value" : "%1$@ хочет вставить \"%2$@\" в ваш буфер обмена." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ต้องการแทรก \"%2$@\" ไปยังคลิปบอร์ดของ Mac ของคุณ" + "value" : "%1$@ ต้องการเพิ่ม \"%2$@\" ไปยังคลิปบอร์ดของคุณ" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хоче вставити \"%2$@\" до буфера обміну вашого Mac." + "value" : "%1$@ хоче вставити \"%2$@\" у ваш буфер обміну." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 想要将 \"%2$@\" 插入到你的 Mac 剪贴板。" + "value" : "%1$@ 想将“%2$@”插入到你的剪贴板。" } } } @@ -2999,103 +2999,103 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يرغب %1$@ في فتح \"%2$@\" على جهاز Mac الخاص بك." + "value" : "%1$@ يريد فتح \"%2$@\" على هذا الجهاز." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ möchte \"%2$@\" auf deinem Mac öffnen." + "value" : "%1$@ möchte \"%2$@\" auf diesem Gerät öffnen." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to open \"%2$@\" on your Mac." + "value" : "%1$@ wants to open \"%2$@\" on this device." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quiere abrir \"%2$@\" en tu Mac." + "value" : "%1$@ quiere abrir \"%2$@\" en este dispositivo." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ souhaite ouvrir \"%2$@\" sur votre Mac." + "value" : "%1$@ souhaite ouvrir \"%2$@\" sur cet appareil." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ meg szeretné nyitni a(z) \"%2$@\" hivatkozást a Mac-eden." + "value" : "%1$@ meg akarja nyitni a(z) \"%2$@\" elemet ezen az eszközön." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vuole aprire \"%2$@\" sul tuo Mac." + "value" : "%1$@ vuole aprire \"%2$@\" su questo dispositivo." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ があなたの Mac で \"%2$@\" を開こうとしています。" + "value" : "%1$@がこのデバイスで「%2$@」を開こうとしています。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@이(가) Mac에서 \"%2$@\"을(를) 열려고 합니다." + "value" : "%1$@님이 이 장치에서 \"%2$@\"을(를) 열려고 합니다." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wil \"%2$@\" openen op je Mac." + "value" : "%1$@ wil \"%2$@\" openen op dit apparaat." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ deseja abrir \"%2$@\" no seu Mac." + "value" : "%1$@ deseja abrir \"%2$@\" neste dispositivo." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ pretende abrir \"%2$@\" no seu Mac." + "value" : "%1$@ quer abrir \"%2$@\" neste dispositivo." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ dorește să deschidă \"%2$@\" pe Mac-ul tău." + "value" : "%1$@ dorește să deschidă \"%2$@\" pe acest dispozitiv." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хочет открыть \"%2$@\" на вашем Mac." + "value" : "%1$@ хочет открыть \"%2$@\" на этом устройстве." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ต้องการเปิด \"%2$@\" บน Mac ของคุณ" + "value" : "%1$@ ต้องการเปิด \"%2$@\" บนอุปกรณ์นี้" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хоче відкрити \"%2$@\" на вашому Mac." + "value" : "%1$@ хоче відкрити \"%2$@\" на цьому пристрої." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 想要在你的 Mac 上打开 \"%2$@\"。" + "value" : "%1$@ 想在此设备上打开“%2$@”。" } } } @@ -9622,6 +9622,113 @@ } } }, + "Unknown" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "غير معروف" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unbekannt" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unknown" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconocido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inconnu" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ismeretlen" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sconosciuto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "알 수 없음" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onbekend" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconhecido" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desconhecido" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Necunoscut" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неизвестно" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไม่ทราบ" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Невідомо" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未知" + } + } + } + }, "UnsupportedType" : { "extractionState" : "manual", "localizations" : { diff --git a/QuickDrop/Util/ErrorAlertHandler.swift b/QuickDrop/Util/ErrorAlertHandler.swift index 0cc7df6..c0f2d36 100644 --- a/QuickDrop/Util/ErrorAlertHandler.swift +++ b/QuickDrop/Util/ErrorAlertHandler.swift @@ -87,13 +87,21 @@ class ErrorAlertHandler { log("Unsuccessful transmission. Already successful transmissions: \(incomingTransmissionCount())") #if os(macOS) + + let primaryButtonTitle = "InformDeveloper".localized() + let primaryButtonAction: () -> Void = { + log("Sending logging string") + sendLoggingString() + } + let secondaryButtonTitle = "CloseAlert".localized() + self.isAlertShown = true let alert = NSAlert() alert.alertStyle = .critical alert.messageText = title alert.informativeText = description - alert.addButton(withTitle: "InformDeveloper".localized()) - alert.addButton(withTitle: "CloseAlert".localized()) + alert.addButton(withTitle: primaryButtonTitle) + alert.addButton(withTitle: secondaryButtonTitle) let result = alert.runModal() self.isAlertShown = false diff --git a/QuickDropiOS/ContentView.swift b/QuickDropiOS/ContentView.swift index 8ba2cc1..fe998eb 100644 --- a/QuickDropiOS/ContentView.swift +++ b/QuickDropiOS/ContentView.swift @@ -10,11 +10,12 @@ import LUI struct ContentView: View { - @StateObject var model = ShareViewModel() + @StateObject var sendModel = SendModel() + @StateObject var receiveModel = ReceiveModel() var body: some View { AppRootView(isPlus: .constant(true), phoneView: { - DeviceListView(model: model) + DeviceListView(sendModel: sendModel, receiveModel: receiveModel) }, settingsView: { EmptyView() }) @@ -23,7 +24,8 @@ struct ContentView: View { struct DeviceListView: View { - @ObservedObject var model: ShareViewModel + @ObservedObject var sendModel: SendModel + @ObservedObject var receiveModel: ReceiveModel var body: some View { @@ -33,7 +35,7 @@ struct DeviceListView: View { VStack(alignment: .leading, spacing: 8) { FooterView(text: "YouAreVisibleAs") - Text(NearbyConnectionManager.shared.getEndpointInfo().name ?? "QuickDrop") + Text(NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized()) .fontWeight(.bold) .padding(.horizontal, 16) .padding(.vertical, 6) @@ -45,7 +47,7 @@ struct DeviceListView: View { .frame(maxWidth: .infinity, alignment: .leading) - if model.foundDevices.isEmpty { + if sendModel.foundDevices.isEmpty { CustomSection(header: "NoDevicesFound") { @@ -61,9 +63,9 @@ struct DeviceListView: View { } else { CustomSection(header: "AvailableDevices") { - ForEach(model.foundDevices) { device in + ForEach(sendModel.foundDevices) { device in - let isSelected = model.selectedDevice == device + let isSelected = sendModel.selectedDevice == device SendPickerButton { HStack(spacing: 12) { @@ -92,14 +94,14 @@ struct DeviceListView: View { .fontWeight(.medium) .foregroundColor(Color.primary) - Text(isSelected ? model.progressState ?? "..." : "Available".localized()) + Text(isSelected ? sendModel.progressState ?? "..." : "Available".localized()) .font(.system(size: 12)) .foregroundColor(Color.primary.opacity(0.6)) } Spacer() - if isSelected, let progress = model.progressValue { + if isSelected, let progress = sendModel.progressValue { PieProgressView(progress: progress, size: 20) } else { @@ -109,11 +111,11 @@ struct DeviceListView: View { } .padding(.vertical, 13) } onResult: { urls, text in - model.urls = urls ?? [] - model.textToSend = text - model.selectDevice(device: device) + sendModel.urls = urls ?? [] + sendModel.textToSend = text + sendModel.selectDevice(device: device) } - .animation(.easeInOut, value: model.progressValue) + .animation(.easeInOut, value: sendModel.progressValue) .animation(.easeInOut, value: isSelected) } } @@ -135,14 +137,14 @@ struct DeviceListView: View { extension DeviceListView { static var preview: DeviceListView { - let model = ShareViewModel() + let model = SendModel() model.foundDevices = [ RemoteDeviceInfo(name: "MacBook Pro", type: .computer, id: "macbook-pro-id"), RemoteDeviceInfo(name: "iPhone 14", type: .phone, id: "iphone-14-id"), RemoteDeviceInfo(name: "Samsung Galaxy S21", type: .phone, id: "samsung-galaxy-s21-id") ] - return DeviceListView(model: model) + return DeviceListView(sendModel: model, receiveModel: ReceiveModel()) } } diff --git a/QuickDropiOS/ReceiveModel.swift b/QuickDropiOS/ReceiveModel.swift index 9232613..e56b7c5 100644 --- a/QuickDropiOS/ReceiveModel.swift +++ b/QuickDropiOS/ReceiveModel.swift @@ -6,20 +6,164 @@ // import SwiftUI +import StoreKit +import UserNotifications + +#if os(macOS) +import BezelNotification +#else +import LUI +#endif class ReceiveModel: ObservableObject, InboundAppDelegate { + let controlPlusScreen: (Bool) -> Void - init() { - + init(controlPlusScreen: @escaping (Bool) -> Void = { _ in }) { + self.controlPlusScreen = controlPlusScreen + NearbyConnectionManager.shared.mainAppDelegate = self + NearbyConnectionManager.shared.becomeVisible() } func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo) { + #if os(macOS) + NSApp.activate(ignoringOtherApps: true) + AudioManager.playIncomingFileSound() + #endif + + let acceptAutomatically = UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue) + + let fileStr: String + + if let textTitle = transfer.textDescription { + fileStr = textTitle + } else if transfer.files.count == 1 { + fileStr = transfer.files[0].name + } else { + fileStr = String.localizedStringWithFormat("NFiles".localized(), transfer.files.count) + } + + + let mainMessage: String + let name = device.name ?? "Android" + + switch transfer.type { + case .file: + mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [name, fileStr]) + + case .text: + mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [name, fileStr]) + + case .url: + mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [name, fileStr]) + } + + + let pinCodeMessage = String(format: "PinCode".localized(), arguments: [transfer.pinCode ?? "?"]) + let transferID = transfer.id + + if acceptAutomatically { + pressAcceptButton(transferID: transfer.id) + + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in + if granted { + log("[ReceiveModel] User granted notification permissions") + + let notificationContent = UNMutableNotificationContent() + notificationContent.title = "QuickDrop" + notificationContent.body = mainMessage + notificationContent.sound = nil + let notificationId = UUID().uuidString + + let notificationReq = UNNotificationRequest(identifier: notificationId, content: notificationContent, trigger: nil) + UNUserNotificationCenter.current().add(notificationReq) + + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId]) + } + } + } + } else { + + let title = "QuickDrop - \(pinCodeMessage)" + let primaryButtonTitle = "Accept".localized() + let primaryButtonAction = { self.pressAcceptButton(transferID: transferID) } + let secondaryButtonTitle = "Decline".localized() + let secondaryButtonAction = { self.continueTransmission(accept: false, transferID: transferID) } + + #if os(macOS) + let alert = NSAlert() + alert.alertStyle = .informational + + alert.messageText = title + alert.informativeText = mainMessage + alert.addButton(withTitle: primaryButtonTitle) + alert.addButton(withTitle: secondaryButtonTitle) + + let result = alert.runModal() + + if result == .alertFirstButtonReturn { + primaryButtonAction() + } else if result == .alertSecondButtonReturn { + secondaryButtonAction() + } + #else + // iOS + showAlert(title: title, message: mainMessage, primaryButton: LUIAlertButton(title: secondaryButtonTitle, action: secondaryButtonAction), secondaryButton: LUIAlertButton(title: primaryButtonTitle, action: primaryButtonAction)) + #endif + } } + func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: (any Error)?) { - + if let error = error { + controlPlusScreen(false) + ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error) + } else { + + if wasPlainTextTransfer { + showCopiedToClipboardAlert() + } + + let currentCount = incomingTransmissionCount() + if currentCount == 0 { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + SKStoreReviewController.requestReview() + } + } + + UserDefaults.standard.set(currentCount + 1, forKey: UserDefaultsKeys.transmissionCount.rawValue) + log("[ReceiveModel] Successful transmission. Current count: \(currentCount)") + } + } + + + private func showCopiedToClipboardAlert() { + #if os(macOS) + DispatchQueue.main.async { + BezelNotification.show(messageText: "InsertedIntoClipboard".localized(), icon: .clipboard) + } + #else + // iOS + doubleVibration() + #endif + } + + + private func pressAcceptButton(transferID: String) { + if isFileTransferRestricted() { + continueTransmission(accept: true, transferID: transferID, storeInTemp: true) + log("[ReceiveModel] Showing plus screen...") + controlPlusScreen(true) + } else { + continueTransmission(accept: true, transferID: transferID) + } + } + + + private func continueTransmission(accept: Bool, transferID: String, storeInTemp: Bool = false) { + NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: accept, storeInTemp: storeInTemp) } } diff --git a/QuickDropiOS/SendModel.swift b/QuickDropiOS/SendModel.swift new file mode 100644 index 0000000..dfd7ecc --- /dev/null +++ b/QuickDropiOS/SendModel.swift @@ -0,0 +1,90 @@ +// +// Model.swift +// QuickDrop +// +// Created by Leon Böttger on 28.07.25. +// + +import SwiftUI + +class SendModel: ObservableObject, OutboundAppDelegate { + + @Published var foundDevices: [RemoteDeviceInfo] = [] + @Published var selectedDevice: RemoteDeviceInfo? + + @Published var progressState: String? = nil + @Published var progressValue: Double? = nil + + var urls: [URL] = [] + var textToSend: String? = nil + + private var connectionEstablished = false + private var timeoutDispatchWorkItem: DispatchWorkItem? = nil + + init() { + NearbyConnectionManager.shared.startDeviceDiscovery() + NearbyConnectionManager.shared.becomeVisible() + NearbyConnectionManager.shared.addShareExtensionDelegate(self) + } + + func addDevice(device: RemoteDeviceInfo) { + withAnimation { + foundDevices.append(device) + } + } + + func removeDevice(id: String) { + withAnimation { + foundDevices.removeAll { $0.id == id } + } + } + + func connectionWasEstablished(pinCode: String) { + connectionEstablished = true + progressState = String(format: "PinCode".localized(), arguments: [pinCode]) + progressValue = 0 + } + + func connectionFailed(with error: any Error) { + + ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "?", error: error) + + progressValue = nil + selectedDevice = nil + } + + func transferAccepted() { + progressState = "Sending".localized() + } + + func transferProgress(progress: Double) { + progressValue = progress + } + + func transferFinished() { + progressValue = 0 + selectedDevice = nil + progressState = "TransferFinished".localized() + } + + func startTransferWithQrCode(device: RemoteDeviceInfo) { + selectDevice(device: device) + } + + func selectDevice(device: RemoteDeviceInfo) { + + // already selected, cancel transfer + if device == selectedDevice { + progressValue = 0 + progressState = nil + selectedDevice = nil + NearbyConnectionManager.shared.cancelOutgoingTransfer(id: device.id!) + } + else { + progressValue = 0 + progressState = "Connecting".localized() + selectedDevice = device + NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: urls, textToSend: textToSend) + } + } +} diff --git a/QuickDropiOS/ShareViewModel.swift b/QuickDropiOS/ShareViewModel.swift index 9ea0f0d..dfd7ecc 100644 --- a/QuickDropiOS/ShareViewModel.swift +++ b/QuickDropiOS/ShareViewModel.swift @@ -7,7 +7,7 @@ import SwiftUI -class ShareViewModel: ObservableObject, OutboundAppDelegate { +class SendModel: ObservableObject, OutboundAppDelegate { @Published var foundDevices: [RemoteDeviceInfo] = [] @Published var selectedDevice: RemoteDeviceInfo?