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 NsIcon { id: deviceIcon anchors.centerIn: parent icon: Icons.getBluetoothIcon(device.modelData?.icon ?? "") color: device.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer fontStyle: 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%1").arg(device.modelData?.batteryAvailable ? " • " + Math.round(device.modelData.battery * 100) + "%" : "") : qsTr("Saved") color: Colours.palette.m3outline font: Tokens.font.label.small elide: Text.ElideRight animate: true } } 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: { root.nState.selectedBtDevice = device.modelData; root.nState.openSubPage(1); // Per device info page } } } 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: root.nState.openSubPage(2) } 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 {} } NsIcon { icon: "add" fontStyle: 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 {} } } } }