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 {
// 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
+4 -4
View File
@@ -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;
@@ -52,7 +52,7 @@
"location" : "https://github.com/leonboe1/LUI.git",
"state" : {
"branch" : "main",
"revision" : "170752316e3b728bd06f8fa4073fe18e9a99a5cd"
"revision" : "c26d4a19b175cf725deb00b412cf2f8c6df95826"
}
},
{
+1 -1
View File
@@ -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
+16 -12
View File
@@ -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: {})
}
}
}
+2 -2
View File
@@ -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
+14 -2
View File
@@ -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"
}
}
}