pragma ComponentBehavior: Bound import ".." import "../components" import QtQuick import QtQuick.Layouts import Quickshell.Bluetooth import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services import qs.utils StyledFlickable { id: root required property Session session readonly property BluetoothDevice device: session.bt.active flickableDirection: Flickable.VerticalFlick contentHeight: detailsWrapper.height StyledScrollBar.vertical: StyledScrollBar { flickable: root } Item { id: detailsWrapper anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top implicitHeight: details.implicitHeight DeviceDetails { id: details anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top session: root.session device: root.device headerComponent: Component { SettingsHeader { icon: Icons.getBluetoothIcon(root.device?.icon ?? "") title: root.device?.name ?? "" } } sections: [ Component { ColumnLayout { spacing: Tokens.spacing.medium StyledText { Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Connection status") font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { text: qsTr("Connection settings for this device") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: deviceStatus.implicitHeight + Tokens.padding.extraLargeIncreased radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: deviceStatus anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large spacing: Tokens.spacing.large Toggle { label: qsTr("Connected") checked: root.device?.connected ?? false toggle.onToggled: root.device.connected = checked } Toggle { label: qsTr("Paired") checked: root.device?.paired ?? false toggle.onToggled: { if (root.device.paired) root.device.forget(); else root.device.pair(); } } Toggle { label: qsTr("Blocked") checked: root.device?.blocked ?? false toggle.onToggled: root.device.blocked = checked } } } } }, Component { ColumnLayout { spacing: Tokens.spacing.medium StyledText { Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Device properties") font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { text: qsTr("Additional settings") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: deviceProps.implicitHeight + Tokens.padding.extraLargeIncreased radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: deviceProps anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large spacing: Tokens.spacing.large RowLayout { Layout.fillWidth: true spacing: Tokens.spacing.small Item { id: renameDevice Layout.fillWidth: true Layout.rightMargin: Tokens.spacing.small implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight states: State { name: "editingDeviceName" when: root.session.bt.editingDeviceName AnchorChanges { target: deviceNameEdit anchors.top: renameDevice.top } PropertyChanges { renameDevice.implicitHeight: deviceNameEdit.implicitHeight renameLabel.opacity: 0 deviceNameEdit.padding: root.Tokens.padding.medium } } transitions: Transition { AnchorAnim { type: AnchorAnim.Standard } Anim { properties: "implicitHeight,opacity,padding" } } StyledText { id: renameLabel anchors.left: parent.left text: qsTr("Device name") color: Colours.palette.m3outline font: Tokens.font.label.small } StyledTextField { id: deviceNameEdit anchors.left: parent.left anchors.right: parent.right anchors.top: renameLabel.bottom anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Tokens.padding.medium text: root.device?.name ?? "" readOnly: !root.session.bt.editingDeviceName onAccepted: { root.session.bt.editingDeviceName = false; root.device.name = text; } leftPadding: Tokens.padding.medium rightPadding: Tokens.padding.medium background: StyledRect { radius: Tokens.rounding.medium border.width: 2 border.color: Colours.palette.m3primary opacity: root.session.bt.editingDeviceName ? 1 : 0 Behavior on border.color { CAnim {} } Behavior on opacity { Anim {} } } Behavior on anchors.leftMargin { Anim {} } } } StyledRect { implicitWidth: implicitHeight implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.large radius: Tokens.rounding.medium color: Colours.palette.m3secondaryContainer opacity: root.session.bt.editingDeviceName ? 1 : 0 scale: root.session.bt.editingDeviceName ? 1 : 0.5 StateLayer { onClicked: { root.session.bt.editingDeviceName = false; deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); } color: Colours.palette.m3onSecondaryContainer disabled: !root.session.bt.editingDeviceName } MaterialIcon { id: cancelEditIcon anchors.centerIn: parent animate: true text: "cancel" color: Colours.palette.m3onSecondaryContainer } Behavior on opacity { Anim {} } Behavior on scale { Anim { type: Anim.FastSpatial } } } StyledRect { implicitWidth: implicitHeight implicitHeight: editIcon.implicitHeight + Tokens.padding.large radius: root.session.bt.editingDeviceName ? Tokens.rounding.medium : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) StateLayer { onClicked: { root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; if (root.session.bt.editingDeviceName) deviceNameEdit.forceActiveFocus(); else deviceNameEdit.accepted(); } color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { id: editIcon anchors.centerIn: parent animate: true text: root.session.bt.editingDeviceName ? "check_circle" : "edit" color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } Behavior on radius { Anim {} } } } Toggle { label: qsTr("Trusted") checked: root.device?.trusted ?? false toggle.onToggled: root.device.trusted = checked } Toggle { label: qsTr("Wake allowed") checked: root.device?.wakeAllowed ?? false toggle.onToggled: root.device.wakeAllowed = checked } } } } }, Component { ColumnLayout { spacing: Tokens.spacing.medium StyledText { Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Device information") font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { text: qsTr("Information about this device") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: deviceInfo.implicitHeight + Tokens.padding.extraLargeIncreased radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: deviceInfo anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large spacing: Tokens.spacing.extraSmall StyledText { text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") } RowLayout { id: batteryPercent Layout.topMargin: Tokens.spacing.extraSmall Layout.fillWidth: true Layout.preferredHeight: Tokens.padding.small spacing: Tokens.spacing.extraSmall StyledRect { Layout.fillWidth: true Layout.fillHeight: true radius: Tokens.rounding.full color: Colours.palette.m3secondaryContainer StyledRect { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: parent.height * 0.25 implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0 radius: Tokens.rounding.full color: Colours.palette.m3primary } } } StyledText { Layout.topMargin: Tokens.spacing.medium text: qsTr("Dbus path") } StyledText { text: root.device?.dbusPath ?? "" color: Colours.palette.m3outline font: Tokens.font.body.small } StyledText { Layout.topMargin: Tokens.spacing.medium text: qsTr("MAC address") } StyledText { text: root.device?.address ?? "" color: Colours.palette.m3outline font: Tokens.font.body.small } StyledText { Layout.topMargin: Tokens.spacing.medium text: qsTr("Bonded") } StyledText { text: root.device?.bonded ? qsTr("Yes") : qsTr("No") color: Colours.palette.m3outline font: Tokens.font.body.small } StyledText { Layout.topMargin: Tokens.spacing.medium text: qsTr("System name") } StyledText { text: root.device?.deviceName ?? "" color: Colours.palette.m3outline font: Tokens.font.body.small } } } } } ] } } ColumnLayout { anchors.right: fabRoot.right anchors.bottom: fabRoot.top anchors.bottomMargin: Tokens.padding.medium Repeater { id: fabMenu model: ListModel { ListElement { name: "trust" icon: "handshake" } ListElement { name: "block" icon: "block" } ListElement { name: "pair" icon: "missing_controller" } ListElement { name: "connect" icon: "bluetooth_connected" } } StyledClippingRect { id: fabMenuItem required property var modelData required property int index Layout.alignment: Qt.AlignRight implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.medium * 2 radius: Tokens.rounding.full color: Colours.palette.m3primaryContainer opacity: 0 states: State { name: "visible" when: root.session.bt.fabMenuOpen PropertyChanges { fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + root.Tokens.padding.extraLargeIncreased fabMenuItem.opacity: 1 fabMenuItemInner.opacity: 1 } } transitions: [ Transition { to: "visible" SequentialAnimation { PauseAnimation { duration: (fabMenu.count - 1 - fabMenuItem.index) * Tokens.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" type: Anim.FastSpatial } Anim { property: "opacity" type: Anim.StandardSmall } } } }, Transition { from: "visible" SequentialAnimation { PauseAnimation { duration: fabMenuItem.index * Tokens.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" type: Anim.FastSpatial } Anim { property: "opacity" type: Anim.StandardSmall } } } } ] StateLayer { onClicked: { root.session.bt.fabMenuOpen = false; const name = fabMenuItem.modelData.name; if (fabMenuItem.modelData.name !== "pair") root.device[`${name}ed`] = !root.device[`${name}ed`]; else if (root.device.paired) root.device.forget(); else root.device.pair(); } } RowLayout { id: fabMenuItemInner anchors.centerIn: parent spacing: Tokens.spacing.medium opacity: 0 MaterialIcon { text: fabMenuItem.modelData.icon color: Colours.palette.m3onPrimaryContainer fill: 1 } StyledText { animate: true text: (root.device && root.device[`${fabMenuItem.modelData.name}ed`] ? fabMenuItem.modelData.name === "connect" ? "dis" : "un" : "") + fabMenuItem.modelData.name color: Colours.palette.m3onPrimaryContainer font: Tokens.font.body.builders.small.capitalisation(Font.Capitalize).build() Layout.preferredWidth: implicitWidth Behavior on Layout.preferredWidth { Anim { type: Anim.StandardSmall } } } } } } } Item { id: fabRoot x: root.contentX + root.width - width y: root.contentY + root.height - height width: 64 height: 64 z: 10000 StyledRect { id: fabBg anchors.right: parent.right anchors.top: parent.top implicitWidth: 64 implicitHeight: 64 radius: Tokens.rounding.large 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.fontStyle: Tokens.font.icon.large } } transitions: Transition { Anim { properties: "implicitWidth,implicitHeight" type: Anim.FastSpatial } Anim { properties: "radius,font.pointSize" } } Elevation { anchors.fill: parent radius: parent.radius z: -1 level: fabState.containsMouse && !fabState.pressed ? 4 : 3 } StateLayer { id: fabState onClicked: { root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; } color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer } 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 fontStyle: Tokens.font.icon.large fill: 1 } } } component Toggle: RowLayout { required property string label property alias checked: toggle.checked property alias toggle: toggle Layout.fillWidth: true spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true text: parent.label } StyledSwitch { id: toggle cLayer: 2 } } }