pragma Singleton pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Bluetooth Singleton { id: root readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter readonly property bool available: adapter !== null readonly property bool enabled: adapter?.enabled ?? false readonly property bool discovering: adapter?.discovering ?? false readonly property var devices: adapter ? adapter.devices : null function sortByRssi(devices) { return devices.sort((a, b) => { var aRssi = (a.rssi !== undefined && a.rssi !== 0) ? a.rssi : -100 var bRssi = (b.rssi !== undefined && b.rssi !== 0) ? b.rssi : -100 return bRssi - aRssi }) } readonly property var pairedDevices: { if (!adapter || !adapter.devices) return [] return adapter.devices.values.filter(dev => dev && dev.paired && isValidDevice(dev)) } readonly property var availableDevices: { if (!adapter || !adapter.discovering || !Bluetooth.devices) return [] var filtered = Bluetooth.devices.values .filter(dev => dev && !dev.paired && !dev.pairing && !dev.blocked && isValidDevice(dev) && (dev.rssi === undefined || dev.rssi !== 0)) return sortByRssi(filtered) } readonly property var allDevicesWithBattery: { if (!adapter || !adapter.devices) return [] return adapter.devices.values.filter(dev => dev && dev.batteryAvailable && dev.battery > 0) } function isValidDevice(device) { if (!device) return false var displayName = device.name || device.deviceName if (!displayName || displayName.length < 2) return false if (displayName.startsWith('/org/bluez') || displayName.includes('hci0')) return false return displayName.length >= 3 } function getDeviceIcon(device) { if (!device) return "bluetooth" var name = (device.name || device.deviceName || "").toLowerCase() var icon = (device.icon || "").toLowerCase() if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") || name.includes("headset") || name.includes("arctis")) return "headset" if (icon.includes("mouse") || name.includes("mouse")) return "mouse" if (icon.includes("keyboard") || name.includes("keyboard")) return "keyboard" if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") || name.includes("samsung")) return "smartphone" if (icon.includes("watch") || name.includes("watch")) return "watch" if (icon.includes("speaker") || name.includes("speaker")) return "speaker" if (icon.includes("display") || name.includes("tv")) return "tv" return "bluetooth" } function getDeviceType(device) { if (!device) return "bluetooth" var name = (device.name || device.deviceName || "").toLowerCase() var icon = (device.icon || "").toLowerCase() if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") || name.includes("headset") || name.includes("arctis")) return "headset" if (icon.includes("mouse") || name.includes("mouse")) return "mouse" if (icon.includes("keyboard") || name.includes("keyboard")) return "keyboard" if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") || name.includes("samsung")) return "phone" if (icon.includes("watch") || name.includes("watch")) return "watch" if (icon.includes("speaker") || name.includes("speaker")) return "speaker" if (icon.includes("display") || name.includes("tv")) return "tv" return "bluetooth" } function canPair(device) { if (!device) return false return !device.paired && !device.pairing && !device.blocked } function debugDevice(device) { console.log("Device:", device.name, "paired:", device.paired, "connected:", device.connected, "rssi:", device.rssi) } function getPairingStatus(device) { if (!device) return "unknown" if (device.pairing) return "pairing" if (device.paired) return "paired" if (device.blocked) return "blocked" return "available" } function getSignalStrength(device) { if (!device || device.rssi === undefined || device.rssi === 0) return "Unknown" var rssi = device.rssi if (rssi >= -50) return "Excellent" if (rssi >= -60) return "Good" if (rssi >= -70) return "Fair" if (rssi >= -80) return "Poor" return "Very Poor" } function getSignalIcon(device) { if (!device || device.rssi === undefined || device.rssi === 0) return "signal_cellular_null" var rssi = device.rssi if (rssi >= -50) return "signal_cellular_4_bar" if (rssi >= -60) return "signal_cellular_3_bar" if (rssi >= -70) return "signal_cellular_2_bar" if (rssi >= -80) return "signal_cellular_1_bar" return "signal_cellular_0_bar" } function toggleAdapter() { if (adapter) adapter.enabled = !adapter.enabled } function startScan() { if (adapter) adapter.discovering = true } function stopScan() { if (adapter) adapter.discovering = false } function connect(address) { var device = _findDevice(address) if (device) device.connect() } function disconnect(address) { var device = _findDevice(address) if (device) device.disconnect() } function pair(address) { var device = _findDevice(address) if (device && canPair(device)) device.pair() } function forget(address) { var device = _findDevice(address) if (device) device.forget() } function toggle(address) { var device = _findDevice(address) if (device) { if (device.connected) device.disconnect() else device.connect() } } function _findDevice(address) { if (!adapter) return null return adapter.devices.values.find(d => d && d.address === address) || (Bluetooth.devices ? Bluetooth.devices.values.find(d => d && d.address === address) : null) } }