nosignal-shell/modules/nexus/pages/BluetoothPage.qml
2026-06-07 02:48:11 +10:00

251 lines
9 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Caelestia.Config
import qs.components
import qs.components.controls
import qs.services
import qs.utils
import qs.modules.nexus.common
PageBase {
id: root
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter // qmllint disable unresolved-type
readonly property bool btEnabled: adapter?.enabled ?? false
title: qsTr("Connected devices")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
ToggleRow {
Layout.fillWidth: true
first: true
text: qsTr("Bluetooth")
font: Tokens.font.body.medium
horizontalPadding: Tokens.padding.largeIncreased
checked: root.btEnabled
onToggled: {
if (root.adapter)
root.adapter.enabled = checked;
}
}
ItemList {
id: savedList
showList: root.btEnabled
placeholderIcon: root.btEnabled ? "devices_other" : "bluetooth_disabled"
placeholderText: root.btEnabled ? qsTr("No saved devices") : qsTr("Bluetooth disabled")
model: ScriptModel {
values: Bluetooth.devices.values.filter(d => d.bonded).sort((a, b) => (b.connected - a.connected) || a.name.localeCompare(b.name)) // qmllint disable unresolved-type
}
delegate: StyledRect {
id: device
required property BluetoothDevice modelData
readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type
readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) // qmllint disable unresolved-type
property real textOpacity: loading ? 0.5 : 1
anchors.left: savedList.list.contentItem.left
anchors.right: savedList.list.contentItem.right
implicitHeight: deviceLayout.implicitHeight + deviceLayout.anchors.margins * 2
radius: Tokens.rounding.extraSmall
color: "transparent"
Behavior on textOpacity {
Anim {
type: Anim.DefaultEffects
}
}
StateLayer {
disabled: device.loading
onClicked: {
if (!device.modelData || device.loading)
return;
device.modelData.connected = !device.connected;
}
}
RowLayout {
id: deviceLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
StyledRect {
implicitWidth: implicitHeight
implicitHeight: deviceIcon.implicitHeight + Tokens.padding.small * 2
radius: Tokens.rounding.full
color: device.connected ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon {
id: deviceIcon
anchors.centerIn: parent
text: Icons.getBluetoothIcon(device.modelData?.icon ?? "")
color: device.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
font: Tokens.font.icon.medium
fill: device.connected ? 1 : 0
opacity: device.textOpacity
Behavior on fill {
Anim {}
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
opacity: device.textOpacity
StyledText {
Layout.fillWidth: true
text: device.modelData?.name ?? qsTr("Unknown")
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: device.connected ? qsTr("Connected") : qsTr("Saved")
color: Colours.tPalette.m3outline
font: Tokens.font.label.small
elide: Text.ElideRight
}
}
MaterialIcon {
visible: device.connected && (device.modelData?.batteryAvailable ?? false)
text: Icons.getBatteryIcon(device.modelData?.battery ?? 0)
color: (device.modelData?.battery ?? 1) < 0.2 ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
}
Item {
Layout.fillHeight: true
implicitWidth: height
AnimLoader {
anchors.centerIn: parent
sourceComp: device.loading ? loadingComp : btnComp
Component {
id: btnComp
IconButton {
icon: "settings"
type: IconButton.Text
padding: Tokens.padding.small
inactiveOnColour: device.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
label.fill: 0
onClicked: ; // TODO
}
}
Component {
id: loadingComp
LoadingIndicator {
implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
}
}
}
}
}
}
}
ConnectedRect {
Layout.fillWidth: true
implicitHeight: pairLayout.implicitHeight + pairLayout.anchors.margins * 2
last: true
StateLayer {
disabled: !root.btEnabled
onClicked: ; // TODO
}
RowLayout {
id: pairLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
opacity: root.btEnabled ? 1 : 0.5
Behavior on opacity {
Anim {}
}
MaterialIcon {
text: "add"
font: Tokens.font.icon.medium
}
StyledText {
Layout.fillWidth: true
text: qsTr("Pair new device")
font: Tokens.font.body.small
elide: Text.ElideRight
}
}
}
ToggleRow {
Layout.fillWidth: true
Layout.topMargin: Tokens.spacing.large - parent.spacing
first: true
text: qsTr("Discoverable")
subtext: qsTr("Allow nearby devices to find this one")
enabled: root.btEnabled
opacity: root.btEnabled ? 1 : 0.5
checked: root.adapter?.discoverable ?? false
onToggled: {
if (root.adapter)
root.adapter.discoverable = checked;
}
Behavior on opacity {
Anim {}
}
}
ToggleRow {
Layout.fillWidth: true
last: true
text: qsTr("Pairable")
subtext: qsTr("Allow nearby devices to pair with this one")
enabled: root.btEnabled
opacity: root.btEnabled ? 1 : 0.5
checked: root.adapter?.pairable ?? false
onToggled: {
if (root.adapter)
root.adapter.pairable = checked;
}
Behavior on opacity {
Anim {}
}
}
}
}