pragma ComponentBehavior: Bound import ".." import qs.widgets import qs.services import qs.config import qs.utils import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts import QtQuick.Effects Item { id: root required property Session session readonly property BluetoothDevice device: session.bt.active StyledFlickable { anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: layout.height ColumnLayout { id: layout anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: Icons.getBluetoothIcon(root.device.icon) font.pointSize: Appearance.font.size.extraLarge * 3 font.bold: true } StyledText { Layout.alignment: Qt.AlignHCenter text: root.device.name font.pointSize: Appearance.font.size.large font.bold: true } StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Connection status") font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { text: qsTr("Connection settings for this device") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: adapterStatus.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.palette.m3surfaceContainer ColumnLayout { id: adapterStatus anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.larger Toggle { label: qsTr("Connected") checked: root.device.connected toggle.onToggled: root.device.connected = checked } Toggle { label: qsTr("Paired") checked: root.device.paired toggle.onToggled: { if (root.device.paired) root.device.forget(); else root.device.pair(); } } Toggle { label: qsTr("Blocked") checked: root.device.blocked toggle.onToggled: root.device.blocked = checked } } } } } ColumnLayout { anchors.right: fabRoot.right anchors.bottom: fabRoot.top anchors.bottomMargin: Appearance.padding.normal Repeater { id: fabMenu model: [ { icon: "handshake", label: root.device.trusted ? qsTr("Untrust") : qsTr("Trust"), onClicked: () => root.device.trusted = !root.device.trusted }, { icon: "block", label: root.device.blocked ? qsTr("Unblock") : qsTr("Block"), onClicked: () => root.device.blocked = !root.device.blocked }, { icon: "missing_controller", label: root.device.paired ? qsTr("Unpair") : qsTr("Pair"), onClicked: () => { if (root.device.paired) root.device.forget(); else root.device.pair(); } }, { icon: "bluetooth_connected", label: root.device.connected ? qsTr("Disconnect") : qsTr("Connect"), onClicked: () => root.device.connected = !root.device.connected } ] StyledClippingRect { id: fabMenuItem required property var modelData required property int index Layout.alignment: Qt.AlignRight implicitHeight: fabMenuItemInner.implicitHeight + Appearance.padding.larger * 2 radius: Appearance.rounding.full color: Colours.palette.m3primaryContainer opacity: 0 states: State { name: "visible" when: root.session.bt.fabMenuOpen PropertyChanges { fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Appearance.padding.large * 2 fabMenuItem.opacity: 1 fabMenuItemInner.opacity: 1 } } transitions: [ Transition { to: "visible" SequentialAnimation { PauseAnimation { duration: (fabMenu.count - 1 - fabMenuItem.index) * Appearance.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } Anim { property: "opacity" duration: Appearance.anim.durations.small } } } }, Transition { from: "visible" SequentialAnimation { PauseAnimation { duration: fabMenuItem.index * Appearance.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } Anim { property: "opacity" duration: Appearance.anim.durations.small } } } } ] StateLayer { function onClicked(): void { root.session.bt.fabMenuOpen = false; fabMenuItem.modelData.onClicked(); } } RowLayout { id: fabMenuItemInner anchors.centerIn: parent spacing: Appearance.spacing.normal opacity: 0 MaterialIcon { text: fabMenuItem.modelData.icon color: Colours.palette.m3onPrimaryContainer fill: 1 } StyledText { text: fabMenuItem.modelData.label color: Colours.palette.m3onPrimaryContainer } } } } } Item { id: fabRoot anchors.right: parent.right anchors.bottom: parent.bottom implicitWidth: 64 implicitHeight: 64 StyledRect { id: fabBg anchors.right: parent.right anchors.top: parent.top implicitWidth: 64 implicitHeight: 64 radius: Appearance.rounding.normal color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer states: State { name: "expanded" when: root.session.bt.fabMenuOpen PropertyChanges { fabBg.implicitWidth: 48 fabBg.implicitHeight: 48 fabBg.radius: 48 / 2 fab.font.pointSize: Appearance.font.size.larger } } transitions: Transition { Anim { properties: "implicitWidth,implicitHeight" duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } Anim { properties: "radius,font.pointSize" } } Elevation { anchors.fill: parent radius: parent.radius z: -1 level: fabState.containsMouse && !fabState.pressed ? 4 : 3 } StateLayer { id: fabState color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer function onClicked(): void { root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; } } MaterialIcon { id: fab anchors.centerIn: parent animate: true text: root.session.bt.fabMenuOpen ? "close" : "settings" color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer font.pointSize: Appearance.font.size.large fill: 1 } Behavior on radius { Anim {} } } } component Toggle: RowLayout { required property string label property alias checked: toggle.checked property alias toggle: toggle Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: parent.label } StyledSwitch { id: toggle } } component Anim: NumberAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } }