Improve Bluetooth panel connected devices
This commit is contained in:
@@ -29,11 +29,51 @@ function isAddressLike(value) {
|
||||
return /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(text)
|
||||
}
|
||||
|
||||
function normalizedAddress(value) {
|
||||
return String(value || "").trim().toLowerCase().replace(/[^0-9a-f]/g, "")
|
||||
}
|
||||
|
||||
function hasHumanName(device) {
|
||||
var label = deviceLabel(device)
|
||||
return label !== "" && !isUuidLike(label) && !isAddressLike(label)
|
||||
}
|
||||
|
||||
function nodeProps(node) {
|
||||
return node && node.ready && node.properties ? node.properties : {}
|
||||
}
|
||||
|
||||
function nodeText(node) {
|
||||
var props = nodeProps(node)
|
||||
return [
|
||||
node ? node.name : "",
|
||||
node ? node.description : "",
|
||||
node ? node.nickname : "",
|
||||
node ? node.nick : "",
|
||||
props["node.name"],
|
||||
props["node.description"],
|
||||
props["node.nick"],
|
||||
props["device.name"],
|
||||
props["device.description"],
|
||||
props["device.product.name"],
|
||||
props["device.alias"],
|
||||
props["device.string"],
|
||||
props["api.bluez5.address"],
|
||||
props["bluez5.address"],
|
||||
props["media.name"]
|
||||
].join(" ").toLowerCase()
|
||||
}
|
||||
|
||||
function bluetoothSinkMatchesDevice(node, device) {
|
||||
if (!node || !node.isSink || node.isStream || !device) return false
|
||||
|
||||
var address = normalizedAddress(device.address)
|
||||
var text = nodeText(node)
|
||||
if (address !== "" && normalizedAddress(text).indexOf(address) !== -1) return true
|
||||
|
||||
var label = deviceLabel(device).toLowerCase()
|
||||
return label !== "" && text.indexOf(label) !== -1
|
||||
}
|
||||
|
||||
function sortedByLabel(devices) {
|
||||
var list = toArray(devices)
|
||||
list.sort(function(a, b) { return deviceLabel(a).localeCompare(deviceLabel(b)) })
|
||||
@@ -101,7 +141,11 @@ if (typeof module !== "undefined") {
|
||||
toArray: toArray,
|
||||
isUuidLike: isUuidLike,
|
||||
isAddressLike: isAddressLike,
|
||||
normalizedAddress: normalizedAddress,
|
||||
hasHumanName: hasHumanName,
|
||||
nodeProps: nodeProps,
|
||||
nodeText: nodeText,
|
||||
bluetoothSinkMatchesDevice: bluetoothSinkMatchesDevice,
|
||||
sortedByLabel: sortedByLabel,
|
||||
deviceLists: deviceLists,
|
||||
cloneMap: cloneMap,
|
||||
|
||||
@@ -3,6 +3,7 @@ import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Ui
|
||||
import qs.Commons
|
||||
import "Model.js" as Model
|
||||
@@ -19,6 +20,9 @@ Panel {
|
||||
|
||||
readonly property var adapter: Bluetooth.defaultAdapter
|
||||
readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : []
|
||||
readonly property var pipewireNodes: Pipewire.nodes ? Pipewire.nodes.values : []
|
||||
property var pendingAudioOutputDevice: null
|
||||
property int pendingAudioOutputAttempts: 0
|
||||
|
||||
function deviceLabel(device) {
|
||||
return Model.deviceLabel(device)
|
||||
@@ -112,6 +116,64 @@ Panel {
|
||||
return Model.sectionDevices(deviceGroups, section)
|
||||
}
|
||||
|
||||
function audioSinks() {
|
||||
var sinks = []
|
||||
for (var i = 0; i < pipewireNodes.length; i++) {
|
||||
var node = pipewireNodes[i]
|
||||
if (node && node.isSink && !node.isStream) sinks.push(node)
|
||||
}
|
||||
return sinks
|
||||
}
|
||||
|
||||
function bluetoothAudioSink(device) {
|
||||
var sinks = audioSinks()
|
||||
for (var i = 0; i < sinks.length; i++) {
|
||||
if (Model.bluetoothSinkMatchesDevice(sinks[i], device)) return sinks[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function setDefaultAudioSink(sink) {
|
||||
if (!sink) return
|
||||
Pipewire.preferredDefaultAudioSink = sink
|
||||
if (root.bar && root.bar.omarchyPath && sink.id !== undefined && sink.name) {
|
||||
Quickshell.execDetached([
|
||||
root.bar.omarchyPath + "/bin/omarchy-audio-output-set-default",
|
||||
String(sink.id),
|
||||
String(sink.name)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleAudioOutputSwitch(device) {
|
||||
pendingAudioOutputDevice = {
|
||||
address: device && device.address ? device.address : "",
|
||||
name: device && device.name ? device.name : "",
|
||||
deviceName: device && device.deviceName ? device.deviceName : ""
|
||||
}
|
||||
pendingAudioOutputAttempts = 0
|
||||
audioSwitchTimer.restart()
|
||||
}
|
||||
|
||||
function switchPendingAudioOutput() {
|
||||
if (!pendingAudioOutputDevice) return
|
||||
|
||||
var sink = bluetoothAudioSink(pendingAudioOutputDevice)
|
||||
if (sink) {
|
||||
setDefaultAudioSink(sink)
|
||||
pendingAudioOutputDevice = null
|
||||
audioSwitchTimer.stop()
|
||||
return
|
||||
}
|
||||
|
||||
pendingAudioOutputAttempts += 1
|
||||
if (pendingAudioOutputAttempts >= 8) {
|
||||
pendingAudioOutputDevice = null
|
||||
return
|
||||
}
|
||||
audioSwitchTimer.restart()
|
||||
}
|
||||
|
||||
function deviceAt(section, index) {
|
||||
var list = devicesForSection(section)
|
||||
return index >= 0 && index < list.length ? list[index] : null
|
||||
@@ -179,9 +241,11 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
if ((action === "connecting" && found && found.connected)
|
||||
var finishedConnecting = action === "connecting" && found && found.connected
|
||||
if (finishedConnecting
|
||||
|| (action === "disconnecting" && found && !found.connected)
|
||||
|| (action === "forgetting" && (!found || (!found.paired && !found.bonded && !found.trusted)))) {
|
||||
if (finishedConnecting) scheduleAudioOutputSwitch(found)
|
||||
delete next[address]
|
||||
changed = true
|
||||
}
|
||||
@@ -360,6 +424,13 @@ Panel {
|
||||
onTriggered: root.pendingActions = ({})
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: audioSwitchTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: root.switchPendingAudioOutput()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: phraseTimer
|
||||
interval: 2800
|
||||
@@ -512,6 +583,39 @@ Panel {
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
Column {
|
||||
id: connectedList
|
||||
visible: root.connectedDevices.length > 0
|
||||
width: parent.width
|
||||
spacing: Style.space(10)
|
||||
|
||||
PanelSectionHeader {
|
||||
text: "CONNECTED"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.connectedDevices
|
||||
DeviceRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: connectedList.width
|
||||
dev: modelData
|
||||
rowIndex: index
|
||||
sectionName: "connected"
|
||||
isDiscovered: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelSeparator {
|
||||
visible: root.connectedDevices.length > 0
|
||||
&& (root.knownDevices.length > 0
|
||||
|| (root.adapter && root.adapter.discovering && root.discoveredDevices.length > 0))
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: deviceFlick
|
||||
width: parent.width
|
||||
@@ -528,33 +632,7 @@ Panel {
|
||||
width: parent.width
|
||||
spacing: Style.space(10)
|
||||
|
||||
// Connected devices.
|
||||
PanelSectionHeader {
|
||||
visible: root.connectedDevices.length > 0
|
||||
text: "CONNECTED"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.connectedDevices
|
||||
DeviceRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: deviceList.width
|
||||
dev: modelData
|
||||
rowIndex: index
|
||||
sectionName: "connected"
|
||||
isDiscovered: false
|
||||
}
|
||||
}
|
||||
|
||||
// Remembered devices.
|
||||
PanelSeparator {
|
||||
visible: root.connectedDevices.length > 0 && root.knownDevices.length > 0
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
PanelSectionHeader {
|
||||
visible: root.knownDevices.length > 0
|
||||
text: "PAIRED"
|
||||
@@ -578,7 +656,7 @@ Panel {
|
||||
// Discovered (unpaired) devices, only shown while scanning.
|
||||
PanelSeparator {
|
||||
visible: root.adapter && root.adapter.discovering && root.discoveredDevices.length > 0
|
||||
&& (root.connectedDevices.length > 0 || root.knownDevices.length > 0)
|
||||
&& root.knownDevices.length > 0
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user