Bluetooth WIP but way more reliable than before

This commit is contained in:
quadbyte 2025-08-17 10:43:17 -04:00
parent a390af2aae
commit d2acdd1c19
3 changed files with 371 additions and 593 deletions

View file

@ -15,18 +15,11 @@ NIconButton {
showBorder: false
visible: bluetoothEnabled
Component.onCompleted: {
Logger.log("Bluetooth", "Component loaded, bluetoothEnabled:", bluetoothEnabled)
Logger.log("Bluetooth", "BluetoothService available:", typeof BluetoothService !== 'undefined')
if (typeof BluetoothService !== 'undefined') {
Logger.log("Bluetooth", "Connected devices:", BluetoothService.connectedDevices.length)
}
}
icon: {
// Show different icons based on connection status
if (BluetoothService.connectedDevices.length > 0) {
if (BluetoothService.pairedDevices.length > 0) {
return "bluetooth_connected"
} else if (BluetoothService.isDiscovering) {
} else if (BluetoothService.discovering) {
return "bluetooth_searching"
} else {
return "bluetooth"

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Commons
import qs.Services
@ -38,7 +39,7 @@ NLoader {
onVisibleChanged: {
if (visible && Settings.data.network.bluetoothEnabled) {
// Always refresh devices when menu opens to get fresh device objects
BluetoothService.refreshDevices()
BluetoothService.adapter.discovering = true
} else if (bluetoothMenuRect.opacityValue > 0) {
// Start hide animation
bluetoothMenuRect.scaleValue = 0.8
@ -63,11 +64,14 @@ NLoader {
Rectangle {
id: bluetoothMenuRect
property var deviceData: null
color: Color.mSurface
radius: Style.radiusLarge * scaling
border.color: Color.mOutlineVariant
border.width: Math.max(1, Style.borderThin * scaling)
width: 340 * scaling
width: 400 * scaling
height: 500 * scaling
anchors.top: parent.top
anchors.right: parent.right
@ -81,6 +85,11 @@ NLoader {
scale: scaleValue
opacity: opacityValue
// Prevent closing the window if clicking inside it
MouseArea {
anchors.fill: parent
}
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
@ -107,6 +116,7 @@ NLoader {
anchors.margins: Style.marginLarge * scaling
spacing: Style.marginMedium * scaling
// HEADER
RowLayout {
Layout.fillWidth: true
spacing: Style.marginMedium * scaling
@ -121,18 +131,19 @@ NLoader {
NText {
text: "Bluetooth"
font.pointSize: Style.fontSizeLarge * scaling
font.bold: true
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: "refresh"
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh"
tooltipText: "Refresh Devices"
sizeMultiplier: 0.8
enabled: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering
onClicked: {
BluetoothService.refreshDevices()
if (BluetoothService.adapter) {
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
}
@ -148,246 +159,310 @@ NLoader {
NDivider {}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
// Available devices
Column {
id: column
// Loading indicator
ColumnLayout {
anchors.centerIn: parent
visible: Settings.data.network.bluetoothEnabled && BluetoothService.isDiscovering
width: parent.width
spacing: Style.marginMedium * scaling
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
NBusyIndicator {
running: BluetoothService.isDiscovering
color: Color.mPrimary
size: Style.baseWidgetSize * scaling
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Scanning for devices..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
// Bluetooth disabled message
ColumnLayout {
anchors.centerIn: parent
visible: !Settings.data.network.bluetoothEnabled
spacing: Style.marginMedium * scaling
NText {
text: "bluetooth_disabled"
font.family: "Material Symbols Outlined"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Bluetooth is disabled"
font.pointSize: Style.fontSizeLarge * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Bluetooth to see available devices"
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
// Device list
ListView {
id: deviceList
anchors.fill: parent
visible: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering
model: []
spacing: Style.marginMedium * scaling
clip: true
// Combine all devices into a single list for the ListView
property var allDevices: {
const devices = []
// Add connected devices first
for (const device of BluetoothService.connectedDevices) {
devices.push({
"device": device,
"type": 'connected',
"section": 'Connected Devices'
})
}
// Add paired devices
for (const device of BluetoothService.pairedDevices) {
devices.push({
"device": device,
"type": 'paired',
"section": 'Paired Devices'
})
}
// Add available devices
for (const device of BluetoothService.availableDevices) {
devices.push({
"device": device,
"type": 'available',
"section": 'Available Devices'
})
}
return devices
}
// Update model when devices change
onAllDevicesChanged: {
deviceList.model = allDevices
}
// Also watch for changes in the service arrays
Connections {
target: BluetoothService
function onConnectedDevicesChanged() {
deviceList.model = deviceList.allDevices
}
function onPairedDevicesChanged() {
deviceList.model = deviceList.allDevices
}
function onAvailableDevicesChanged() {
deviceList.model = deviceList.allDevices
}
}
delegate: Item {
width: parent ? parent.width : 0
height: Style.baseWidgetSize * 1.5 * scaling
Rectangle {
anchors.fill: parent
radius: Style.radiusMedium * scaling
color: modelData.device.connected ? Color.mPrimary : (deviceMouseArea.containsMouse ? Color.mTertiary : Color.transparent)
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginSmall * scaling
width: parent.width
spacing: Style.marginMedium * scaling
NText {
text: "Available Devices"
font.pointSize: Style.fontSizeLarge * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
}
}
Repeater {
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 && (dev.signalStrength === undefined
|| dev.signalStrength > 0)
})
return BluetoothService.sortDevices(filtered)
}
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 70
radius: Style.radiusMedium * scaling
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.08)
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.12)
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
}
border.color: {
if (modelData.pairing)
return Color.mError
if (modelData.blocked)
return Color.mError
return Color.mOutline
}
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Style.marginMedium * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginSmall * scaling
NText {
text: BluetoothService.getDeviceIcon(modelData.device)
text: BluetoothService.getDeviceIcon(modelData)
font.family: "Material Symbols Outlined"
font.pointSize: Style.fontSizeXL * scaling
color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
color: {
if (modelData.pairing)
return Color.mError
if (modelData.blocked)
return Color.mError
return Color.mOnSurface
}
anchors.verticalCenter: parent.verticalCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginTiny * scaling
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
NText {
text: modelData.device.name || modelData.device.deviceName || "Unknown Device"
font.pointSize: Style.fontSizeNormal * scaling
elide: Text.ElideRight
Layout.fillWidth: true
color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
text: modelData.name || modelData.deviceName
font.pointSize: Style.fonttSizeMedium * scaling
color: {
if (modelData.pairing)
return Color.mError
if (modelData.blocked)
return Color.mError
return Color.mOnSurface
}
font.weight: modelData.pairing ? Style.fontWeightMedium : Font.Normal
}
Row {
spacing: Style.marginTiny * scaling
Row {
spacing: Style.marginSmall * spacing
NText {
text: {
if (modelData.device.connected) {
return "Connected"
} else if (modelData.device.paired) {
return "Paired"
} else {
return "Available"
}
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pointSize: Style.fontSizeSmall * scaling
color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurfaceVariant)
color: {
if (modelData.pairing)
return Color.mError
if (modelData.blocked)
return Theme.error
return Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7)
}
}
NText {
text: BluetoothService.getBatteryText(modelData.device)
text: BluetoothService.getSignalIcon(modelData)
font.family: "Material Symbols Outlined"
font.pointSize: Style.fontSizeSmall * scaling
color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurfaceVariant)
visible: modelData.device.batteryAvailable
color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7)
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeSmall * scaling
color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.5)
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
}
}
}
}
Item {
Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling
visible: modelData.device.pairing || modelData.device.state === 2 // Connecting state
Rectangle {
width: 80
height: 28
radius: Style.radiusMedium * scaling
anchors.right: parent.right
anchors.rightMargin: Style.marginMedium * scaling
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: {
if (!canConnect && !isBusy)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
NBusyIndicator {
visible: modelData.device.pairing || modelData.device.state === 2
running: modelData.device.pairing || modelData.device.state === 2
color: Color.mPrimary
if (actionButtonArea.containsMouse && !isBusy)
return Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.12)
return "transparent"
}
border.color: canConnect || isBusy ? Color.mPrimary : Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
opacity: canConnect || isBusy ? 1 : 0.5
NText {
anchors.centerIn: parent
size: Style.baseWidgetSize * 0.7 * scaling
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return "Connect"
}
font.pointSize: Style.fontSizeSmall * scaling
color: canConnect || isBusy ? Color.mPrimary : Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g,
Color.mOnSurface.b, 0.5)
font.weight: Style.fontWeightMedium
}
NText {
visible: modelData.device.connected
text: "connected"
font.pointSize: Style.fontSizeSmall * scaling
color: modelData.device.connected ? Color.mSurface : (deviceMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
MouseArea {
id: deviceMouseArea
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData.device.connected) {
BluetoothService.disconnectDevice(modelData.device)
} else if (modelData.device.paired) {
BluetoothService.connectDevice(modelData.device)
} else {
BluetoothService.pairDevice(modelData.device)
}
}
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
// Empty state when no devices found
ColumnLayout {
anchors.centerIn: parent
visible: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering
&& deviceList.count === 0
Column {
width: parent.width
spacing: Style.marginMedium * scaling
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
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.marginMedium * scaling
NText {
text: "bluetooth_disabled"
text: "sync"
font.family: "Material Symbols Outlined"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
font.pointSize: 32 * scaling
color: Color.mPrimary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
}
NText {
text: "No Bluetooth devices"
text: "Scanning for devices..."
font.pointSize: Style.fontSizeLarge * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
}
}
NText {
text: "Click the refresh button to discover devices"
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
text: "Make sure your device is in pairing mode"
font.pointSize: Style.fontSizeMedium * scaling
color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
NText {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pointSize: Style.fontSizeMedium * scaling
color: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.7)
visible: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return true
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0 && !BluetoothService.adapter.discovering
}
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
// This item takes up all the remaining vertical space.
Item {
Layout.fillHeight: true
}
}
}

View file

@ -3,432 +3,142 @@ pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Bluetooth
import qs.Commons
Singleton {
id: root
// Bluetooth state properties
property bool isEnabled: Settings.data.network.bluetoothEnabled
property bool isDiscovering: false
property var connectedDevices: []
property var pairedDevices: []
property var availableDevices: []
property string lastConnectedDevice: ""
property string connectStatus: ""
property string connectStatusDevice: ""
property string connectError: ""
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
readonly property bool available: adapter !== null
readonly property bool enabled: (adapter && adapter.enabled) ?? false
readonly property bool discovering: (adapter && adapter.discovering) ?? false
readonly property var devices: adapter ? adapter.devices : null
readonly property var pairedDevices: {
if (!adapter || !adapter.devices)
return []
// Timer for refreshing device lists
property Timer refreshTimer: Timer {
interval: 5000 // Refresh every 5 seconds when discovery is active
repeat: true
running: root.isEnabled && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering
onTriggered: root.refreshDevices()
}
Component.onCompleted: {
Logger.log("Bluetooth", "Service started")
if (isEnabled && Bluetooth.defaultAdapter) {
// Ensure adapter is enabled
if (!Bluetooth.defaultAdapter.enabled) {
Bluetooth.defaultAdapter.enabled = true
}
// Start discovery to find devices
if (!Bluetooth.defaultAdapter.discovering) {
Bluetooth.defaultAdapter.discovering = true
}
// Refresh devices after a short delay to allow discovery to start
Qt.callLater(function () {
refreshDevices()
return adapter.devices.values.filter(dev => {
return dev && (dev.paired || dev.trusted)
})
}
readonly property var allDevicesWithBattery: {
if (!adapter || !adapter.devices)
return []
return adapter.devices.values.filter(dev => {
return dev && dev.batteryAvailable && dev.battery > 0
})
}
// Function to enable/disable Bluetooth
function setBluetoothEnabled(enabled) {
function sortDevices(devices) {
return devices.sort((a, b) => {
var aName = a.name || a.deviceName || ""
var bName = b.name || b.deviceName || ""
if (enabled) {
// Store the currently connected devices before enabling
for (const device of connectedDevices) {
if (device.connected) {
lastConnectedDevice = device.name || device.deviceName
break
}
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
})
}
// Enable Bluetooth
if (Bluetooth.defaultAdapter) {
Bluetooth.defaultAdapter.enabled = true
// Start discovery to find devices
if (!Bluetooth.defaultAdapter.discovering) {
Bluetooth.defaultAdapter.discovering = true
}
// Refresh devices after enabling
Qt.callLater(refreshDevices)
} else {
Logger.warn("Bluetooth", "No adapter found")
}
} else {
// Disconnect from current devices before disabling
for (const device of connectedDevices) {
if (device.connected) {
device.disconnect()
}
}
// Disable Bluetooth
if (Bluetooth.defaultAdapter) {
Logger.log("Bluetooth", "Disabling adapter")
Bluetooth.defaultAdapter.enabled = false
}
}
Settings.data.network.bluetoothEnabled = enabled
isEnabled = enabled
}
// Function to refresh device lists
function refreshDevices() {
if (!isEnabled || !Bluetooth.defaultAdapter) {
connectedDevices = []
pairedDevices = []
availableDevices = []
return
}
Logger.log("Bluetooth", "refreshDevices")
// Remove duplicate check since we already did it above
const connected = []
const paired = []
const available = []
let devices = null
// Try adapter devices first
if (Bluetooth.defaultAdapter.enabled && Bluetooth.defaultAdapter.devices) {
devices = Bluetooth.defaultAdapter.devices
}
// Fallback to global devices list
if (!devices && Bluetooth.devices) {
devices = Bluetooth.devices
}
if (!devices) {
connectedDevices = []
pairedDevices = []
availableDevices = []
return
}
// Use Qt model methods to iterate through the ObjectModel
let deviceFound = false
try {
// Get the row count using the Qt model method
const rowCount = devices.rowCount()
if (rowCount > 0) {
// Iterate through each row using the Qt model data() method
for (var i = 0; i < rowCount; i++) {
try {
// Create a model index for this row
const modelIndex = devices.index(i, 0)
if (!modelIndex.valid)
continue
// Get the device object using the Qt.UserRole (typically 256)
const device = devices.data(modelIndex, 256) // Qt.UserRole
if (!device) {
// Try alternative role values
const deviceAlt = devices.data(modelIndex, 0) // Qt.DisplayRole
if (deviceAlt) {
device = deviceAlt
} else {
continue
}
}
deviceFound = true
if (device.connected) {
connected.push(device)
} else if (device.paired) {
paired.push(device)
} else {
available.push(device)
}
} catch (e) {
// Silent error handling
}
}
}
// Alternative method: try the values property if available
if (!deviceFound && devices.values) {
try {
const values = devices.values
if (values && typeof values === 'object') {
// Try to iterate through values if it's iterable
if (values.length !== undefined) {
for (var i = 0; i < values.length; i++) {
const device = values[i]
if (device) {
deviceFound = true
if (device.connected) {
connected.push(device)
} else if (device.paired) {
paired.push(device)
} else {
available.push(device)
}
}
}
}
}
} catch (e) {
// Silent error handling
}
}
} catch (e) {
Logger.warn("Bluetooth", "Error accessing device model:", e)
}
if (!deviceFound) {
Logger.log("Bluetooth", "No device found")
}
connectedDevices = connected
pairedDevices = paired
availableDevices = available
}
// Function to start discovery
function startDiscovery() {
if (!isEnabled || !Bluetooth.defaultAdapter)
return
isDiscovering = true
Bluetooth.defaultAdapter.discovering = true
}
// Function to stop discovery
function stopDiscovery() {
if (!Bluetooth.defaultAdapter)
return
isDiscovering = false
Bluetooth.defaultAdapter.discovering = false
}
// Function to connect to a device
function connectDevice(device) {
if (!device)
return
// Check if device is still valid (not stale from previous session)
if (!device.connect || typeof device.connect !== 'function') {
Logger.warn("Bluetooth", "Device object is stale, refreshing devices")
refreshDevices()
return
}
connectStatus = "connecting"
connectStatusDevice = device.name || device.deviceName
connectError = ""
try {
device.connect()
} catch (error) {
Logger.error("Bluetooth", "Error connecting to device:", error)
connectStatus = "error"
connectError = error.toString()
Qt.callLater(refreshDevices)
}
}
// Function to disconnect from a device
function disconnectDevice(device) {
if (!device)
return
// Check if device is still valid (not stale from previous session)
if (!device.disconnect || typeof device.disconnect !== 'function') {
Logger.warn("Bluetooth", "Device object is stale, refreshing devices")
refreshDevices()
return
}
try {
device.disconnect()
// Clear connection status
connectStatus = ""
connectStatusDevice = ""
connectError = ""
} catch (error) {
Logger.warn("Bluetooth", "Error disconnecting device:", error)
Qt.callLater(refreshDevices)
}
}
// Function to pair with a device
function pairDevice(device) {
if (!device)
return
// Check if device is still valid (not stale from previous session)
if (!device.pair || typeof device.pair !== 'function') {
Logger.warn("Bluetooth", "Device object is stale, refreshing devices")
refreshDevices()
return
}
try {
device.pair()
} catch (error) {
Logger.warn("Bluetooth", "Error pairing device:", error)
Qt.callLater(refreshDevices)
}
}
// Function to forget a device
function forgetDevice(device) {
if (!device)
return
// Check if device is still valid (not stale from previous session)
if (!device.forget || typeof device.forget !== 'function') {
Logger.warn("Bluetooth", "Device object is stale, refreshing devices")
refreshDevices()
return
}
// Store device info before forgetting (in case device object becomes invalid)
const deviceName = device.name || device.deviceName || "Unknown Device"
try {
device.forget()
// Clear any connection status that might be related to this device
if (connectStatusDevice === deviceName) {
connectStatus = ""
connectStatusDevice = ""
connectError = ""
}
// Refresh devices after a delay to ensure the forget operation is complete
Qt.callLater(refreshDevices, 1000)
} catch (error) {
Logger.warn("Bluetooth", "Error forgetting device:", error)
Qt.callLater(refreshDevices, 500)
}
}
// Function to get device icon
function getDeviceIcon(device) {
if (!device)
return "bluetooth"
// Use device icon if available, otherwise fall back to device type
if (device.icon) {
return device.icon
}
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"
// Fallback icons based on common device types
const name = (device.name || device.deviceName || "").toLowerCase()
if (name.includes("headphone") || name.includes("earbud") || name.includes("airpods")) {
return "headphones"
} else if (name.includes("speaker")) {
return "speaker"
} else if (name.includes("keyboard")) {
return "keyboard"
} else if (name.includes("mouse")) {
if (icon.includes("mouse") || name.includes("mouse"))
return "mouse"
} else if (name.includes("phone") || name.includes("mobile")) {
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"
} else if (name.includes("laptop") || name.includes("computer")) {
return "laptop"
}
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 to get device status text
function getDeviceStatus(device) {
function canConnect(device) {
if (!device)
return ""
return false
if (device.connected) {
return "Connected"
} else if (device.pairing) {
return "Pairing..."
} else if (device.paired) {
return "Paired"
} else {
return "Available"
}
return !device.paired && !device.pairing && !device.blocked
}
// Function to get battery level text
function getBatteryText(device) {
if (!device || !device.batteryAvailable)
return ""
function getSignalStrength(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
return "Unknown"
const percentage = Math.round(device.battery * 100)
return `${percentage}%`
var signal = device.signalStrength
if (signal >= 80)
return "Excellent"
if (signal >= 60)
return "Good"
if (signal >= 40)
return "Fair"
if (signal >= 20)
return "Poor"
return "Very Poor"
}
// Watch for Bluetooth adapter changes
Connections {
target: Bluetooth.defaultAdapter
ignoreUnknownSignals: true
function getSignalIcon(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
return "signal_cellular_null"
function onEnabledChanged() {
root.isEnabled = Bluetooth.defaultAdapter.enabled
Settings.data.network.bluetoothEnabled = root.isEnabled
if (root.isEnabled) {
Qt.callLater(refreshDevices)
} else {
connectedDevices = []
pairedDevices = []
availableDevices = []
}
var signal = device.signalStrength
if (signal >= 80)
return "signal_cellular_4_bar"
if (signal >= 60)
return "signal_cellular_3_bar"
if (signal >= 40)
return "signal_cellular_2_bar"
if (signal >= 20)
return "signal_cellular_1_bar"
return "signal_cellular_0_bar"
}
function onDiscoveringChanged() {
root.isDiscovering = Bluetooth.defaultAdapter.discovering
if (Bluetooth.defaultAdapter.discovering) {
Qt.callLater(refreshDevices)
}
function isDeviceBusy(device) {
if (!device)
return false
return device.pairing || device.state === BluetoothDeviceState.Disconnecting
|| device.state === BluetoothDeviceState.Connecting
}
function onStateChanged() {
if (Bluetooth.defaultAdapter.state >= 4) {
Qt.callLater(refreshDevices)
}
}
function connectDeviceWithTrust(device) {
if (!device)
return
function onDevicesChanged() {
Qt.callLater(refreshDevices)
}
}
// Watch for global device changes
Connections {
target: Bluetooth
ignoreUnknownSignals: true
function onDevicesChanged() {
Qt.callLater(refreshDevices)
}
device.trusted = true
device.connect()
}
}