Prefer fixed port if possible

This commit is contained in:
Leon Böttger committed 2026-01-23 20:58:45 +01:00
1 parent b546370e87
commit cce84e9460
7 files changed
+133 -41

No files matched your search

@@ -21,6 +21,8 @@ import LUI
public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate, ObservableObject { public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate, ObservableObject {
// MARK: Private Properties
#if !EXTENSION #if !EXTENSION
private let sleepManager = SleepManager.shared private let sleepManager = SleepManager.shared
#endif #endif
@@ -32,7 +34,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
private var inboundAppDelegates: [InboundAppDelegate] = [] private var inboundAppDelegates: [InboundAppDelegate] = []
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:] private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
private var startedDeviceDiscovery = false private var startedDeviceDiscovery = false
private var startedAdvertising = false private var startedAdvertising = false { didSet { informAboutStatus() }}
private var browsers: [NWBrowser] = [] private var browsers: [NWBrowser] = []
private let serviceTypes = ["_FC9F5ED42C8A._tcp."] private let serviceTypes = ["_FC9F5ED42C8A._tcp."]
private var qrCodePrivateKey: ECPrivateKey? private var qrCodePrivateKey: ECPrivateKey?
@@ -42,24 +44,31 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue") private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue")
private var securityScopeUrl: URL? private var securityScopeUrl: URL?
private static let customDeviceNameKey = "com.leonboettger.quickdrop.deviceName" private static let customDeviceNameKey = "com.leonboettger.quickdrop.deviceName"
private var defaultPort: NWEndpoint.Port = 50362
public let endpointID: [UInt8] = generateEndpointID()
// MARK: Shared Instance
public static let shared = NearbyConnectionManager() public static let shared = NearbyConnectionManager()
// MARK: Published State
@Published var attachments: AttachmentDetails? = nil @Published var attachments: AttachmentDetails? = nil
@Published var hasLocalNetworkPermission = true @Published var hasLocalNetworkPermission = true
@Published var isConnectedToLocalNetwork = true @Published var isConnectedToLocalNetwork = true { didSet { informAboutStatus() }}
@Published var deviceInfo: EndpointInfo @Published var deviceInfo: EndpointInfo
public var connectionUpdateCallback: (Bool) -> Void = { _ in } {
didSet {
connectionUpdateCallback(isConnectedToLocalNetwork)
}
}
// MARK: Public Properties
public var endpointID: [UInt8] = generateEndpointID(forceRegeneration: false)
public var connectionUpdateCallback: (Bool) -> Void = { _ in } { didSet { informAboutStatus() }}
public var changedDeviceNameCallback: () -> Void = { } public var changedDeviceNameCallback: () -> Void = { }
// MARK: Initializers
override init() { override init() {
self.deviceInfo = Self.getEndpointInfo() self.deviceInfo = Self.getEndpointInfo()
tcpListener = try! NWListener(using: NWParameters(tls: .none)) tcpListener = try! NWListener(using: NWParameters(tls: .none))
@@ -83,7 +92,6 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
log("[NearbyConnectionManager] Local network lost.") log("[NearbyConnectionManager] Local network lost.")
} }
self.isConnectedToLocalNetwork = isConnected self.isConnectedToLocalNetwork = isConnected
self.connectionUpdateCallback(isConnected)
} }
} }
@@ -113,6 +121,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
} }
// MARK: Deinitializer
deinit { deinit {
self.stopAccessingSaveDirectory() self.stopAccessingSaveDirectory()
} }
@@ -161,22 +171,73 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
} }
public func becomeVisible() { public func becomeVisible(randomPort: Bool = false) {
if startedAdvertising { if startedAdvertising {
log("[NearbyConnectionManager] Already advertising, skipping") log("[NearbyConnectionManager] Already advertising, skipping")
return return
} }
log("[NearbyConnectionManager] Becoming visible") log("[NearbyConnectionManager] Becoming visible")
let parameters = NWParameters(tls: .none)
tcpListener = try! NWListener(using: NWParameters(tls: .none)) do {
startedAdvertising = true if randomPort {
self.tcpListener = try NWListener(using: parameters)
tcpListener.stateUpdateHandler = { (state: NWListener.State) in } else {
if case .ready = state { self.tcpListener = try NWListener(using: parameters, on: defaultPort)
self.initMDNS()
} }
} }
catch {
log("[NearbyConnectionManager] Error starting TCP listener: \(error). Trying with random port.")
runAfter(seconds: 1) {
self.becomeVisible(randomPort: true)
}
return
}
startedAdvertising = true
tcpListener.stateUpdateHandler = { state in
switch state {
case .ready:
log("[NearbyConnectionManager] Listener ready")
self.initMDNS(forceIDRegeneration: randomPort)
if let port = self.tcpListener.port, port != self.defaultPort {
log("[NearbyConnectionManager] Updated default port to \(port)")
self.defaultPort = port
}
case .failed(let error):
log("[NearbyConnectionManager] Listener failed: \(error)")
self.becomeInvisible()
runAfter(seconds: 1) {
if case .posix(let posixError) = error,
posixError == .EADDRINUSE {
self.becomeVisible(randomPort: true)
}
else {
self.becomeVisible()
}
}
case .cancelled:
log("[NearbyConnectionManager] Listener cancelled")
case .setup:
log("[NearbyConnectionManager] Listener setup")
case .waiting(let state):
log("[NearbyConnectionManager] Listener waiting: \(state)")
@unknown default:
log("[NearbyConnectionManager] Listener unknown state")
}
}
tcpListener.newConnectionHandler = { (connection: NWConnection) in tcpListener.newConnectionHandler = { (connection: NWConnection) in
let id = UUID().uuidString let id = UUID().uuidString
let conn = InboundNearbyConnection(connection: connection, id: id) let conn = InboundNearbyConnection(connection: connection, id: id)
@@ -184,6 +245,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
conn.delegate = self conn.delegate = self
conn.start() conn.start()
} }
tcpListener.start(queue: .global(qos: .utility)) tcpListener.start(queue: .global(qos: .utility))
} }
@@ -202,11 +264,13 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
} }
private static func generateEndpointID() -> [UInt8] { private static func generateEndpointID(forceRegeneration: Bool) -> [UInt8] {
// Try to retrieve from UserDefaults // Try to retrieve from UserDefaults
if let savedString = Settings.shared.endpointID, if !forceRegeneration,
let savedString = Settings.shared.endpointID,
let savedData = savedString.data(using: .utf8) { let savedData = savedString.data(using: .utf8) {
log("[NearbyConnectionManager] Using cached endpoint ID: \(savedString)")
return [UInt8](savedData) return [UInt8](savedData)
} }
@@ -220,12 +284,16 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
// Save to UserDefaults as String // Save to UserDefaults as String
let idString = String(bytes: id, encoding: .utf8) ?? "" let idString = String(bytes: id, encoding: .utf8) ?? ""
Settings.shared.endpointID = idString Settings.shared.endpointID = idString
log("[NearbyConnectionManager] Storing new endpoint ID: \(idString) ======================================")
return id return id
} }
private func initMDNS() { private func initMDNS(forceIDRegeneration: Bool) {
self.endpointID = Self.generateEndpointID(forceRegeneration: forceIDRegeneration)
let nameBytes: [UInt8] = [ let nameBytes: [UInt8] = [
0x23, // PCP 0x23, // PCP
endpointID[0], endpointID[1], endpointID[2], endpointID[3], endpointID[0], endpointID[1], endpointID[2], endpointID[3],
@@ -721,6 +789,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
} }
// MARK: Callbacks
func informAboutStatus() {
DispatchQueue.main.async {
self.connectionUpdateCallback(self.isConnectedToLocalNetwork && self.startedAdvertising)
}
}
// -- MARK: - Internal Data Model // -- MARK: - Internal Data Model
+4 -4
View File
@@ -1719,8 +1719,8 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx; SDKROOT = macosx;
SHARED_BUILD = 108; SHARED_BUILD = 111;
SHARED_VERSION = 2.10.4; SHARED_VERSION = 2.10.6;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1782,8 +1782,8 @@
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = macosx; SDKROOT = macosx;
SHARED_BUILD = 108; SHARED_BUILD = 111;
SHARED_VERSION = 2.10.4; SHARED_VERSION = 2.10.6;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -52,7 +52,7 @@
"location" : "https://github.com/leonboe1/LUI.git", "location" : "https://github.com/leonboe1/LUI.git",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "170752316e3b728bd06f8fa4073fe18e9a99a5cd" "revision" : "c26d4a19b175cf725deb00b412cf2f8c6df95826"
} }
}, },
{ {
+1 -1
View File
@@ -337,7 +337,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
} }
}), hasSheet: false) }), hasSheet: false)
let plusViewWidth: CGFloat = 550 let plusViewWidth: CGFloat = 750
let plusViewHeight: CGFloat = 750 let plusViewHeight: CGFloat = 750
// Create an NSWindow to host the SwiftUI view // Create an NSWindow to host the SwiftUI view
+16 -12
View File
@@ -5,8 +5,9 @@
// Created by Leon Böttger on 05.10.25. // Created by Leon Böttger on 05.10.25.
// //
import SwiftUI
import AppKit import AppKit
import LUI
import SwiftUI
let toastViewSize = CGSize(width: 400, height: 120) let toastViewSize = CGSize(width: 400, height: 120)
@@ -32,14 +33,12 @@ struct QuickDropToastView: View {
let isDone = receiveModel.toastActions != nil let isDone = receiveModel.toastActions != nil
VStack(spacing: 12) { VStack(spacing: 12) {
HStack(spacing: 12) { HStack(spacing: 10) {
ZStack(alignment: .bottomLeading) {
Image(.quickDropIcon) let iconSize = 39.0
.resizable()
.scaledToFill() AppIconView(hasPlusIcon: false, size: iconSize)
.frame(width: 44, height: 44) .frame(width: iconSize, height: iconSize)
.clipShape(Circle())
}
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(String("QuickDrop")) Text(String("QuickDrop"))
@@ -118,7 +117,7 @@ struct QuickDropToastView: View {
.padding(16) .padding(16)
.background( .background(
VisualEffectView(material: .hudWindow) VisualEffectView(material: .hudWindow)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
) )
.overlay( .overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous) RoundedRectangle(cornerRadius: 16, style: .continuous)
@@ -143,6 +142,10 @@ fileprivate struct QuickDropToastViewButton: View {
Color.gray.opacity(0.15) Color.gray.opacity(0.15)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
) )
.overlay(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.stroke(Color.black.opacity(0.04), lineWidth: 1)
)
} }
.buttonStyle(.plain) // prevents blue macOS button look .buttonStyle(.plain) // prevents blue macOS button look
} }
@@ -203,7 +206,7 @@ struct CapsuleProgress: View {
struct QuickDropToastView_Previews: PreviewProvider { struct QuickDropToastView_Previews: PreviewProvider {
struct Demo: View { struct Demo: View {
@State var model = ReceiveModel(controlPlusScreen: { _ in }) @State var model = ReceiveModel(controlPlusScreen: { _ in })
@State var done = false @State var done = true
var body: some View { var body: some View {
QuickDropToastView( QuickDropToastView(
@@ -215,7 +218,8 @@ struct QuickDropToastView_Previews: PreviewProvider {
.padding(100) .padding(100)
.background(Color.black.opacity(0.2)) .background(Color.black.opacity(0.2))
.onAppear { .onAppear {
model.progress = 0.01 model.progress = 1
model.toastActions = .init(openFilesAction: {}, importPhotosAction: {}, closeToastAction: {})
} }
} }
} }
+2 -2
View File
@@ -355,8 +355,8 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
let frame = screen.frame let frame = screen.frame
let visible = screen.visibleFrame let visible = screen.visibleFrame
let marginX: CGFloat = 24 let marginX: CGFloat = 20
let marginFromTop: CGFloat = 30 let marginFromTop: CGFloat = 20
// If the menu bar is auto-hidden, visible.maxY == frame.maxY; subtract the status bar thickness to stay clear // If the menu bar is auto-hidden, visible.maxY == frame.maxY; subtract the status bar thickness to stay clear
let menuBarHidden = abs(frame.maxY - visible.maxY) < 0.5 let menuBarHidden = abs(frame.maxY - visible.maxY) < 0.5
+14 -2
View File
@@ -10,7 +10,19 @@
"layers" : [ "layers" : [
{ {
"image-name" : "QuickDropArrows.png", "image-name" : "QuickDropArrows.png",
"name" : "QuickDropArrows" "name" : "QuickDropArrows",
"position-specializations" : [
{
"idiom" : "square",
"value" : {
"scale" : 1,
"translation-in-points" : [
0,
-5
]
}
}
]
}, },
{ {
"image-name" : "QuickDropCircle.png", "image-name" : "QuickDropCircle.png",
@@ -34,4 +46,4 @@
], ],
"squares" : "shared" "squares" : "shared"
} }
} }