Add device name lookup for unnamed devices

This commit is contained in:
Leon Böttger committed 2026-04-27 20:29:35 +02:00
1 parent 84d2150893
commit e6d1c7b1a3
9 files changed
+397 -24

No files matched your search

@@ -36,6 +36,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
private var startedDeviceDiscovery = false
private var startedAdvertising = false { didSet { informAboutStatus() }}
private var browsers: [NWBrowser] = []
private let deviceNameResolver = NearbyDeviceNameResolver()
private let serviceTypes = ["_FC9F5ED42C8A._tcp."]
private var qrCodePrivateKey: ECPrivateKey?
private var qrCodeAdvertisingToken: Data?
@@ -315,11 +316,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
var txtRecord: [String: Data] = [
txtKeyEndpointInfo: deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!,
]
#if os(macOS)
txtRecord[txtQuickDropCapabilities] = Data(notificationSyncCapabilityToken.utf8)
if let keyFingerprint = notificationSyncKeyFingerprintHex() {
txtRecord[txtQuickDropDeviceKeyFingerprint] = Data(keyFingerprint.utf8)
}
#if os(macOS)
txtRecord[txtQuickDropCapabilities] = Data(notificationSyncCapabilityToken.utf8)
#endif
service.setTXTRecord(NetService.data(fromTXTRecord: txtRecord))
service.publish()
@@ -473,7 +474,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
browsers.removeAll()
startedDeviceDiscovery = false
deviceNameResolver.cancelAll()
for delegate in outboundAppDelegates {
@@ -552,14 +553,40 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return }
guard let endpointInfoEncoded = txtRecord.dictionary[txtKeyEndpointInfo] else { return }
guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return }
var endpointInfo = EndpointInfo(data: endpointInfoData)
let parsedEndpointInfo = EndpointInfo(data: endpointInfoData)
var endpointInfo = parsedEndpointInfo
?? EndpointInfo(name: unnamedAndroidFallbackDeviceName, deviceType: .unknown)
let cleanedName = endpointInfo.name?.trimmingCharacters(in: .whitespacesAndNewlines)
if cleanedName?.isEmpty != false {
let isQuickDropPeer = txtRecord.dictionary[txtQuickDropDeviceKeyFingerprint] != nil
let needsDeviceNameLookup = deviceNameResolver.needsLookup(for: parsedEndpointInfo)
if needsDeviceNameLookup {
endpointInfo.name = unnamedAndroidFallbackDeviceName
}
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
let deviceInfo = addFoundDevice(
foundService: &foundService,
endpointInfo: endpointInfo,
endpointID: endpointID,
isQuickDropPeer: isQuickDropPeer
)
if needsDeviceNameLookup {
deviceNameResolver.resolveName(for: service, endpointID: endpointID) { [weak self] deviceName in
guard let self else { return }
guard self.foundServices[endpointID]?.device?.name == self.unnamedAndroidFallbackDeviceName else { return }
var resolvedEndpointInfo = endpointInfo
resolvedEndpointInfo.name = deviceName
var resolvedFoundService = FoundServiceInfo(service: service)
_ = self.addFoundDevice(
foundService: &resolvedFoundService,
endpointInfo: resolvedEndpointInfo,
endpointID: endpointID,
isQuickDropPeer: isQuickDropPeer
)
}
}
else {
deviceNameResolver.cancelLookup(for: endpointID)
}
if let qrData = endpointInfo.qrCodeData, let qrCodeAdvertisingToken = qrCodeAdvertisingToken, let qrCodeNameEncryptionKey = qrCodeNameEncryptionKey {
@@ -576,7 +603,12 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
let decryptedName = try AES.GCM.open(box, using: qrCodeNameEncryptionKey, authenticating: qrCodeAdvertisingToken)
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
endpointInfo.name = name
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
let deviceInfo = addFoundDevice(
foundService: &foundService,
endpointInfo: endpointInfo,
endpointID: endpointID,
isQuickDropPeer: isQuickDropPeer
)
for delegate in outboundAppDelegates {
delegate.startTransferWithQrCode(device: deviceInfo)
}
@@ -588,9 +620,18 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
private func addFoundDevice(foundService: inout FoundServiceInfo, endpointInfo: EndpointInfo, endpointID: String) -> RemoteDeviceInfo {
private func addFoundDevice(
foundService: inout FoundServiceInfo,
endpointInfo: EndpointInfo,
endpointID: String,
isQuickDropPeer: Bool = false
) -> RemoteDeviceInfo {
let hadPreviousDevice = foundServices[endpointID]?.device != nil
let deviceInfo = RemoteDeviceInfo(info: endpointInfo, id: endpointID)
let deviceInfo = RemoteDeviceInfo(
info: endpointInfo,
id: endpointID,
isQuickDropPeer: isQuickDropPeer
)
foundService.device = deviceInfo
foundServices[endpointID] = foundService
@@ -610,6 +651,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
private func removeFoundDevice(service: NWBrowser.Result) {
guard let endpointID = endpointID(for: service) else { return }
deviceNameResolver.cancelLookup(for: endpointID)
guard let _ = foundServices.removeValue(forKey: endpointID) else { return }
for delegate in outboundAppDelegates {
delegate.removeDevice(id: endpointID)
@@ -0,0 +1,244 @@
//
// NearbyDeviceNameResolver.swift
// QuickDrop
//
// Created by Leon Böttger on 25.04.2026.
//
import Darwin
import Foundation
import Network
final class NearbyDeviceNameResolver {
private var hostnameResolvers: [UUID: NearbyNetworkScannerHostnameResolver] = [:]
private var lookupTokens: [String: UUID] = [:]
func needsLookup(for endpointInfo: EndpointInfo?) -> Bool {
let cleanedName = endpointInfo?.name?.trimmingCharacters(in: .whitespacesAndNewlines)
return cleanedName?.isEmpty != false
}
func resolveName(for service: NWBrowser.Result, endpointID: String, completion: @escaping (String) -> Void) {
let lookupToken = UUID()
lookupTokens[endpointID] = lookupToken
resolveAdvertisedHostname(for: service) { [weak self] hostname in
guard let self else { return }
guard self.lookupTokens[endpointID] == lookupToken else { return }
self.lookupTokens.removeValue(forKey: endpointID)
guard let hostname else { return }
completion(hostname)
}
}
func cancelLookup(for endpointID: String) {
lookupTokens.removeValue(forKey: endpointID)
}
func cancelAll() {
lookupTokens.removeAll()
hostnameResolvers.values.forEach { $0.cancel() }
hostnameResolvers.removeAll()
}
private func resolveAdvertisedHostname(for service: NWBrowser.Result, completion: @escaping (String?) -> Void) {
let resolverID = UUID()
guard let resolver = NearbyNetworkScannerHostnameResolver(result: service, resolveTimeout: 0.5, completion: { [weak self] hostname in
self?.hostnameResolvers.removeValue(forKey: resolverID)
completion(hostname)
}) else {
completion(nil)
return
}
hostnameResolvers[resolverID] = resolver
resolver.start()
}
}
private final class NearbyNetworkScannerHostnameResolver: NSObject, NetServiceDelegate {
private static let hostnameLookupQueue = DispatchQueue(
label: "com.leonboettger.quickdrop.networkScannerHostname",
qos: .utility
)
private let service: NetService
private let resolveTimeout: TimeInterval
private var timeoutWorkItem: DispatchWorkItem?
private var completion: ((String?) -> Void)?
private var finished = false
init?(
result: NWBrowser.Result,
resolveTimeout: TimeInterval,
completion: @escaping (String?) -> Void
) {
guard case let NWEndpoint.service(name: name, type: type, domain: domain, interface: _) = result.endpoint else {
return nil
}
self.service = NetService(
domain: Self.normalizedServiceDomain(domain),
type: Self.normalizedServiceType(type),
name: name
)
self.resolveTimeout = resolveTimeout
self.completion = completion
super.init()
}
func start() {
service.delegate = self
service.resolve(withTimeout: resolveTimeout)
let timeoutWorkItem = DispatchWorkItem { [weak self] in
self?.finish(hostname: nil)
}
self.timeoutWorkItem = timeoutWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + resolveTimeout + 1.0, execute: timeoutWorkItem)
}
func cancel() {
finish(hostname: nil, notify: false)
}
func netServiceDidResolveAddress(_ sender: NetService) {
let numericAddresses = sender.addresses?.compactMap(Self.numericHostString) ?? []
guard let ipv4Address = numericAddresses.first(where: NearbyNetworkScannerHostLookup.isIPv4Address) else {
finish(hostname: nil)
return
}
Self.hostnameLookupQueue.async { [weak self] in
let hostname = NearbyNetworkScannerHostLookup.hostname(forIPv4: ipv4Address)
DispatchQueue.main.async {
self?.finish(hostname: hostname)
}
}
}
func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
finish(hostname: nil)
}
private func finish(hostname: String?, notify: Bool = true) {
guard !finished else { return }
finished = true
timeoutWorkItem?.cancel()
service.stop()
service.delegate = nil
let completion = completion
self.completion = nil
if notify {
completion?(hostname)
}
}
private static func normalizedServiceDomain(_ domain: String) -> String {
let trimmedDomain = domain.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedDomain.isEmpty else { return "local." }
return trimmedDomain.hasSuffix(".") ? trimmedDomain : "\(trimmedDomain)."
}
private static func normalizedServiceType(_ type: String) -> String {
let trimmedType = type.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmedType.hasSuffix(".") ? trimmedType : "\(trimmedType)."
}
private static func numericHostString(from addressData: Data) -> String? {
addressData.withUnsafeBytes { rawBuffer in
guard let baseAddress = rawBuffer.baseAddress else { return nil }
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let sockaddrPointer = baseAddress.assumingMemoryBound(to: sockaddr.self)
let result = getnameinfo(
sockaddrPointer,
socklen_t(addressData.count),
&hostBuffer,
socklen_t(hostBuffer.count),
nil,
0,
NI_NUMERICHOST
)
guard result == 0 else { return nil }
return String(cString: hostBuffer)
.split(separator: "%")
.first
.map(String.init)
}
}
}
private enum NearbyNetworkScannerHostLookup {
static func hostname(forIPv4 address: String) -> String? {
reverseDNSName(forIPv4: address).flatMap(prettifyDisplayName)
}
static func isIPv4Address(_ address: String) -> Bool {
!address.contains(":")
}
private static func reverseDNSName(forIPv4 address: String) -> String? {
var socketAddress = sockaddr_in()
socketAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
socketAddress.sin_family = sa_family_t(AF_INET)
guard inet_pton(AF_INET, address, &socketAddress.sin_addr) == 1 else {
return nil
}
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = withUnsafePointer(to: &socketAddress) { pointer in
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
getnameinfo(
sockaddrPointer,
socklen_t(MemoryLayout<sockaddr_in>.size),
&hostBuffer,
socklen_t(hostBuffer.count),
nil,
0,
NI_NAMEREQD
)
}
}
guard result == 0 else { return nil }
return String(cString: hostBuffer)
}
private static func prettifyDisplayName(_ value: String?) -> String? {
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
return nil
}
let withoutZone = value.split(separator: "%").first.map(String.init) ?? value
let withoutDomain = withoutZone.split(separator: ".").first.map(String.init) ?? withoutZone
let prettified = withoutDomain
.replacingOccurrences(of: "-", with: " ")
.replacingOccurrences(of: "_", with: " ")
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !prettified.isEmpty else { return nil }
return prettified.capitalized
}
}
+34 -2
View File
@@ -62,20 +62,52 @@ public struct RemoteDeviceInfo: Codable, Identifiable, Equatable {
public let name: String?
public let type: DeviceType
public let qrCodeData: Data?
public let isQuickDropPeer: Bool
public var id: String?
init(name: String?, type: DeviceType?, id: String? = nil) {
init(name: String?, type: DeviceType?, id: String? = nil, isQuickDropPeer: Bool = false) {
self.name = name
self.type = type ?? .phone
self.id = id
self.qrCodeData = nil
self.isQuickDropPeer = isQuickDropPeer
}
init(info: EndpointInfo, id: String? = nil) {
init(info: EndpointInfo, id: String? = nil, isQuickDropPeer: Bool = false) {
self.name = info.name
self.type = info.deviceType
self.qrCodeData = info.qrCodeData
self.id = id
self.isQuickDropPeer = isQuickDropPeer
}
private enum CodingKeys: String, CodingKey {
case name
case type
case qrCodeData
case isQuickDropPeer
case isVerifiedQuickDropPeer
case id
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeIfPresent(String.self, forKey: .name)
type = try container.decode(DeviceType.self, forKey: .type)
qrCodeData = try container.decodeIfPresent(Data.self, forKey: .qrCodeData)
isQuickDropPeer = try container.decodeIfPresent(Bool.self, forKey: .isQuickDropPeer)
?? container.decodeIfPresent(Bool.self, forKey: .isVerifiedQuickDropPeer)
?? false
id = try container.decodeIfPresent(String.self, forKey: .id)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(name, forKey: .name)
try container.encode(type, forKey: .type)
try container.encodeIfPresent(qrCodeData, forKey: .qrCodeData)
try container.encode(isQuickDropPeer, forKey: .isQuickDropPeer)
try container.encodeIfPresent(id, forKey: .id)
}
+14
View File
@@ -79,6 +79,7 @@
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; };
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; };
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
@@ -147,6 +148,7 @@
EB3B9F6C2EF7151900D6704E /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; };
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; };
@@ -199,11 +201,13 @@
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; };
EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; };
EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */; };
@@ -213,6 +217,7 @@
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
@@ -285,6 +290,7 @@
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
@@ -495,6 +501,7 @@
69DA9A1329E0BF5200A442DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
69DA9A1829E0BF5200A442DA /* QuickDrop.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickDrop.entitlements; sourceTree = "<group>"; };
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyConnectionManager.swift; sourceTree = "<group>"; };
EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDeviceNameResolver.swift; sourceTree = "<group>"; };
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboundNearbyConnection.swift; sourceTree = "<group>"; };
69DA9A2829E18CB500A442DA /* ukey.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ukey.pb.swift; sourceTree = "<group>"; };
69DA9A2929E18CB500A442DA /* wire_format.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = wire_format.pb.swift; sourceTree = "<group>"; };
@@ -788,6 +795,7 @@
isa = PBXGroup;
children = (
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */,
EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */,
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */,
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */,
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */,
@@ -1315,6 +1323,7 @@
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */,
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
@@ -1354,6 +1363,7 @@
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */,
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */,
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */,
@@ -1411,6 +1421,7 @@
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */,
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */,
EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */,
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */,
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */,
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */,
@@ -1476,6 +1487,7 @@
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */,
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */,
EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */,
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */,
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */,
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */,
@@ -1496,6 +1508,7 @@
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */,
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */,
EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */,
@@ -1551,6 +1564,7 @@
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */,
EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */,
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */,
EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */,
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
EBBC93B22EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */,
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
+15 -3
View File
@@ -289,9 +289,21 @@ struct DeviceButtonLabel: View {
}
VStack(alignment: .leading, spacing: 5) {
Text(name)
.fontWeight(.medium)
.foregroundColor(Color.primary)
HStack(spacing: 5) {
Text(name)
.fontWeight(.medium)
.foregroundColor(Color.primary)
if device.isQuickDropPeer {
Image(.menuBarIcon)
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 14, height: 14)
.foregroundColor(Color.primary)
.opacity(0.5)
}
}
Text(isSelected ? progress ?? "..." : "Available".localized())
.font(.system(size: 12))
+9 -2
View File
@@ -95,6 +95,9 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
progressDeviceIconWrap!.wantsLayer = true
progressDeviceIconWrap!.layer!.masksToBounds = false
progressDeviceSecondaryIcon?.image = NSImage(named: "MenuBarIcon")
progressDeviceSecondaryIcon?.image?.isTemplate = true
progressDeviceSecondaryIcon?.contentTintColor = .white
}
@@ -250,6 +253,7 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
progressView?.animator().isHidden = false
progressDeviceName?.stringValue = getDeviceName(device: device)
progressDeviceIcon?.image = imageForDeviceType(type: device.type)
progressDeviceSecondaryIcon?.isHidden = !device.isQuickDropPeer
progressProgressBar?.startAnimation(nil)
progressState?.stringValue = "Preparing".localized()
chosenDevice = device
@@ -383,8 +387,11 @@ extension ShareViewController: NSCollectionViewDataSource {
let device = foundDevices[indexPath[1]]
collectionViewItem.textField?.stringValue = getDeviceName(device: device)
collectionViewItem.imageView?.image = imageForDeviceType(type: device.type)
collectionViewItem.configure(
deviceName: getDeviceName(device: device),
deviceImage: imageForDeviceType(type: device.type),
showsQuickDropPeerMarker: device.isQuickDropPeer && device.type == .phone
)
collectionViewItem.clickHandler = {
self.selectDevice(device: device)
}
@@ -177,17 +177,17 @@ Gw
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="rb3-78-PXe"/>
</imageView>
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="BEO-kU-a2Q">
<rect key="frame" x="24" y="-8" width="32" height="32"/>
<rect key="frame" x="16.800000000000001" y="16.800000000000001" width="14.4" height="14.4"/>
<constraints>
<constraint firstAttribute="height" constant="32" id="05I-Ef-84Z"/>
<constraint firstAttribute="width" constant="32" id="V0g-Fz-HWH"/>
<constraint firstAttribute="height" constant="14.4" id="05I-Ef-84Z"/>
<constraint firstAttribute="width" constant="14.4" id="V0g-Fz-HWH"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSCaution" id="k1c-Xl-fTQ"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="k1c-Xl-fTQ"/>
</imageView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="BEO-kU-a2Q" secondAttribute="trailing" constant="-8" id="59U-Zi-ESa"/>
<constraint firstAttribute="bottom" secondItem="BEO-kU-a2Q" secondAttribute="bottom" constant="-8" id="8ty-pq-miR"/>
<constraint firstItem="BEO-kU-a2Q" firstAttribute="centerX" secondItem="84U-lB-SRB" secondAttribute="centerX" id="59U-Zi-ESa"/>
<constraint firstItem="BEO-kU-a2Q" firstAttribute="centerY" secondItem="84U-lB-SRB" secondAttribute="centerY" id="8ty-pq-miR"/>
<constraint firstItem="84U-lB-SRB" firstAttribute="top" secondItem="jBg-Ih-Gac" secondAttribute="top" id="MB9-UI-2mb"/>
<constraint firstItem="84U-lB-SRB" firstAttribute="leading" secondItem="jBg-Ih-Gac" secondAttribute="leading" id="b23-hJ-aWT"/>
<constraint firstAttribute="trailing" secondItem="84U-lB-SRB" secondAttribute="trailing" id="eKl-3V-K49"/>
+11
View File
@@ -9,6 +9,7 @@ import Cocoa
class DeviceListCell: NSCollectionViewItem {
public var clickHandler: (() -> Void)?
@IBOutlet private var peerMarkerView: NSImageView?
override func viewDidLoad() {
super.viewDidLoad()
@@ -17,10 +18,20 @@ class DeviceListCell: NSCollectionViewItem {
btn.setButtonType(.momentaryPushIn)
btn.action = #selector(onClick)
btn.target = self
peerMarkerView?.image = NSImage(named: "MenuBarIcon")
peerMarkerView?.image?.isTemplate = true
peerMarkerView?.contentTintColor = .white
}
@IBAction func onClick(_: Any?) {
guard let handler = clickHandler else { return }
handler()
}
func configure(deviceName: String, deviceImage: NSImage, showsQuickDropPeerMarker: Bool) {
textField?.stringValue = deviceName
imageView?.image = deviceImage
peerMarkerView?.isHidden = !showsQuickDropPeerMarker
}
}
+11
View File
@@ -36,10 +36,20 @@
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="QGU-rR-rJQ"/>
</imageView>
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="89K-DQ-f8g">
<rect key="frame" x="95.799999999999997" y="58.799999999999997" width="14.4" height="14.4"/>
<constraints>
<constraint firstAttribute="width" constant="14.4" id="Lfh-Ha-Njl"/>
<constraint firstAttribute="height" constant="14.4" id="Wm1-YW-85X"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="0fp-y5-fj9"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="89K-DQ-f8g" firstAttribute="centerX" secondItem="O1J-am-CoK" secondAttribute="centerX" id="Bja-VK-UQf"/>
<constraint firstItem="O1J-am-CoK" firstAttribute="centerX" secondItem="Hz6-mo-xeY" secondAttribute="centerX" id="R76-2L-pXb"/>
<constraint firstAttribute="bottom" secondItem="QPM-sE-maP" secondAttribute="bottom" id="Ryn-nY-yZp"/>
<constraint firstItem="89K-DQ-f8g" firstAttribute="centerY" secondItem="O1J-am-CoK" secondAttribute="centerY" id="Yeh-mY-trP"/>
<constraint firstItem="O1J-am-CoK" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="ZFA-S9-fpT"/>
<constraint firstAttribute="trailing" secondItem="QPM-sE-maP" secondAttribute="trailing" id="eOl-Va-HXq"/>
<constraint firstItem="QPM-sE-maP" firstAttribute="top" secondItem="O1J-am-CoK" secondAttribute="bottom" constant="10" id="leg-nD-mHB"/>
@@ -53,6 +63,7 @@
<collectionViewItem id="1Lb-iJ-Nar" customClass="DeviceListCell" customModule="QuickDropShareExtension" customModuleProvider="target">
<connections>
<outlet property="imageView" destination="O1J-am-CoK" id="zQn-mV-bRy"/>
<outlet property="peerMarkerView" destination="89K-DQ-f8g" id="iWY-ru-o1h"/>
<outlet property="textField" destination="QPM-sE-maP" id="s0j-Qq-7iA"/>
<outlet property="view" destination="Hz6-mo-xeY" id="gZn-20-o77"/>
</connections>