bluetooth model delegate animations
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 19s
JS/TS / lint (pull_request) Successful in 21s
Python / fmt (pull_request) Successful in 35s
Python / lint (pull_request) Successful in 36s
Python / test (pull_request) Successful in 1m3s
Python / typecheck (pull_request) Failing after 1m4s
C++ / build (pull_request) Successful in 1m52s
Rust / build (pull_request) Successful in 1m41s
Rust / fmt (pull_request) Successful in 1m6s
Python / buildcheck (pull_request) Successful in 2m28s
Rust / clippy (pull_request) Successful in 1m55s
C++ / clang-tidy (pull_request) Successful in 3m36s

This commit is contained in:
2026-07-11 14:00:08 +02:00
parent 15ccfd24c6
commit 954dc10fbb
2 changed files with 143 additions and 75 deletions
+143 -62
View File
@@ -2,8 +2,10 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQml.Models
import Quickshell import Quickshell
import Quickshell.Bluetooth import Quickshell.Bluetooth
import ZShell.Components
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Config import qs.Config
@@ -24,7 +26,7 @@ CustomClippingRect {
CustomText { CustomText {
Layout.rightMargin: Appearance.padding.extraSmall Layout.rightMargin: Appearance.padding.extraSmall
Layout.topMargin: Appearance.padding.normal Layout.topMargin: Appearance.padding.normal
font.pointSize: Appearance.font.size.medium font.pointSize: Appearance.font.size.large
text: qsTr("Bluetooth") text: qsTr("Bluetooth")
} }
@@ -43,6 +45,7 @@ CustomClippingRect {
Toggle { Toggle {
checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type
last: rows.sortedConnected.length === 0
subtext: qsTr("Enable scanning for new devices") subtext: qsTr("Enable scanning for new devices")
text: qsTr("Discover") text: qsTr("Discover")
@@ -53,43 +56,83 @@ CustomClippingRect {
} }
} }
Repeater { ListView {
id: connected id: rows
readonly property var sortedConnected: {
const byName = (a, b) => a.name.localeCompare(b.name);
return [...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5);
}
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
section.property: "connected"
spacing: Appearance.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
}
delegate: DelegateChooser {
role: "isDivider"
DelegateChoice {
roleValue: true
delegate: StatusDivider {
}
}
DelegateChoice {
delegate: BluetoothItem {
list: rows
width: rows.width
}
}
}
displaced: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
model: ScriptModel { model: ScriptModel {
values: [...Bluetooth.devices.values].filter(d => d.connected).sort((a, b) => a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type objectProp: "address"
values: {
const byName = (a, b) => a.name.localeCompare(b.name);
return [...[...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5),
{
isDivider: true
},
...[...Bluetooth.devices.values].filter(d => !d.connected).sort(byName).slice(0, 5)];
}
} }
move: Transition {
BluetoothItem { Anim {
first: false easing.type: Easing.OutCubic
repeater: connected property: "y"
}
} }
} remove: Transition {
Anim {
CustomText { property: "opacity"
Layout.rightMargin: Appearance.padding.extraSmall to: 0
Layout.topMargin: Appearance.spacing.small }
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: {
const devices = Bluetooth.devices.values; // qmllint disable unresolved-type
let available = qsTr("%1 device%2 available").arg(devices.filter(d => !d.connected).length).arg(devices.length === 1 ? "" : "s");
const connected = devices.filter(d => d.connected).length;
if (connected > 0)
available += qsTr(" (%1 connected)").arg(connected);
return available;
} }
} section.delegate: Rectangle {
required property string section
Repeater { color: "transparent"
id: disconnected height: childrenRect.height
width: ListView.view.width
model: ScriptModel { CustomText {
values: [...Bluetooth.devices.values].filter(d => !d.connected).sort((a, b) => a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type text: parent.section
} }
BluetoothItem {
repeater: disconnected
} }
} }
} }
@@ -97,21 +140,35 @@ CustomClippingRect {
component BluetoothItem: ConnectedRect { component BluetoothItem: ConnectedRect {
id: btItem id: btItem
readonly property int connectedCount: rows.sortedConnected.length
readonly property bool connecting: Network.connectingNetwork === modelData readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Appearance.padding.largeIncreased property int horizontalPadding: Appearance.padding.largeIncreased
readonly property bool inConnectedGroup: index < connectedCount
required property int index required property int index
required property ListView list
readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type
required property BluetoothDevice modelData required property BluetoothDevice modelData
required property Repeater repeater property int verticalPadding: Appearance.padding.smaller
Layout.fillWidth: true first: !inConnectedGroup && index === connectedCount + 1
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Appearance.padding.smaller * 2 : 0 implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
first: index === 0 last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1
last: index === (repeater.count - 1)
radius: Appearance.rounding.small radius: Appearance.rounding.small
Timer {
interval: 50
running: btItem.ListView.delayRemove
onTriggered: btItem.ListView.delayRemove = false
}
Connections { Connections {
function onConnectedChanged(): void {
if (!btItem.modelData.connected)
btItem.ListView.delayRemove = true;
}
function onPairedChanged(): void { function onPairedChanged(): void {
if (btItem.modelData.paired) if (btItem.modelData.paired)
btItem.modelData.connect(); btItem.modelData.connect();
@@ -148,7 +205,7 @@ CustomClippingRect {
color: DynamicColors.palette.m3outline color: DynamicColors.palette.m3outline
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: Appearance.font.size.small font.pointSize: Appearance.font.size.small
text: qsTr("%1 battery left").arg(Math.round(btItem.modelData.battery * 100) + "%") text: btItem.modelData.connected ? (btItem.modelData.batteryAvailable ? qsTr("%1 battery left").arg(Math.round(btItem.modelData.battery * 100) + "%") : qsTr("Battery status unknown")) : btItem.modelData.address
} }
} }
@@ -162,7 +219,7 @@ CustomClippingRect {
font.pointSize: Appearance.font.size.large font.pointSize: Appearance.font.size.large
opacity: btItem.connecting ? 0 : 1 opacity: btItem.connecting ? 0 : 1
text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_alert" text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_unknown"
visible: btItem.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type visible: btItem.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type
Behavior on opacity { Behavior on opacity {
@@ -186,33 +243,28 @@ CustomClippingRect {
} }
} }
} }
}
StateLayer { ButtonRow {
onClicked: { spacing: Appearance.spacing.small
if (btItem.modelData.paired && !btItem.modelData.connected)
btItem.modelData.connect(); IconTextButton {
else if (btItem.modelData.pairing) font.pointSize: Appearance.font.size.small
btItem.modelData.cancelPair(); icon: btItem.modelData.connected ? "link_off" : btItem.modelData.paired ? "link" : "add_link"
else if (!btItem.modelData.paired) text: btItem.modelData.connected ? qsTr("Disconnect") : btItem.modelData.paired ? qsTr("Connect") : qsTr("Pair")
btItem.modelData.pair();
else if (btItem.modelData.connected) onClicked: {
btItem.modelData.disconnect(); if (btItem.modelData.connected)
btItem.modelData.disconnect();
else if (btItem.modelData.paired)
btItem.modelData.connect();
else if (!btItem.modelData.paired)
btItem.modelData.pair();
else if (btItem.modelData.pairing)
btItem.modelData.cancelPair();
}
}
} }
} }
IconButton {
anchors.right: rowLayout.right
anchors.rightMargin: rowLayout.anchors.rightMargin + icon.width
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Appearance.font.size.large
icon: "cancel"
implicitHeight: parent.height - Appearance.padding.larger * 2
type: IconButton.Text
visible: btItem.modelData.paired
onClicked: btItem.modelData.forget()
}
} }
component ConnectedRect: CustomRect { component ConnectedRect: CustomRect {
id: bg id: bg
@@ -225,6 +277,35 @@ CustomClippingRect {
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall
Behavior on bottomLeftRadius {
Anim {
}
}
Behavior on bottomRightRadius {
Anim {
}
}
Behavior on topLeftRadius {
Anim {
}
}
Behavior on topRightRadius {
Anim {
}
}
}
component StatusDivider: CustomText {
bottomPadding: Appearance.spacing.extraSmall
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: {
const devices = Bluetooth.devices.values; // qmllint disable unresolved-type
const connectedCount = devices.filter(d => d.connected).length;
let available = qsTr("%1 device%2 available").arg(devices.length - connectedCount).arg(devices.length === 1 ? "" : "s");
return available;
}
topPadding: Appearance.spacing.small
} }
component Toggle: CustomSwitch { component Toggle: CustomSwitch {
id: toggleItem id: toggleItem
-13
View File
@@ -253,19 +253,6 @@ CustomClippingRect {
} }
} }
} }
IconButton {
anchors.right: rowLayout.right
anchors.rightMargin: rowLayout.anchors.rightMargin + icon.width
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Appearance.font.size.large
icon: "cancel"
implicitHeight: parent.height - Appearance.padding.larger * 2
type: IconButton.Text
visible: networkItem.known
onClicked: networkItem.modelData.forget()
}
} }
component Spacer: Item { component Spacer: Item {
id: spacer id: spacer