diff options
| -rw-r--r-- | modules/bar/components/StatusIcons.qml | 44 | ||||
| -rw-r--r-- | modules/bar/popouts/Bluetooth.qml | 130 | ||||
| -rw-r--r-- | modules/lock/Status.qml | 5 | ||||
| -rw-r--r-- | services/Bluetooth.qml | 104 |
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 {} - } -} |