From eab94c1c1292e3bd356ba1a1366d4a7bfd5d7a48 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Wed, 3 Jun 2026 17:55:33 +0200 Subject: [PATCH] feat: flash the transfer arrow icon for per-peripheral switches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The menu-bar sending/receiving arrows previously fired only for the full-set switch (CONNECT_ALL / UNREGISTER_ALL). A single-peripheral switch — from the menu's per-peripheral rows or the Peripheral tab — moved only that peripheral's row, leaving the status-bar icon idle on both Macs. Add two direction-based signals (magicSwitchPeripheralIncoming / magicSwitchPeripheralOutgoing) posted both by the local per-peripheral senders (takePeripheralFromPeer / sendPeripheralToPeer, after the no-peer guard so a local-only fallback doesn't flash a cross-gap arrow) and by the incoming .connectOne / .unregisterOne handlers. AppDelegate observes them and drives the same .receiving / .sending icon through beginTransferAutoEnd, so both Macs show arrows pointing the same physical way during a one-peripheral handoff — matching the full-set behaviour. Both sides use the receiver's 5s auto-end; the per-row (Pairing…) state still carries exact progress. --- Magic Switch/AppDelegate/AppDelegate.swift | 24 ++++++++++++++++++- Magic Switch/Manager/IncomingConnection.swift | 14 +++++++++++ .../Store/BluetoothPeripheralStore.swift | 5 ++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Magic Switch/AppDelegate/AppDelegate.swift b/Magic Switch/AppDelegate/AppDelegate.swift index b87680b..754099e 100644 --- a/Magic Switch/AppDelegate/AppDelegate.swift +++ b/Magic Switch/AppDelegate/AppDelegate.swift @@ -33,9 +33,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private var pingObserver: NSObjectProtocol? /// Resets the status-bar icon back to its real state after a Ping flash. private var pingFlashTimer: DispatchSourceTimer? - /// Observers for inbound peripheral-handoff posts from `IncomingConnection`. + /// Observers for inbound full-set peripheral-handoff posts from + /// `IncomingConnection`. private var transferReceiveObserver: NSObjectProtocol? private var transferReleaseObserver: NSObjectProtocol? + /// Same, for single-peripheral switches — posted both by the local + /// per-peripheral senders and by the incoming `.connectOne` / + /// `.unregisterOne` handlers. + private var transferIncomingOneObserver: NSObjectProtocol? + private var transferOutgoingOneObserver: NSObjectProtocol? /// Direction the status-bar icon should currently advertise. `idle` falls /// through to the normal/needs-attention logic in `refreshStatusBarIcon`. private enum TransferState { @@ -138,6 +144,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { if let token = transferReleaseObserver { NotificationCenter.default.removeObserver(token) } + if let token = transferIncomingOneObserver { + NotificationCenter.default.removeObserver(token) + } + if let token = transferOutgoingOneObserver { + NotificationCenter.default.removeObserver(token) + } } // MARK: - Setup Methods @@ -300,6 +312,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate { ) { [weak self] _ in self?.beginTransferAutoEnd(.sending) } + transferIncomingOneObserver = NotificationCenter.default.addObserver( + forName: .magicSwitchPeripheralIncoming, object: nil, queue: .main + ) { [weak self] _ in + self?.beginTransferAutoEnd(.receiving) + } + transferOutgoingOneObserver = NotificationCenter.default.addObserver( + forName: .magicSwitchPeripheralOutgoing, object: nil, queue: .main + ) { [weak self] _ in + self?.beginTransferAutoEnd(.sending) + } } private func statusBarTooltip() -> String { diff --git a/Magic Switch/Manager/IncomingConnection.swift b/Magic Switch/Manager/IncomingConnection.swift index 368a6b6..fbd6e3a 100644 --- a/Magic Switch/Manager/IncomingConnection.swift +++ b/Magic Switch/Manager/IncomingConnection.swift @@ -18,6 +18,16 @@ extension Notification.Name { /// status-bar icon to the "sending peripherals" state. static let magicSwitchReceivedUnregisterAll = Notification.Name( "magicSwitchReceivedUnregisterAll") + /// Per-peripheral counterparts of the `…ConnectAll` / `…UnregisterAll` + /// signals above: the same status-bar arrows, but for a single-device + /// switch. Posted both by this Mac's per-peripheral senders + /// (`takePeripheralFromPeer` / `sendPeripheralToPeer`) and by the incoming + /// `.connectOne` / `.unregisterOne` handlers, so both Macs show arrows + /// pointing the same physical way during a one-peripheral handoff. + static let magicSwitchPeripheralIncoming = Notification.Name( + "magicSwitchPeripheralIncoming") + static let magicSwitchPeripheralOutgoing = Notification.Name( + "magicSwitchPeripheralOutgoing") } /// Per-accept handler. Owns the NWConnection, its SecureChannel, idle/total @@ -264,6 +274,8 @@ final class IncomingConnection { let store = bluetoothStore let address = message DispatchQueue.main.async { + // Peripheral is leaving this Mac for the peer — flash the sending arrow. + NotificationCenter.default.post(name: .magicSwitchPeripheralOutgoing, object: nil) if let peripheral = store.peripherals.first(where: { $0.id == address }) { store.unregisterFromPC(peripheral) } @@ -281,6 +293,8 @@ final class IncomingConnection { let store = bluetoothStore let address = message DispatchQueue.main.async { + // Peripheral is arriving at this Mac — flash the receiving arrow. + NotificationCenter.default.post(name: .magicSwitchPeripheralIncoming, object: nil) if let peripheral = store.peripherals.first(where: { $0.id == address }) { store.connectPeripheral(peripheral) } diff --git a/Magic Switch/Model/Store/BluetoothPeripheralStore.swift b/Magic Switch/Model/Store/BluetoothPeripheralStore.swift index 7a83640..e7e059f 100644 --- a/Magic Switch/Model/Store/BluetoothPeripheralStore.swift +++ b/Magic Switch/Model/Store/BluetoothPeripheralStore.swift @@ -454,6 +454,9 @@ final class BluetoothPeripheralStore: NSObject, ObservableObject, BluetoothPerip return } + // Peripheral is arriving at this Mac — flash the receiving arrow, the same + // signal the full-set take raises on the menu-bar icon. + NotificationCenter.default.post(name: .magicSwitchPeripheralIncoming, object: nil) setConnectionState(.connecting, for: peripheral.id) schedulePairWatchdog(for: peripheral, announceTimeout: true) networkStore.executeUnregisterOne(address: peripheral.id, on: device) { @@ -501,6 +504,8 @@ final class BluetoothPeripheralStore: NSObject, ObservableObject, BluetoothPerip unregisterFromPC(peripheral) return } + // Peripheral is leaving this Mac for the peer — flash the sending arrow. + NotificationCenter.default.post(name: .magicSwitchPeripheralOutgoing, object: nil) unregisterFromPC(peripheral) waitForLocalDisconnect(of: peripheral) { success in guard success else {