diff --git a/modules/nexus/PageCompRegistry.qml b/modules/nexus/PageCompRegistry.qml index efc831ae..4ac3d2c3 100644 --- a/modules/nexus/PageCompRegistry.qml +++ b/modules/nexus/PageCompRegistry.qml @@ -36,6 +36,14 @@ QtObject { NetworkPage {} } } + }, + Component { + // Bluetooth + StackPage { + Component { + BluetoothPage {} + } + } } ] } diff --git a/modules/nexus/pages/BluetoothPage.qml b/modules/nexus/pages/BluetoothPage.qml new file mode 100644 index 00000000..7380132e --- /dev/null +++ b/modules/nexus/pages/BluetoothPage.qml @@ -0,0 +1,254 @@ +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) + } + } + } + } + } + } + } + + StyledRect { + Layout.fillWidth: true + bottomLeftRadius: Tokens.rounding.extraLarge + bottomRightRadius: Tokens.rounding.extraLarge + radius: Tokens.rounding.extraSmall + implicitHeight: pairLayout.implicitHeight + pairLayout.anchors.margins * 2 + color: Colours.tPalette.m3surfaceContainer + + 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 {} + } + } + } +}