summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/bar/components/StatusIcons.qml44
-rw-r--r--modules/bar/popouts/Bluetooth.qml130
-rw-r--r--modules/lock/Status.qml5
-rw-r--r--services/Bluetooth.qml104
4 files changed, 158 insertions, 125 deletions
diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml
index 91f11be..b2cca54 100644
--- a/modules/bar/components/StatusIcons.qml
+++ b/modules/bar/components/StatusIcons.qml
@@ -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
}
}
diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml
index f9ab3b3..245834e 100644
--- a/modules/bar/popouts/Bluetooth.qml
+++ b/modules/bar/popouts/Bluetooth.qml
@@ -1,18 +1,140 @@
+pragma ComponentBehavior: Bound
+
import qs.widgets
-import qs.services
import qs.config
+import qs.utils
+import Quickshell
+import Quickshell.Bluetooth
import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
-Column {
+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
}
}
diff --git a/modules/lock/Status.qml b/modules/lock/Status.qml
index 0e43aeb..e16c42d 100644
--- a/modules/lock/Status.qml
+++ b/modules/lock/Status.qml
@@ -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
}
}
diff --git a/services/Bluetooth.qml b/services/Bluetooth.qml
deleted file mode 100644
index 0769095..0000000
--- a/services/Bluetooth.qml
+++ /dev/null
@@ -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 {}
- }
-}