bluetooth: use qs bluetooth

This commit is contained in:
Soramane 2025-07-16 23:49:45 +10:00
parent f263df3ccd
commit 8784a015c2
4 changed files with 161 additions and 128 deletions

View file

@ -3,6 +3,7 @@ import qs.services
import qs.utils
import qs.config
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Services.UPower
import QtQuick
@ -38,7 +39,7 @@ Item {
anchors.topMargin: Appearance.spacing.smaller / 2
animate: true
text: Bluetooth.powered ? "bluetooth" : "bluetooth_disabled"
text: Bluetooth.defaultAdapter.enabled ? "bluetooth" : "bluetooth_disabled"
color: root.colour
}
@ -55,16 +56,35 @@ Item {
id: repeater
model: ScriptModel {
values: Bluetooth.devices.filter(d => d.connected)
values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected)
}
MaterialIcon {
required property Bluetooth.Device modelData
id: device
required property BluetoothDevice modelData
animate: true
text: Icons.getBluetoothIcon(modelData.icon)
color: root.colour
fill: 1
SequentialAnimation on opacity {
running: device.modelData.state !== BluetoothDeviceState.Connected
alwaysRunToEnd: true
loops: Animation.Infinite
Anim {
from: 1
to: 0
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
Anim {
from: 0
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
}
}
@ -99,19 +119,13 @@ Item {
fill: 1
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
Behavior on implicitHeight {
Anim {}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}

View file

@ -1,18 +1,140 @@
import qs.widgets
import qs.services
import qs.config
import QtQuick
pragma ComponentBehavior: Bound
Column {
import qs.widgets
import qs.config
import qs.utils
import Quickshell
import Quickshell.Bluetooth
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
ColumnLayout {
id: root
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Bluetooth %1").arg(Bluetooth.powered ? "enabled" : "disabled")
text: qsTr("Bluetooth %1").arg(BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state).toLowerCase())
}
StyledText {
text: Bluetooth.devices.some(d => d.connected) ? qsTr("Connected to: %1").arg(Bluetooth.devices.filter(d => d.connected).map(d => d.alias).join(", ")) : qsTr("No devices connected")
text: qsTr("%n connected device(s)", "", Bluetooth.devices.values.filter(d => d.connected).length)
}
Repeater {
model: ScriptModel {
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired))
}
RowLayout {
id: device
required property var modelData
readonly property bool loading: device.modelData.state === BluetoothDeviceState.Connecting || device.modelData.state === BluetoothDeviceState.Disconnecting
Layout.fillWidth: true
spacing: Appearance.spacing.small
opacity: 0
scale: 0.7
Component.onCompleted: {
opacity = 1;
scale = 1;
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
MaterialIcon {
text: Icons.getBluetoothIcon(device.modelData.icon)
}
StyledText {
Layout.fillWidth: true
text: device.modelData.name
}
Item {
id: connectBtn
implicitWidth: loadingIndicator.implicitWidth - Appearance.padding.small * 2
implicitHeight: loadingIndicator.implicitHeight - Appearance.padding.small * 2
BusyIndicator {
id: loadingIndicator
anchors.centerIn: parent
implicitWidth: Appearance.font.size.large * 2 + Appearance.padding.small * 2
implicitHeight: Appearance.font.size.large * 2 + Appearance.padding.small * 2
background: null
running: opacity > 0
opacity: device.loading ? 1 : 0
Behavior on opacity {
Anim {}
}
}
StateLayer {
radius: Appearance.rounding.full
disabled: device.loading
function onClicked(): void {
device.modelData.connected = !device.modelData.connected;
}
}
MaterialIcon {
anchors.centerIn: parent
animate: true
text: device.modelData.connected ? "link_off" : "link"
font.pointSize: device.loading ? Appearance.font.size.normal : Appearance.font.size.larger
Behavior on font.pointSize {
Anim {
duration: Appearance.anim.durations.small
}
}
}
}
Loader {
asynchronous: true
active: device.modelData.paired
sourceComponent: Item {
implicitWidth: connectBtn.implicitWidth
implicitHeight: connectBtn.implicitHeight
StateLayer {
radius: Appearance.rounding.full
function onClicked(): void {
device.modelData.forget();
}
}
MaterialIcon {
anchors.centerIn: parent
text: "delete"
}
}
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View file

@ -4,6 +4,7 @@ import qs.config
import qs.utils
import Quickshell
import Quickshell.Widgets
import Quickshell.Bluetooth
import Quickshell.Services.UPower
import QtQuick
import QtQuick.Layouts
@ -100,7 +101,7 @@ WrapperItem {
Layout.alignment: Qt.AlignVCenter
animate: true
text: Bluetooth.powered ? "bluetooth" : "bluetooth_disabled"
text: Bluetooth.defaultAdapter.enabled ? "bluetooth" : "bluetooth_disabled"
font.pointSize: Appearance.font.size.large
}
@ -111,7 +112,7 @@ WrapperItem {
sourceComponent: StyledText {
animate: true
text: qsTr("%n device(s) connected", "", Bluetooth.devices.filter(d => d.connected).length)
text: qsTr("%n device(s) connected", "", Bluetooth.devices.values.filter(d => d.connected).length)
font.pointSize: Appearance.font.size.normal
}
}

View file

@ -1,104 +0,0 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property bool powered
property bool discovering
readonly property list<Device> devices: []
Process {
running: true
command: ["bluetoothctl"]
stdout: SplitParser {
onRead: {
getInfo.running = true;
getDevices.running = true;
}
}
}
Process {
id: getInfo
running: true
command: ["bluetoothctl", "show"]
environment: ({
LANG: "C",
LC_ALL: "C"
})
stdout: StdioCollector {
onStreamFinished: {
root.powered = text.includes("Powered: yes");
root.discovering = text.includes("Discovering: yes");
}
}
}
Process {
id: getDevices
running: true
command: ["fish", "-c", `
for a in (bluetoothctl devices)
if string match -q 'Device *' $a
bluetoothctl info $addr (string split ' ' $a)[2]
echo
end
end`]
environment: ({
LANG: "C",
LC_ALL: "C"
})
stdout: StdioCollector {
onStreamFinished: {
const devices = text.trim().split("\n\n").map(d => ({
name: d.match(/Name: (.*)/)[1],
alias: d.match(/Alias: (.*)/)[1],
address: d.match(/Device ([0-9A-Z:]{17})/)[1],
icon: d.match(/Icon: (.*)/)[1],
connected: d.includes("Connected: yes"),
paired: d.includes("Paired: yes"),
trusted: d.includes("Trusted: yes")
}));
const rDevices = root.devices;
const destroyed = rDevices.filter(rd => !devices.find(d => d.address === rd.address));
for (const device of destroyed)
rDevices.splice(rDevices.indexOf(device), 1).forEach(d => d.destroy());
for (const device of devices) {
const match = rDevices.find(d => d.address === device.address);
if (match) {
match.lastIpcObject = device;
} else {
rDevices.push(deviceComp.createObject(root, {
lastIpcObject: device
}));
}
}
}
}
}
component Device: QtObject {
required property var lastIpcObject
readonly property string name: lastIpcObject.name
readonly property string alias: lastIpcObject.alias
readonly property string address: lastIpcObject.address
readonly property string icon: lastIpcObject.icon
readonly property bool connected: lastIpcObject.connected
readonly property bool paired: lastIpcObject.paired
readonly property bool trusted: lastIpcObject.trusted
}
Component {
id: deviceComp
Device {}
}
}