diff --git a/modules/nexus/PageCompRegistry.qml b/modules/nexus/PageCompRegistry.qml index c7d34f69..bce3527a 100644 --- a/modules/nexus/PageCompRegistry.qml +++ b/modules/nexus/PageCompRegistry.qml @@ -47,6 +47,9 @@ QtObject { Component { BtDeviceInfo {} } + Component { + BluetoothPairing {} + } } } ] diff --git a/modules/nexus/common/ItemList.qml b/modules/nexus/common/ItemList.qml index e5a7e0a2..83d9acaa 100644 --- a/modules/nexus/common/ItemList.qml +++ b/modules/nexus/common/ItemList.qml @@ -6,7 +6,7 @@ import Caelestia.Config import qs.components import qs.services -StyledRect { +ConnectedRect { id: root property bool showList @@ -20,7 +20,6 @@ StyledRect { Layout.fillWidth: true implicitHeight: (showList && list.count > 0 ? list.contentHeight : placeholder.implicitHeight + Tokens.padding.extraLarge * 2) + extraHeight - radius: Tokens.rounding.extraSmall color: Colours.tPalette.m3surfaceContainer clip: true diff --git a/modules/nexus/pages/BluetoothPage.qml b/modules/nexus/pages/BluetoothPage.qml index af8be06b..eaa1e6e1 100644 --- a/modules/nexus/pages/BluetoothPage.qml +++ b/modules/nexus/pages/BluetoothPage.qml @@ -176,7 +176,7 @@ PageBase { StateLayer { disabled: !root.btEnabled - onClicked: ; // TODO + onClicked: root.nState.openSubPage(2) } RowLayout { diff --git a/modules/nexus/pages/bluetooth/BluetoothPairing.qml b/modules/nexus/pages/bluetooth/BluetoothPairing.qml new file mode 100644 index 00000000..32b234b1 --- /dev/null +++ b/modules/nexus/pages/bluetooth/BluetoothPairing.qml @@ -0,0 +1,193 @@ +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 + + function setScan(on: bool): void { + if (adapter?.enabled) + adapter.discovering = on; + } + + title: qsTr("Pair new device") + isSubPage: true + + Component.onCompleted: setScan(true) + Component.onDestruction: setScan(false) + onVisibleChanged: setScan(visible) + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + Connections { + function onEnabledChanged(): void { + if (root.adapter && !root.adapter.enabled) + root.nState.closeSubPage(); + } + + target: root.adapter + } + + ConnectedRect { + Layout.fillWidth: true + implicitHeight: headerText.implicitHeight + Tokens.padding.medium * 2 + first: true + + StyledText { + id: headerText + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Tokens.padding.large + anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.2) + + text: qsTr("Available devices") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.small + } + } + + ItemList { + id: deviceList + + Layout.fillWidth: true + showList: true + extraHeight: scanIndicator.implicitHeight + last: true + placeholderIcon: "bluetooth_searching" + placeholderText: qsTr("Searching for devices…") + list.anchors.top: scanIndicator.bottom + + model: ScriptModel { + values: Bluetooth.devices.values.filter(d => !d.bonded).sort((a, b) => (b.pairing - a.pairing) || a.name.localeCompare(b.name)) // qmllint disable unresolved-type + } + + delegate: Item { + id: newDevice + + required property BluetoothDevice modelData + required property int index + property real textOpacity: modelData?.pairing ? 0.5 : 1 + property bool wasPairing + + anchors.left: deviceList.list.contentItem.left + anchors.right: deviceList.list.contentItem.right + implicitHeight: newLayout.implicitHeight + newLayout.anchors.margins * 2 + + Behavior on textOpacity { + Anim { + type: Anim.DefaultEffects + } + } + + Connections { + function onPairedChanged(): void { + if (newDevice.wasPairing && newDevice.modelData?.paired) + root.nState.closeSubPage(); + } + + target: newDevice.modelData + } + + StateLayer { + radius: Tokens.rounding.extraSmall + bottomLeftRadius: newDevice.index === deviceList.list.count - 1 ? Tokens.rounding.extraLarge : radius + bottomRightRadius: newDevice.index === deviceList.list.count - 1 ? Tokens.rounding.extraLarge : radius + disabled: newDevice.modelData?.pairing ?? false + + onClicked: { + newDevice.modelData?.pair(); + newDevice.wasPairing = true; + } + } + + RowLayout { + id: newLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + MaterialIcon { + text: Icons.getBluetoothIcon(newDevice.modelData?.icon ?? "") + color: Colours.tPalette.m3onSurfaceVariant + font: Tokens.font.icon.medium + opacity: newDevice.textOpacity + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + opacity: newDevice.textOpacity + + StyledText { + Layout.fillWidth: true + text: newDevice.modelData?.name || qsTr("Unknown device") + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: newDevice.modelData?.pairing ? qsTr("Pairing...") : (newDevice.modelData?.address ?? "") + color: Colours.tPalette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + animate: true + } + } + + Loader { + asynchronous: true + active: opacity > 0 + opacity: newDevice.modelData?.pairing ? 1 : 0 + + sourceComponent: LoadingIndicator { + implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3) + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + } + } + + StyledProgressBar { + id: scanIndicator + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 1 + implicitHeight: Tokens.rounding.extraSmall + indeterminate: true + + Behavior on implicitHeight { + Anim { + type: Anim.DefaultEffects + } + } + } + } + } +}