popouts: better bluetooth popout

This commit is contained in:
2 * r + 2 * t 2025-07-23 17:01:48 +10:00
parent 4709d65249
commit b09b262f72
2 changed files with 212 additions and 11 deletions

View file

@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import qs.widgets import qs.widgets
import qs.services
import qs.config import qs.config
import qs.utils import qs.utils
import Quickshell import Quickshell
@ -11,30 +12,64 @@ import QtQuick.Layouts
ColumnLayout { ColumnLayout {
id: root id: root
spacing: Appearance.spacing.small / 2 spacing: Appearance.spacing.small
StyledText { StyledText {
Layout.bottomMargin: Appearance.spacing.small Layout.topMargin: Appearance.padding.normal
Layout.rightMargin: Appearance.padding.small
text: qsTr("Bluetooth %1").arg(BluetoothAdapterState.toString(Bluetooth.defaultAdapter?.state).toLowerCase()) text: qsTr("Bluetooth %1").arg(BluetoothAdapterState.toString(Bluetooth.defaultAdapter?.state).toLowerCase())
font.weight: 500
}
Toggle {
label: qsTr("Enabled")
checked: Bluetooth.defaultAdapter?.enabled ?? false
toggle.onToggled: {
const adapter = Bluetooth.defaultAdapter;
if (adapter)
adapter.enabled = checked;
}
}
Toggle {
label: qsTr("Discovering")
checked: Bluetooth.defaultAdapter?.discovering ?? false
toggle.onToggled: {
const adapter = Bluetooth.defaultAdapter;
if (adapter)
adapter.discovering = checked;
}
} }
StyledText { StyledText {
text: qsTr("%n connected device(s)", "", Bluetooth.devices.values.filter(d => d.connected).length) Layout.topMargin: Appearance.spacing.small
Layout.rightMargin: Appearance.padding.small
text: {
const devices = Bluetooth.devices.values;
let available = qsTr("%1 device%2 available").arg(devices.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;
}
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
} }
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired)) values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired)).slice(0, 5)
} }
RowLayout { RowLayout {
id: device id: device
required property var modelData required property BluetoothDevice modelData
readonly property bool loading: device.modelData.state === BluetoothDeviceState.Connecting || device.modelData.state === BluetoothDeviceState.Disconnecting readonly property bool loading: device.modelData.state === BluetoothDeviceState.Connecting || device.modelData.state === BluetoothDeviceState.Disconnecting
Layout.fillWidth: true Layout.fillWidth: true
spacing: Appearance.spacing.small / 2 Layout.rightMargin: Appearance.padding.small
spacing: Appearance.spacing.small
opacity: 0 opacity: 0
scale: 0.7 scale: 0.7
@ -53,20 +88,24 @@ ColumnLayout {
} }
MaterialIcon { MaterialIcon {
Layout.rightMargin: Appearance.spacing.small
text: Icons.getBluetoothIcon(device.modelData.icon) text: Icons.getBluetoothIcon(device.modelData.icon)
} }
StyledText { StyledText {
Layout.leftMargin: Appearance.spacing.small / 2
Layout.rightMargin: Appearance.spacing.small / 2
Layout.fillWidth: true Layout.fillWidth: true
text: device.modelData.name text: device.modelData.name
} }
Item { StyledRect {
id: connectBtn id: connectBtn
implicitWidth: implicitHeight implicitWidth: implicitHeight
implicitHeight: connectIcon.implicitHeight + Appearance.padding.small * 2 implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
radius: Appearance.rounding.full
color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3primary : Colours.palette.m3surface
StyledBusyIndicator { StyledBusyIndicator {
anchors.centerIn: parent anchors.centerIn: parent
@ -83,7 +122,7 @@ ColumnLayout {
} }
StateLayer { StateLayer {
radius: Appearance.rounding.full color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
disabled: device.loading disabled: device.loading
function onClicked(): void { function onClicked(): void {
@ -97,6 +136,7 @@ ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
animate: true animate: true
text: device.modelData.connected ? "link_off" : "link" text: device.modelData.connected ? "link_off" : "link"
color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
opacity: device.loading ? 0 : 1 opacity: device.loading ? 0 : 1
@ -108,7 +148,7 @@ ColumnLayout {
Loader { Loader {
asynchronous: true asynchronous: true
active: device.modelData.paired active: device.modelData.bonded
sourceComponent: Item { sourceComponent: Item {
implicitWidth: connectBtn.implicitWidth implicitWidth: connectBtn.implicitWidth
implicitHeight: connectBtn.implicitHeight implicitHeight: connectBtn.implicitHeight
@ -130,6 +170,25 @@ ColumnLayout {
} }
} }
component Toggle: RowLayout {
required property string label
property alias checked: toggle.checked
property alias toggle: toggle
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
spacing: Appearance.spacing.normal
StyledText {
Layout.fillWidth: true
text: parent.label
}
StyledSwitch {
id: toggle
}
}
component Anim: NumberAnimation { component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline

142
widgets/StyledSwitch.qml Normal file
View file

@ -0,0 +1,142 @@
import qs.services
import qs.config
import QtQuick
import QtQuick.Controls
import QtQuick.Shapes
Switch {
id: root
implicitWidth: implicitIndicatorWidth
implicitHeight: implicitIndicatorHeight
indicator: StyledRect {
radius: Appearance.rounding.full
color: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest
implicitWidth: implicitHeight * 1.7
implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2
StyledRect {
readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight
radius: Appearance.rounding.full
color: root.checked ? Colours.palette.m3onPrimary : Colours.palette.m3outline
x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2
implicitWidth: nonAnimWidth
implicitHeight: parent.implicitHeight - Appearance.padding.small
anchors.verticalCenter: parent.verticalCenter
Shape {
id: icon
property point start1: {
if (root.pressed)
return Qt.point(width * 0.2, height / 2);
if (root.checked)
return Qt.point(width * 0.15, height / 2);
return Qt.point(width * 0.15, height * 0.15);
}
property point end1: {
if (root.pressed) {
if (root.checked)
return Qt.point(width * 0.4, height / 2);
return Qt.point(width * 0.8, height / 2);
}
if (root.checked)
return Qt.point(width * 0.4, height * 0.7);
return Qt.point(width * 0.85, height * 0.85);
}
property point start2: {
if (root.pressed) {
if (root.checked)
return Qt.point(width * 0.4, height / 2);
return Qt.point(width * 0.2, height / 2);
}
if (root.checked)
return Qt.point(width * 0.4, height * 0.7);
return Qt.point(width * 0.15, height * 0.85);
}
property point end2: {
if (root.pressed)
return Qt.point(width * 0.8, height / 2);
if (root.checked)
return Qt.point(width * 0.85, height * 0.2);
return Qt.point(width * 0.85, height * 0.15);
}
anchors.centerIn: parent
width: height
height: parent.implicitHeight - Appearance.padding.small * 2
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: Appearance.font.size.larger * 0.15
strokeColor: root.checked ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3surfaceContainerHighest
fillColor: "transparent"
capStyle: ShapePath.RoundCap
startX: icon.start1.x
startY: icon.start1.y
PathLine {
x: icon.end1.x
y: icon.end1.y
}
PathMove {
x: icon.start2.x
y: icon.start2.y
}
PathLine {
x: icon.end2.x
y: icon.end2.y
}
Behavior on strokeColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
Behavior on start1 {
Anim {}
}
Behavior on end1 {
Anim {}
}
Behavior on start2 {
Anim {}
}
Behavior on end2 {
Anim {}
}
}
Behavior on x {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
component Anim: PropertyAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}