Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions airsync-mac/Assets.xcassets/logo.bluetooth.symbolset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "logo.bluetooth.svg",
"idiom" : "universal"
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 67 additions & 1 deletion airsync-mac/Core/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class AppState: ObservableObject {
private var clipboardCancellable: AnyCancellable?
private var lastClipboardValue: String? = nil
private var shouldSkipSave = false
private var cancellables = Set<AnyCancellable>()
private static let licenseDetailsKey = "licenseDetails"

@Published var isOS26: Bool = true
Expand Down Expand Up @@ -97,6 +98,20 @@ class AppState: ObservableObject {

self.licenseDetails = AppState.loadLicenseDetailsFromUserDefaults()

self.isBLEEnabled = UserDefaults.standard.bool(forKey: "isBLEEnabled")
self.isBLEAutoConnectEnabled = UserDefaults.standard.object(forKey: "isBLEAutoConnectEnabled") == nil ? true : UserDefaults.standard.bool(forKey: "isBLEAutoConnectEnabled")

if isBLEEnabled {
BLECentralManager.shared.startScanning()
}

BLECentralManager.shared.$connectionStatus
.receive(on: RunLoop.main)
.sink { [weak self] status in
self?.handleBLEStatusChange(status)
}
.store(in: &cancellables)

if isClipboardSyncEnabled {
startClipboardMonitoring()
}
Expand Down Expand Up @@ -190,7 +205,7 @@ class AppState: ObservableObject {
@Published var isNativeMirroring: Bool = false

var isConnectedOverLocalNetwork: Bool {
guard let ip = device?.ipAddress else { return true }
guard let ip = device?.ipAddress, ip != "BLE" else { return false }
// Tailscale IPs usually start with 100.
return !ip.hasPrefix("100.")
}
Expand Down Expand Up @@ -295,6 +310,24 @@ class AppState: ObservableObject {
}
}

@Published var isBLEEnabled: Bool {
didSet {
UserDefaults.standard.set(isBLEEnabled, forKey: "isBLEEnabled")
if isBLEEnabled {
BLECentralManager.shared.startScanning()
} else {
BLECentralManager.shared.stopScanning()
BLECentralManager.shared.disconnect()
}
}
}

@Published var isBLEAutoConnectEnabled: Bool {
didSet {
UserDefaults.standard.set(isBLEAutoConnectEnabled, forKey: "isBLEAutoConnectEnabled")
}
}

@Published var alwaysOpenWindow: Bool {
didSet {
UserDefaults.standard.set(alwaysOpenWindow, forKey: "alwaysOpenWindow")
Expand Down Expand Up @@ -691,6 +724,9 @@ class AppState: ObservableObject {
self.status = nil
self.currentDeviceWallpaperBase64 = nil

// Disconnect BLE
BLECentralManager.shared.disconnect()

// Clean up Quick Share state
if QuickShareManager.shared.transferState != .idle {
QuickShareManager.shared.transferState = .idle
Expand Down Expand Up @@ -952,6 +988,9 @@ class AppState: ObservableObject {
}
"""
WebSocketServer.shared.sendClipboardUpdate(message)

// Also send via BLE
BLETransportBridge.shared.sendClipboard(text)
}

func updateClipboardFromAndroid(_ text: String) {
Expand Down Expand Up @@ -1183,6 +1222,33 @@ class AppState: ObservableObject {
}
}

private func handleBLEStatusChange(_ status: BLECentralManager.BLEConnectionStatus) {
if status == .authenticated {
if self.device == nil {
updateVirtualDeviceForBLE()
}
} else if status == .disconnected {
// Only clear device if it's the virtual BLE device
if self.device?.ipAddress == "BLE" {
self.device = nil
self.status = nil
self.notifications = []
}
}
}

private func updateVirtualDeviceForBLE() {
let name = BLECentralManager.shared.connectedDeviceName ?? "Android Device"
self.device = Device(
name: name,
ipAddress: "BLE",
port: 0,
version: "2.0.0",
adbPorts: []
)
print("[state] (BLE) Created virtual device: \(name)")
}

/// Revalidates the current network adapter selection and falls back to auto if no longer valid
func revalidateNetworkAdapter() {
let currentSelection = selectedNetworkAdapterName
Expand Down
Loading