pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth import qs.Components import qs.Helpers import qs.Config CustomClippingRect { id: root implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 implicitWidth: 500 ColumnLayout { id: layout anchors.fill: parent anchors.margins: Appearance.padding.larger spacing: Appearance.spacing.extraSmall / 2 CustomText { Layout.rightMargin: Appearance.padding.extraSmall Layout.topMargin: Appearance.padding.normal font.pointSize: Appearance.font.size.medium text: qsTr("Bluetooth") } Toggle { checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type first: true subtext: qsTr("Toggle bluetooth device state") text: checked ? qsTr("Enabled") : qsTr("Disabled") onToggled: { const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type if (adapter) adapter.enabled = checked; } } Toggle { checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type subtext: qsTr("Enable scanning for new devices") text: qsTr("Discover") onToggled: { const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type if (adapter) adapter.discovering = checked; } } Repeater { id: connected model: ScriptModel { values: [...Bluetooth.devices.values].filter(d => d.connected).sort((a, b) => a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type } BluetoothItem { first: false repeater: connected } } CustomText { Layout.rightMargin: Appearance.padding.extraSmall Layout.topMargin: Appearance.spacing.small color: DynamicColors.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small text: { const devices = Bluetooth.devices.values; // qmllint disable unresolved-type let available = qsTr("%1 device%2 available").arg(devices.filter(d => !d.connected).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; } } Repeater { id: disconnected model: ScriptModel { values: [...Bluetooth.devices.values].filter(d => !d.connected).sort((a, b) => a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type } BluetoothItem { repeater: disconnected } } } component BluetoothItem: ConnectedRect { id: btItem readonly property bool connecting: Network.connectingNetwork === modelData property int horizontalPadding: Appearance.padding.largeIncreased required property int index readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type required property BluetoothDevice modelData required property Repeater repeater Layout.fillWidth: true Layout.preferredHeight: visible ? rowLayout.implicitHeight + Appearance.padding.smaller * 2 : 0 first: index === 0 last: index === (repeater.count - 1) radius: Appearance.rounding.small Connections { function onPairedChanged(): void { if (btItem.modelData.paired) btItem.modelData.connect(); } target: btItem.modelData } RowLayout { id: rowLayout anchors.fill: parent anchors.leftMargin: btItem.horizontalPadding anchors.rightMargin: btItem.horizontalPadding Column { id: column Layout.fillWidth: true CustomText { Layout.fillWidth: true Layout.leftMargin: Appearance.spacing.extraSmall Layout.rightMargin: Appearance.spacing.extraSmall elide: Text.ElideRight text: btItem.modelData.name } CustomText { id: subtext anchors.left: parent.left anchors.right: parent.right color: DynamicColors.palette.m3outline elide: Text.ElideRight font.pointSize: Appearance.font.size.small text: qsTr("%1 battery left").arg(Math.round(btItem.modelData.battery * 100) + "%") } } Item { Layout.alignment: Qt.AlignRight implicitHeight: icon.height implicitWidth: icon.width MaterialIcon { id: icon font.pointSize: Appearance.font.size.large opacity: btItem.connecting ? 0 : 1 text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_alert" visible: btItem.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type Behavior on opacity { Anim { } } } Loader { active: opacity > 0 anchors.fill: parent opacity: btItem.connecting ? 1 : 0 Behavior on opacity { Anim { } } sourceComponent: CircularIndicator { bgColor: "transparent" running: true } } } } StateLayer { onClicked: { if (btItem.modelData.paired && !btItem.modelData.connected) btItem.modelData.connect(); else if (btItem.modelData.pairing) btItem.modelData.cancelPair(); else if (!btItem.modelData.paired) btItem.modelData.pair(); else if (btItem.modelData.connected) btItem.modelData.disconnect(); } } IconButton { anchors.right: rowLayout.right anchors.rightMargin: rowLayout.anchors.rightMargin + icon.width anchors.verticalCenter: parent.verticalCenter font.pointSize: Appearance.font.size.large icon: "cancel" implicitHeight: parent.height - Appearance.padding.larger * 2 type: IconButton.Text visible: btItem.modelData.paired onClicked: btItem.modelData.forget() } } component ConnectedRect: CustomRect { id: bg property bool first property bool last bottomLeftRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall bottomRightRadius: last ? Appearance.rounding.large : Appearance.rounding.extraSmall color: DynamicColors.tPalette.m3surfaceContainer topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall } component Toggle: CustomSwitch { id: toggleItem readonly property alias bg: bg property alias first: bg.first property alias last: bg.last property string subtext Layout.fillWidth: true cLayer: 2 horizontalPadding: Appearance.padding.largeIncreased implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2 implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2 indicator.anchors.right: right indicator.anchors.rightMargin: toggleItem.horizontalPadding indicator.anchors.verticalCenter: verticalCenter verticalPadding: Appearance.padding.normal background: ConnectedRect { id: bg StateLayer { id: stateLayer manualPressOverride: toggleItem.pressed } } contentItem: Item { anchors.left: parent.left anchors.leftMargin: toggleItem.horizontalPadding anchors.right: toggleItem.indicator.left anchors.rightMargin: Appearance.spacing.normal implicitHeight: column.implicitHeight implicitWidth: column.implicitWidth Column { id: column anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 0 CustomText { id: label anchors.left: parent.left anchors.right: parent.right elide: Text.ElideRight font: { const f = Qt.font(label.font); f.pointSize = Appearance.font.size.smaller; return f; } text: toggleItem.text } CustomText { id: subtext anchors.left: parent.left anchors.right: parent.right color: DynamicColors.palette.m3outline elide: Text.ElideRight font: { const f = Qt.font(subtext.font); f.pointSize = Appearance.font.size.small; return f; } text: toggleItem.subtext visible: toggleItem.subtext } } } onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY) } }