From cce84e9460e2db3d7b24bc315992b5ce4de35621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6ttger?= Date: Fri, 23 Jan 2026 20:58:45 +0100 Subject: [PATCH] Prefer fixed port if possible --- .../Connection/NearbyConnectionManager.swift | 114 +++++++++++++++--- QuickDrop.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- QuickDrop/AppDelegate.swift | 2 +- QuickDrop/View/QuickDropToastView.swift | 28 +++-- QuickDrop/ViewModel/ReceiveModel.swift | 4 +- QuickDropIcon.icon/icon.json | 16 ++- 7 files changed, 133 insertions(+), 41 deletions(-) diff --git a/NearbyShare/Connection/NearbyConnectionManager.swift b/NearbyShare/Connection/NearbyConnectionManager.swift index 2dfd537..f85aadc 100644 --- a/NearbyShare/Connection/NearbyConnectionManager.swift +++ b/NearbyShare/Connection/NearbyConnectionManager.swift @@ -21,6 +21,8 @@ import LUI public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate, ObservableObject { + // MARK: Private Properties + #if !EXTENSION private let sleepManager = SleepManager.shared #endif @@ -32,7 +34,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb private var inboundAppDelegates: [InboundAppDelegate] = [] private var outgoingTransfers: [String: OutgoingTransferInfo] = [:] private var startedDeviceDiscovery = false - private var startedAdvertising = false + private var startedAdvertising = false { didSet { informAboutStatus() }} private var browsers: [NWBrowser] = [] private let serviceTypes = ["_FC9F5ED42C8A._tcp."] private var qrCodePrivateKey: ECPrivateKey? @@ -42,24 +44,31 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue") private var securityScopeUrl: URL? 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() + + // MARK: Published State + @Published var attachments: AttachmentDetails? = nil @Published var hasLocalNetworkPermission = true - @Published var isConnectedToLocalNetwork = true + @Published var isConnectedToLocalNetwork = true { didSet { informAboutStatus() }} @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 = { } + + // MARK: Initializers + override init() { self.deviceInfo = Self.getEndpointInfo() tcpListener = try! NWListener(using: NWParameters(tls: .none)) @@ -83,7 +92,6 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb log("[NearbyConnectionManager] Local network lost.") } self.isConnectedToLocalNetwork = isConnected - self.connectionUpdateCallback(isConnected) } } @@ -113,6 +121,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb } + // MARK: Deinitializer + deinit { self.stopAccessingSaveDirectory() } @@ -161,22 +171,73 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb } - public func becomeVisible() { + public func becomeVisible(randomPort: Bool = false) { if startedAdvertising { log("[NearbyConnectionManager] Already advertising, skipping") return } log("[NearbyConnectionManager] Becoming visible") + let parameters = NWParameters(tls: .none) - tcpListener = try! NWListener(using: NWParameters(tls: .none)) - startedAdvertising = true - - tcpListener.stateUpdateHandler = { (state: NWListener.State) in - if case .ready = state { - self.initMDNS() + do { + if randomPort { + self.tcpListener = try NWListener(using: parameters) + } else { + self.tcpListener = try NWListener(using: parameters, on: defaultPort) } } + 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 let id = UUID().uuidString let conn = InboundNearbyConnection(connection: connection, id: id) @@ -184,6 +245,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb conn.delegate = self conn.start() } + 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 - if let savedString = Settings.shared.endpointID, + if !forceRegeneration, + let savedString = Settings.shared.endpointID, let savedData = savedString.data(using: .utf8) { + log("[NearbyConnectionManager] Using cached endpoint ID: \(savedString)") return [UInt8](savedData) } @@ -220,12 +284,16 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb // Save to UserDefaults as String let idString = String(bytes: id, encoding: .utf8) ?? "" Settings.shared.endpointID = idString + log("[NearbyConnectionManager] Storing new endpoint ID: \(idString) ======================================") return id } - private func initMDNS() { + private func initMDNS(forceIDRegeneration: Bool) { + + self.endpointID = Self.generateEndpointID(forceRegeneration: forceIDRegeneration) + let nameBytes: [UInt8] = [ 0x23, // PCP 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 diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index 26a36cb..28bd378 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -1719,8 +1719,8 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; - SHARED_BUILD = 108; - SHARED_VERSION = 2.10.4; + SHARED_BUILD = 111; + SHARED_VERSION = 2.10.6; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1782,8 +1782,8 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; - SHARED_BUILD = 108; - SHARED_VERSION = 2.10.4; + SHARED_BUILD = 111; + SHARED_VERSION = 2.10.6; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8fa7ffb..47b2b24 100644 --- a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -52,7 +52,7 @@ "location" : "https://github.com/leonboe1/LUI.git", "state" : { "branch" : "main", - "revision" : "170752316e3b728bd06f8fa4073fe18e9a99a5cd" + "revision" : "c26d4a19b175cf725deb00b412cf2f8c6df95826" } }, { diff --git a/QuickDrop/AppDelegate.swift b/QuickDrop/AppDelegate.swift index 15d241b..feca71d 100644 --- a/QuickDrop/AppDelegate.swift +++ b/QuickDrop/AppDelegate.swift @@ -337,7 +337,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele } }), hasSheet: false) - let plusViewWidth: CGFloat = 550 + let plusViewWidth: CGFloat = 750 let plusViewHeight: CGFloat = 750 // Create an NSWindow to host the SwiftUI view diff --git a/QuickDrop/View/QuickDropToastView.swift b/QuickDrop/View/QuickDropToastView.swift index 1594a05..5da3274 100644 --- a/QuickDrop/View/QuickDropToastView.swift +++ b/QuickDrop/View/QuickDropToastView.swift @@ -5,8 +5,9 @@ // Created by Leon Böttger on 05.10.25. // -import SwiftUI import AppKit +import LUI +import SwiftUI let toastViewSize = CGSize(width: 400, height: 120) @@ -32,14 +33,12 @@ struct QuickDropToastView: View { let isDone = receiveModel.toastActions != nil VStack(spacing: 12) { - HStack(spacing: 12) { - ZStack(alignment: .bottomLeading) { - Image(.quickDropIcon) - .resizable() - .scaledToFill() - .frame(width: 44, height: 44) - .clipShape(Circle()) - } + HStack(spacing: 10) { + + let iconSize = 39.0 + + AppIconView(hasPlusIcon: false, size: iconSize) + .frame(width: iconSize, height: iconSize) VStack(alignment: .leading, spacing: 2) { Text(String("QuickDrop")) @@ -118,7 +117,7 @@ struct QuickDropToastView: View { .padding(16) .background( VisualEffectView(material: .hudWindow) - .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) ) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) @@ -143,6 +142,10 @@ fileprivate struct QuickDropToastViewButton: View { Color.gray.opacity(0.15) .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 } @@ -203,7 +206,7 @@ struct CapsuleProgress: View { struct QuickDropToastView_Previews: PreviewProvider { struct Demo: View { @State var model = ReceiveModel(controlPlusScreen: { _ in }) - @State var done = false + @State var done = true var body: some View { QuickDropToastView( @@ -215,7 +218,8 @@ struct QuickDropToastView_Previews: PreviewProvider { .padding(100) .background(Color.black.opacity(0.2)) .onAppear { - model.progress = 0.01 + model.progress = 1 + model.toastActions = .init(openFilesAction: {}, importPhotosAction: {}, closeToastAction: {}) } } } diff --git a/QuickDrop/ViewModel/ReceiveModel.swift b/QuickDrop/ViewModel/ReceiveModel.swift index 0f458da..6096a03 100644 --- a/QuickDrop/ViewModel/ReceiveModel.swift +++ b/QuickDrop/ViewModel/ReceiveModel.swift @@ -355,8 +355,8 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { let frame = screen.frame let visible = screen.visibleFrame - let marginX: CGFloat = 24 - let marginFromTop: CGFloat = 30 + let marginX: CGFloat = 20 + let marginFromTop: CGFloat = 20 // 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 diff --git a/QuickDropIcon.icon/icon.json b/QuickDropIcon.icon/icon.json index 2879b0a..b05b066 100644 --- a/QuickDropIcon.icon/icon.json +++ b/QuickDropIcon.icon/icon.json @@ -10,7 +10,19 @@ "layers" : [ { "image-name" : "QuickDropArrows.png", - "name" : "QuickDropArrows" + "name" : "QuickDropArrows", + "position-specializations" : [ + { + "idiom" : "square", + "value" : { + "scale" : 1, + "translation-in-points" : [ + 0, + -5 + ] + } + } + ] }, { "image-name" : "QuickDropCircle.png", @@ -34,4 +46,4 @@ ], "squares" : "shared" } -} +} \ No newline at end of file