diff --git a/CLAUDE.md b/CLAUDE.md index 202a1682..303a3108 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -203,6 +203,9 @@ shell.qml # Main entry point (minimal orchestration) - Services expose properties and functions - Widgets bind to service properties for reactive updates - Use service functions for actions: `ServiceName.performAction(value)` + - **CRITICAL**: DO NOT create wrapper functions for everything - bind directly to underlying APIs when possible + - Example: Use `BluetoothService.adapter.discovering = true` instead of `BluetoothService.startScan()` + - Example: Use `device.connect()` directly instead of `BluetoothService.connect(device.address)` ### Error Handling and Debugging @@ -320,3 +323,4 @@ When modifying the shell: - **Robustness**: Implement feature detection and graceful degradation - **Consistency**: Follow Material Design 3 principles via Theme singleton - **Performance**: Minimize expensive operations and use appropriate data structures +- **NO WRAPPER HELL**: Avoid creating unnecessary wrapper functions - bind directly to underlying APIs for better reactivity and performance diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 542601d2..dd5e4cba 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -21,15 +21,6 @@ Singleton { return dev && dev.paired && isValidDevice(dev); }); } - readonly property var availableDevices: { - if (!adapter || !adapter.discovering || !Bluetooth.devices) - return []; - - var filtered = Bluetooth.devices.values.filter((dev) => { - return dev && !dev.paired && !dev.pairing && !dev.blocked && isValidDevice(dev) && (dev.signalStrength === undefined || dev.signalStrength > 0); - }); - return sortBySignalStrength(filtered); - } readonly property var allDevicesWithBattery: { if (!adapter || !adapter.devices) return []; @@ -39,8 +30,17 @@ Singleton { }); } - function sortBySignalStrength(devices) { + function sortDevices(devices) { return devices.sort((a, b) => { + var aName = a.name || a.deviceName || ""; + var bName = b.name || b.deviceName || ""; + + var aHasRealName = aName.includes(" ") && aName.length > 3; + var bHasRealName = bName.includes(" ") && bName.length > 3; + + if (aHasRealName && !bHasRealName) return -1; + if (!aHasRealName && bHasRealName) return 1; + var aSignal = (a.signalStrength !== undefined && a.signalStrength > 0) ? a.signalStrength : 0; var bSignal = (b.signalStrength !== undefined && b.signalStrength > 0) ? b.signalStrength : 0; return bSignal - aSignal; @@ -121,7 +121,7 @@ Singleton { return "bluetooth"; } - function canPair(device) { + function canConnect(device) { if (!device) return false; @@ -132,21 +132,6 @@ Singleton { console.log("Device:", device.name, "paired:", device.paired, "connected:", device.connected, "signalStrength:", device.signalStrength); } - 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.signalStrength === undefined || device.signalStrength <= 0) @@ -188,71 +173,5 @@ Singleton { 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) => { - return d && d.address === address; - }) || (Bluetooth.devices ? Bluetooth.devices.values.find((d) => { - return d && d.address === address; - }) : null); - } } diff --git a/Widgets/ControlCenter/BluetoothTab.qml b/Widgets/ControlCenter/BluetoothTab.qml index 9e0da860..99ab8149 100644 --- a/Widgets/ControlCenter/BluetoothTab.qml +++ b/Widgets/ControlCenter/BluetoothTab.qml @@ -70,7 +70,9 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - BluetoothService.toggleAdapter(); + if (BluetoothService.adapter) { + BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled; + } } } @@ -208,7 +210,11 @@ Item { cursorShape: Qt.PointingHandCursor onClicked: { BluetoothService.debugDevice(modelData); - BluetoothService.toggle(modelData.address); + if (modelData.connected) { + modelData.disconnect(); + } else { + modelData.connect(); + } } } @@ -279,10 +285,9 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - if (BluetoothService.adapter && BluetoothService.adapter.discovering) - BluetoothService.stopScan(); - else - BluetoothService.startScan(); + if (BluetoothService.adapter) { + BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering; + } } } @@ -291,11 +296,18 @@ Item { } Repeater { - model: BluetoothService.availableDevices + model: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return []; + + var filtered = Bluetooth.devices.values.filter((dev) => { + return dev && !dev.paired && !dev.pairing && !dev.blocked && BluetoothService.isValidDevice(dev) && (dev.signalStrength === undefined || dev.signalStrength > 0); + }); + return BluetoothService.sortDevices(filtered); + } Rectangle { - property bool canPair: BluetoothService.canPair(modelData) - property string pairingStatus: BluetoothService.getPairingStatus(modelData) + property bool canConnect: BluetoothService.canConnect(modelData) width: parent.width height: 70 @@ -372,14 +384,11 @@ Item { Text { text: { - switch (pairingStatus) { - case "pairing": + if (modelData.pairing) return "Pairing..."; - case "blocked": + if (modelData.blocked) return "Blocked"; - default: - return BluetoothService.getSignalStrength(modelData); - } + return BluetoothService.getSignalStrength(modelData); } font.pixelSize: Theme.fontSizeSmall color: { @@ -398,14 +407,14 @@ Item { font.family: Theme.iconFont font.pixelSize: Theme.fontSizeSmall color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) - visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && pairingStatus === "available" + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked } Text { text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" font.pixelSize: Theme.fontSizeSmall color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) - visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && pairingStatus === "available" + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked } } @@ -424,7 +433,7 @@ Item { anchors.rightMargin: Theme.spacingM anchors.verticalCenter: parent.verticalCenter color: { - if (!canPair && !modelData.pairing) + if (!canConnect && !modelData.pairing) return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3); if (actionButtonArea.containsMouse) @@ -432,9 +441,9 @@ Item { return "transparent"; } - border.color: canPair || modelData.pairing ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.color: canConnect || modelData.pairing ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 1 - opacity: canPair || modelData.pairing ? 1 : 0.5 + opacity: canConnect || modelData.pairing ? 1 : 0.5 Text { anchors.centerIn: parent @@ -445,10 +454,10 @@ Item { if (modelData.blocked) return "Blocked"; - return "Pair"; + return "Connect"; } font.pixelSize: Theme.fontSizeSmall - color: canPair || modelData.pairing ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + color: canConnect || modelData.pairing ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) font.weight: Font.Medium } @@ -457,12 +466,15 @@ Item { anchors.fill: parent hoverEnabled: true - cursorShape: canPair ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: canPair + cursorShape: canConnect ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: canConnect onClicked: { - if (canPair) - BluetoothService.pair(modelData.address); - + if (canConnect) { + if (BluetoothService.adapter && BluetoothService.adapter.discovering) { + BluetoothService.adapter.discovering = false; + } + modelData.connect(); + } } } @@ -474,12 +486,15 @@ Item { anchors.fill: parent anchors.rightMargin: 90 // Don't overlap with action button hoverEnabled: true - cursorShape: canPair ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: canPair + cursorShape: canConnect ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: canConnect onClicked: { - if (canPair) - BluetoothService.pair(modelData.address); - + if (canConnect) { + if (BluetoothService.adapter && BluetoothService.adapter.discovering) { + BluetoothService.adapter.discovering = false; + } + modelData.connect(); + } } } @@ -490,7 +505,16 @@ Item { Column { width: parent.width spacing: Theme.spacingM - visible: BluetoothService.adapter && BluetoothService.adapter.discovering && BluetoothService.availableDevices.length === 0 + visible: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return false; + + var availableCount = Bluetooth.devices.values.filter((dev) => { + return dev && !dev.paired && !dev.pairing && !dev.blocked && BluetoothService.isValidDevice(dev) && (dev.signalStrength === undefined || dev.signalStrength > 0); + }).length; + + return availableCount === 0; + } Row { anchors.horizontalCenter: parent.horizontalCenter @@ -536,7 +560,16 @@ Item { text: "No devices found. Put your device in pairing mode and click Start Scanning." font.pixelSize: Theme.fontSizeMedium color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) - visible: BluetoothService.availableDevices.length === 0 && (!BluetoothService.adapter || !BluetoothService.adapter.discovering) + visible: { + if (!BluetoothService.adapter || !Bluetooth.devices) + return true; + + var availableCount = Bluetooth.devices.values.filter((dev) => { + return dev && !dev.paired && !dev.pairing && !dev.blocked && BluetoothService.isValidDevice(dev) && (dev.signalStrength === undefined || dev.signalStrength > 0); + }).length; + + return availableCount === 0 && !BluetoothService.adapter.discovering; + } wrapMode: Text.WordWrap width: parent.width horizontalAlignment: Text.AlignHCenter @@ -641,9 +674,13 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - if (bluetoothContextMenuWindow.deviceData) - BluetoothService.toggle(bluetoothContextMenuWindow.deviceData.address); - + if (bluetoothContextMenuWindow.deviceData) { + if (bluetoothContextMenuWindow.deviceData.connected) { + bluetoothContextMenuWindow.deviceData.disconnect(); + } else { + bluetoothContextMenuWindow.deviceData.connect(); + } + } bluetoothContextMenuWindow.hide(); } } @@ -711,9 +748,9 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - if (bluetoothContextMenuWindow.deviceData) - BluetoothService.forget(bluetoothContextMenuWindow.deviceData.address); - + if (bluetoothContextMenuWindow.deviceData) { + bluetoothContextMenuWindow.deviceData.forget(); + } bluetoothContextMenuWindow.hide(); } } diff --git a/Widgets/ControlCenter/ControlCenterPopup.qml b/Widgets/ControlCenter/ControlCenterPopup.qml index 8ab92ac6..ec731168 100644 --- a/Widgets/ControlCenter/ControlCenterPopup.qml +++ b/Widgets/ControlCenter/ControlCenterPopup.qml @@ -19,6 +19,10 @@ PanelWindow { onVisibleChanged: { // Enable/disable WiFi auto-refresh based on control center visibility WifiService.autoRefreshEnabled = visible && NetworkService.wifiEnabled; + // Stop bluetooth scanning when control center is closed + if (!visible && BluetoothService.adapter && BluetoothService.adapter.discovering) { + BluetoothService.adapter.discovering = false; + } } implicitWidth: 600 implicitHeight: 500