WIP: Add abiity to receive using iOS app

This commit is contained in:
Leon Böttger committed 2025-08-17 11:46:47 +02:00
1 parent 608ab41c0b
commit 6ec3902788
13 files changed
+473 -214

No files matched your search

@@ -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)
}
@@ -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
+1 -1
View File
@@ -129,7 +129,7 @@ public struct FileMetadata {
}
struct EndpointInfo {
public struct EndpointInfo {
var name: String?
let deviceType: RemoteDeviceInfo.DeviceType
let qrCodeData: Data?
+5
View File
@@ -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
}
+23 -4
View File
@@ -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 = "<group>"; };
EB979F8E2E50CA23008B8C5C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePicker.swift; sourceTree = "<group>"; };
EB979F902E50CA23008B8C5C /* ShareViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewModel.swift; sourceTree = "<group>"; };
EB979F902E50CA23008B8C5C /* SendModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendModel.swift; sourceTree = "<group>"; };
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickDropiOS.entitlements; sourceTree = "<group>"; };
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickDropiOSApp.swift; sourceTree = "<group>"; };
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PieProgressView.swift; sourceTree = "<group>"; };
@@ -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" */;
@@ -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",
+17 -139
View File
@@ -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() {
+141 -34
View File
@@ -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" : {
+10 -2
View File
@@ -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
+17 -15
View File
@@ -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())
}
}
+147 -3
View File
@@ -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)
}
}
+90
View File
@@ -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)
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
import SwiftUI
class ShareViewModel: ObservableObject, OutboundAppDelegate {
class SendModel: ObservableObject, OutboundAppDelegate {
@Published var foundDevices: [RemoteDeviceInfo] = []
@Published var selectedDevice: RemoteDeviceInfo?