From fb650907a0b18fab4f996c2fdc110d2d091e4060 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:19:58 +1000 Subject: internal: rename dcontent -> controlcenter --- modules/controlcenter/ControlCenter.qml | 61 +++ modules/controlcenter/NavRail.qml | 203 ++++++++ modules/controlcenter/Panes.qml | 98 ++++ modules/controlcenter/Session.qml | 22 + modules/controlcenter/bluetooth/BtPane.qml | 121 +++++ modules/controlcenter/bluetooth/Details.qml | 663 +++++++++++++++++++++++++ modules/controlcenter/bluetooth/DeviceList.qml | 363 ++++++++++++++ modules/controlcenter/bluetooth/Settings.qml | 546 ++++++++++++++++++++ 8 files changed, 2077 insertions(+) create mode 100644 modules/controlcenter/ControlCenter.qml create mode 100644 modules/controlcenter/NavRail.qml create mode 100644 modules/controlcenter/Panes.qml create mode 100644 modules/controlcenter/Session.qml create mode 100644 modules/controlcenter/bluetooth/BtPane.qml create mode 100644 modules/controlcenter/bluetooth/Details.qml create mode 100644 modules/controlcenter/bluetooth/DeviceList.qml create mode 100644 modules/controlcenter/bluetooth/Settings.qml (limited to 'modules/controlcenter') diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml new file mode 100644 index 0000000..2e7ce07 --- /dev/null +++ b/modules/controlcenter/ControlCenter.qml @@ -0,0 +1,61 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property ShellScreen screen + property alias active: session.active + readonly property Session session: Session { + id: session + } + + implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio + implicitHeight: screen.height * Config.controlCenter.sizes.heightMult + + RowLayout { + anchors.fill: parent + + spacing: 0 + + StyledRect { + Layout.fillHeight: true + + topLeftRadius: Appearance.rounding.normal + bottomLeftRadius: Appearance.rounding.normal + implicitWidth: navRail.implicitWidth + color: Colours.palette.m3surfaceContainer + + CustomMouseArea { + anchors.fill: parent + + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y < 0) + root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1); + else if (event.angleDelta.y > 0) + root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); + } + } + + NavRail { + id: navRail + + session: root.session + } + } + + Panes { + Layout.fillWidth: true + Layout.fillHeight: true + + session: root.session + } + } +} diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml new file mode 100644 index 0000000..a631465 --- /dev/null +++ b/modules/controlcenter/NavRail.qml @@ -0,0 +1,203 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + property bool expanded + + implicitWidth: layout.implicitWidth + Appearance.padding.large * 4 + implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + + ColumnLayout { + id: layout + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + states: State { + name: "expanded" + when: root.expanded + + PropertyChanges { + layout.spacing: Appearance.spacing.small + menuIcon.opacity: 0 + menuIconExpanded.opacity: 1 + menuIcon.rotation: 180 + menuIconExpanded.rotation: 0 + } + AnchorChanges { + target: menuIcon + anchors.horizontalCenter: undefined + } + AnchorChanges { + target: menuIconExpanded + anchors.horizontalCenter: undefined + } + } + + transitions: Transition { + Anim { + properties: "spacing,opacity,rotation" + } + } + + Item { + Layout.fillWidth: true + Layout.bottomMargin: Appearance.spacing.large * 2 + implicitHeight: Math.max(menuIcon.implicitHeight, menuIconExpanded.implicitHeight) + Appearance.padding.normal * 2 + + StateLayer { + radius: Appearance.rounding.small + + function onClicked(): void { + root.expanded = !root.expanded; + } + } + + MaterialIcon { + id: menuIcon + + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + text: "menu" + font.pointSize: Appearance.font.size.large + } + + MaterialIcon { + id: menuIconExpanded + + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + text: "menu_open" + font.pointSize: Appearance.font.size.large + opacity: 0 + rotation: -180 + } + } + + NavItem { + icon: "network_manage" + label: "network" + } + + NavItem { + icon: "settings_bluetooth" + label: "bluetooth" + } + + NavItem { + icon: "tune" + label: "audio" + } + } + + component NavItem: Item { + id: item + + required property string icon + required property string label + readonly property bool active: root.session.active === label + + implicitWidth: background.implicitWidth + implicitHeight: background.implicitHeight + smallLabel.implicitHeight + smallLabel.anchors.topMargin + + states: State { + name: "expanded" + when: root.expanded + + PropertyChanges { + expandedLabel.opacity: 1 + smallLabel.opacity: 0 + background.implicitWidth: Config.controlCenter.sizes.expandedNavWidth + background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + item.implicitHeight: background.implicitHeight + } + } + + transitions: Transition { + Anim { + property: "opacity" + duration: Appearance.anim.durations.small + } + + Anim { + properties: "implicitWidth,implicitHeight" + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + StyledRect { + id: background + + radius: Appearance.rounding.full + color: item.active ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainer + + implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + implicitHeight: icon.implicitHeight + Appearance.padding.small + + StateLayer { + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + + function onClicked(): void { + root.session.active = item.label; + } + } + + MaterialIcon { + id: icon + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.large + + text: item.icon + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.large + fill: item.active ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + + StyledText { + id: expandedLabel + + anchors.left: icon.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.spacing.normal + + opacity: 0 + text: item.label + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.capitalization: Font.Capitalize + } + + StyledText { + id: smallLabel + + anchors.horizontalCenter: icon.horizontalCenter + anchors.top: icon.bottom + anchors.topMargin: Appearance.spacing.small / 2 + + text: item.label + font.pointSize: Appearance.font.size.small + font.capitalization: Font.Capitalize + } + } + } + + component Anim: NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } +} diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml new file mode 100644 index 0000000..d1ec581 --- /dev/null +++ b/modules/controlcenter/Panes.qml @@ -0,0 +1,98 @@ +pragma ComponentBehavior: Bound + +import "bluetooth" +import qs.components +import qs.components.effects +import qs.services +import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +ClippingRectangle { + id: root + + required property Session session + + topRightRadius: Appearance.rounding.normal + bottomRightRadius: Appearance.rounding.normal + color: "transparent" + + ColumnLayout { + id: layout + + spacing: 0 + y: -root.session.activeIndex * root.height + + Pane { + index: 0 + sourceComponent: Item { + StyledText { + anchors.centerIn: parent + text: qsTr("Work in progress") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.extraLarge + font.weight: 500 + } + } + } + + Pane { + index: 1 + sourceComponent: BtPane { + session: root.session + } + } + + Pane { + index: 2 + sourceComponent: Item { + StyledText { + anchors.centerIn: parent + text: qsTr("Work in progress") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.extraLarge + font.weight: 500 + } + } + } + + Behavior on y { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + InnerBorder { + leftThickness: 0 + } + + component Pane: Item { + id: pane + + required property int index + property alias sourceComponent: loader.sourceComponent + + implicitWidth: root.width + implicitHeight: root.height + + Loader { + id: loader + + anchors.fill: parent + clip: true + asynchronous: true + active: { + if (root.session.activeIndex === pane.index) + return true; + + const ly = -layout.y; + const ty = pane.index * root.height; + return ly + root.height > ty && ly < ty + root.height; + } + } + } +} diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml new file mode 100644 index 0000000..0c3f1b4 --- /dev/null +++ b/modules/controlcenter/Session.qml @@ -0,0 +1,22 @@ +import Quickshell.Bluetooth +import QtQuick + +QtObject { + readonly property list panes: ["network", "bluetooth", "audio"] + + property string active + property int activeIndex + + readonly property Bt bt: Bt {} + + onActiveChanged: activeIndex = panes.indexOf(active) + onActiveIndexChanged: active = panes[activeIndex] + + component Bt: QtObject { + property BluetoothDevice active + property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter + property bool editingAdapterName + property bool fabMenuOpen + property bool editingDeviceName + } +} diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml new file mode 100644 index 0000000..c6dbbcc --- /dev/null +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -0,0 +1,121 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components.effects +import qs.components.containers +import qs.config +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property Session session + + anchors.fill: parent + + spacing: 0 + + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.fillHeight: true + + DeviceList { + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + + session: root.session + } + + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Loader { + id: loader + + property BluetoothDevice pane: root.session.bt.active + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large * 2 + anchors.rightMargin: Appearance.padding.large * 2 + Appearance.padding.normal / 2 + + asynchronous: true + sourceComponent: pane ? details : settings + + Behavior on pane { + SequentialAnimation { + ParallelAnimation { + Anim { + property: "opacity" + to: 0 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + Anim { + property: "scale" + to: 0.8 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + } + PropertyAction {} + ParallelAnimation { + Anim { + property: "opacity" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + property: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + } + } + + InnerBorder { + leftThickness: Appearance.padding.normal / 2 + } + + Component { + id: settings + + StyledFlickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + + Settings { + id: settingsInner + + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } + } + } + + Component { + id: details + + Details { + session: root.session + } + } + } + + component Anim: NumberAnimation { + target: loader + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.BezierSpline + } +} diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml new file mode 100644 index 0000000..f856002 --- /dev/null +++ b/modules/controlcenter/bluetooth/Details.qml @@ -0,0 +1,663 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import qs.utils +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts + +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 + animate: true + text: Icons.getBluetoothIcon(root.device.icon) + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + animate: true + 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: deviceStatus.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.palette.m3surfaceContainer + + ColumnLayout { + id: deviceStatus + + 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 ?? 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 + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Device properties") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Additional settings") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.palette.m3surfaceContainer + + ColumnLayout { + id: deviceProps + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.larger + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + Item { + id: renameDevice + + Layout.fillWidth: true + Layout.rightMargin: Appearance.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: Appearance.padding.normal + } + } + + transitions: Transition { + AnchorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + Anim { + properties: "implicitHeight,opacity,padding" + } + } + + StyledText { + id: renameLabel + + anchors.left: parent.left + + text: qsTr("Device name") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledTextField { + id: deviceNameEdit + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: renameLabel.bottom + anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal + + text: root.device?.name ?? "" + readOnly: !root.session.bt.editingDeviceName + onAccepted: { + root.session.bt.editingDeviceName = false; + root.device.name = text; + } + + leftPadding: Appearance.padding.normal + rightPadding: Appearance.padding.normal + + background: StyledRect { + radius: Appearance.rounding.small + border.width: 2 + border.color: Colours.palette.m3primary + opacity: root.session.bt.editingDeviceName ? 1 : 0 + + Behavior on border.color { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Behavior on opacity { + Anim {} + } + } + + Behavior on anchors.leftMargin { + Anim {} + } + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.small + color: Colours.palette.m3secondaryContainer + opacity: root.session.bt.editingDeviceName ? 1 : 0 + scale: root.session.bt.editingDeviceName ? 1 : 0.5 + + StateLayer { + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingDeviceName + + function onClicked(): void { + root.session.bt.editingDeviceName = false; + deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); + } + } + + MaterialIcon { + id: cancelEditIcon + + anchors.centerIn: parent + animate: true + text: "cancel" + color: Colours.palette.m3onSecondaryContainer + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 + color: root.session.bt.editingDeviceName ? Colours.palette.m3primary : "transparent" + + StateLayer { + color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + + function onClicked(): void { + root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; + if (root.session.bt.editingDeviceName) + deviceNameEdit.forceActiveFocus(); + else + deviceNameEdit.accepted(); + } + } + + 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 + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Device information") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Information about this device") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.palette.m3surfaceContainer + + ColumnLayout { + id: deviceInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") + } + + RowLayout { + Layout.topMargin: Appearance.spacing.small / 2 + Layout.fillWidth: true + Layout.preferredHeight: Appearance.padding.smaller + spacing: Appearance.spacing.small / 2 + + StyledRect { + Layout.fillHeight: true + implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0 + radius: Appearance.rounding.full + color: Colours.palette.m3primary + } + + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Appearance.rounding.full + color: Colours.palette.m3secondaryContainer + + StyledRect { + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: parent.height * 0.25 + + implicitWidth: height + radius: Appearance.rounding.full + color: Colours.palette.m3primary + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Dbus path") + } + + StyledText { + text: root.device?.dbusPath ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("MAC address") + } + + StyledText { + text: root.device?.address ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Bonded") + } + + StyledText { + text: root.device?.bonded ? qsTr("Yes") : qsTr("No") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("System name") + } + + StyledText { + text: root.device?.deviceName ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } + } + } + + ColumnLayout { + anchors.right: fabRoot.right + anchors.bottom: fabRoot.top + anchors.bottomMargin: Appearance.padding.normal + + 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 + 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; + + 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: Appearance.spacing.normal + 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.capitalization: Font.Capitalize + Layout.preferredWidth: implicitWidth + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.small + } + } + } + } + } + } + } + + 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 + } + } + } + + 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 + } +} diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml new file mode 100644 index 0000000..d975e17 --- /dev/null +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -0,0 +1,363 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import qs.utils +import Quickshell +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +ColumnLayout { + id: root + + required property Session session + + anchors.fill: parent + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + ToggleButton { + toggled: Bluetooth.defaultAdapter?.enabled ?? false + icon: "power" + accent: "Tertiary" + + function onClicked(): void { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.enabled = !adapter.enabled; + } + } + + ToggleButton { + toggled: Bluetooth.defaultAdapter?.discoverable ?? false + icon: QsWindow.window.screen.height <= 1080 ? "group_search" : "" + label: QsWindow.window.screen.height <= 1080 ? "" : qsTr("Discoverable") + + function onClicked(): void { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.discoverable = !adapter.discoverable; + } + } + + ToggleButton { + toggled: Bluetooth.defaultAdapter?.pairable ?? false + icon: "missing_controller" + label: QsWindow.window.screen.height <= 960 ? "" : qsTr("Pairable") + + function onClicked(): void { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.pairable = !adapter.pairable; + } + } + + ToggleButton { + toggled: !root.session.bt.active + icon: "settings" + accent: "Primary" + + function onClicked(): void { + if (root.session.bt.active) + root.session.bt.active = null; + else { + root.session.bt.active = deviceModel.values[0] ?? null; + } + } + } + } + + RowLayout { + Layout.topMargin: Appearance.spacing.large + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + Layout.fillWidth: true + text: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length) + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + Layout.fillWidth: true + text: qsTr("All available bluetooth devices") + color: Colours.palette.m3outline + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: scanIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: Bluetooth.defaultAdapter?.discovering ? Appearance.rounding.normal : implicitHeight / 2 + color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + StateLayer { + color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + + function onClicked(): void { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.discovering = !adapter.discovering; + } + } + + MaterialIcon { + id: scanIcon + + anchors.centerIn: parent + animate: true + text: "bluetooth_searching" + color: Bluetooth.defaultAdapter?.discovering ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + fill: Bluetooth.defaultAdapter?.discovering ? 1 : 0 + } + + Behavior on radius { + Anim {} + } + } + } + + StyledListView { + model: ScriptModel { + id: deviceModel + values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired)) + } + + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: Appearance.spacing.small / 2 + + ScrollBar.vertical: StyledScrollBar {} + + delegate: StyledRect { + id: device + + required property BluetoothDevice modelData + readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting + readonly property bool connected: modelData.state === BluetoothDeviceState.Connected + + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 + + color: root.session.bt.active === modelData ? Colours.palette.m3surfaceContainer : "transparent" + radius: Appearance.rounding.normal + + StateLayer { + id: stateLayer + + function onClicked(): void { + root.session.bt.active = device.modelData; + } + } + + RowLayout { + id: deviceInner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.normal + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: device.connected ? Colours.palette.m3primaryContainer : device.modelData.bonded ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainerHigh + + StyledRect { + anchors.fill: parent + radius: parent.radius + color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : device.modelData.bonded ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: Icons.getBluetoothIcon(device.modelData.icon) + color: device.connected ? Colours.palette.m3onPrimaryContainer : device.modelData.bonded ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.large + fill: device.connected ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + } + + ColumnLayout { + Layout.fillWidth: true + + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: device.modelData.name + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + text: device.modelData.address + (device.connected ? qsTr(" (Connected)") : device.modelData.bonded ? qsTr(" (Paired)") : "") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + elide: Text.ElideRight + } + } + + StyledRect { + id: connectBtn + + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.small * 2 + + radius: Appearance.rounding.full + color: device.connected ? Colours.palette.m3primaryContainer : "transparent" + + StyledBusyIndicator { + anchors.centerIn: parent + + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + + running: opacity > 0 + opacity: device.loading ? 1 : 0 + + Behavior on opacity { + Anim {} + } + } + + StateLayer { + color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + disabled: device.loading + + function onClicked(): void { + device.modelData.connected = !device.modelData.connected; + } + } + + MaterialIcon { + id: connectIcon + + anchors.centerIn: parent + animate: true + text: device.modelData.connected ? "link_off" : "link" + color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + + opacity: device.loading ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + } + } + } + } + + component ToggleButton: StyledRect { + id: toggleBtn + + required property bool toggled + property string icon + property string label + property string accent: "Secondary" + + function onClicked(): void { + } + + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.larger * 2 : toggled ? Appearance.padding.small * 2 : 0) + implicitWidth: toggleBtnInner.implicitWidth + Appearance.padding.large * 2 + implicitHeight: toggleBtnIcon.implicitHeight + Appearance.padding.normal * 2 + + radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 + color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + + StateLayer { + id: toggleStateLayer + + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + + function onClicked(): void { + toggleBtn.onClicked(); + } + } + + RowLayout { + id: toggleBtnInner + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + id: toggleBtnIcon + + visible: !!text + fill: toggleBtn.toggled ? 1 : 0 + text: toggleBtn.icon + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + font.pointSize: Appearance.font.size.large + + Behavior on fill { + Anim {} + } + } + + Loader { + asynchronous: true + active: !!toggleBtn.label + visible: active + + sourceComponent: StyledText { + text: toggleBtn.label + color: toggleBtn.toggled ? Colours.palette[`m3on${toggleBtn.accent}`] : Colours.palette[`m3on${toggleBtn.accent}Container`] + } + } + } + + Behavior on radius { + Anim {} + } + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + + component Anim: NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } +} diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml new file mode 100644 index 0000000..f298432 --- /dev/null +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -0,0 +1,546 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property Session session + + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "bluetooth" + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Bluetooth settings") + font.pointSize: Appearance.font.size.large + font.bold: true + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Adapter status") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("General adapter settings") + 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("Powered") + checked: Bluetooth.defaultAdapter?.enabled ?? false + toggle.onToggled: { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.enabled = checked; + } + } + + Toggle { + label: qsTr("Discoverable") + checked: Bluetooth.defaultAdapter?.discoverable ?? false + toggle.onToggled: { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.discoverable = checked; + } + } + + Toggle { + label: qsTr("Pairable") + checked: Bluetooth.defaultAdapter?.pairable ?? false + toggle.onToggled: { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.pairable = checked; + } + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Adapter properties") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Per-adapter settings") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: adapterSettings.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.palette.m3surfaceContainer + + ColumnLayout { + id: adapterSettings + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.larger + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Current adapter") + } + + Item { + id: adapterPickerButton + + property bool expanded + + implicitWidth: adapterPicker.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2 + + StateLayer { + radius: Appearance.rounding.small + + function onClicked(): void { + adapterPickerButton.expanded = !adapterPickerButton.expanded; + } + } + + RowLayout { + id: adapterPicker + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.topMargin: Appearance.padding.smaller + anchors.bottomMargin: Appearance.padding.smaller + spacing: Appearance.spacing.normal + + StyledText { + Layout.leftMargin: Appearance.padding.small + text: Bluetooth.defaultAdapter?.name ?? qsTr("None") + } + + MaterialIcon { + text: "expand_more" + } + } + + Elevation { + anchors.fill: adapterListBg + radius: adapterListBg.radius + opacity: adapterPickerButton.expanded ? 1 : 0 + scale: adapterPickerButton.expanded ? 1 : 0.7 + level: 2 + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + + StyledClippingRect { + id: adapterListBg + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight + + color: Colours.palette.m3secondaryContainer + radius: Appearance.rounding.small + opacity: adapterPickerButton.expanded ? 1 : 0 + scale: adapterPickerButton.expanded ? 1 : 0.7 + + ColumnLayout { + id: adapterList + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + spacing: 0 + + Repeater { + model: Bluetooth.adapters + + Item { + id: adapter + + required property BluetoothAdapter modelData + + Layout.fillWidth: true + implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2 + + StateLayer { + disabled: !adapterPickerButton.expanded + + function onClicked(): void { + adapterPickerButton.expanded = false; + root.session.bt.currentAdapter = adapter.modelData; + } + } + + RowLayout { + id: adapterInner + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + Layout.leftMargin: Appearance.padding.small + text: adapter.modelData.name + color: Colours.palette.m3onSecondaryContainer + } + + MaterialIcon { + text: "check" + color: Colours.palette.m3onSecondaryContainer + visible: adapter.modelData === root.session.bt.currentAdapter + } + } + } + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Discoverable timeout") + } + + CustomSpinBox { + min: 0 + value: root.session.bt.currentAdapter.discoverableTimeout + onValueModified: value => root.session.bt.currentAdapter.discoverableTimeout = value + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + Item { + id: renameAdapter + + Layout.fillWidth: true + Layout.rightMargin: Appearance.spacing.small + + implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight + + states: State { + name: "editingAdapterName" + when: root.session.bt.editingAdapterName + + AnchorChanges { + target: adapterNameEdit + anchors.top: renameAdapter.top + } + PropertyChanges { + renameAdapter.implicitHeight: adapterNameEdit.implicitHeight + renameLabel.opacity: 0 + adapterNameEdit.padding: Appearance.padding.normal + } + } + + transitions: Transition { + AnchorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + Anim { + properties: "implicitHeight,opacity,padding" + } + } + + StyledText { + id: renameLabel + + anchors.left: parent.left + + text: qsTr("Rename adapter (currently does not work)") // FIXME: remove disclaimer when fixed + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledTextField { + id: adapterNameEdit + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: renameLabel.bottom + anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal + + text: root.session.bt.currentAdapter.name + readOnly: !root.session.bt.editingAdapterName + onAccepted: { + root.session.bt.editingAdapterName = false; + // Doesn't work for now, will be added to QS later + // root.session.bt.currentAdapter.name = text; + } + + leftPadding: Appearance.padding.normal + rightPadding: Appearance.padding.normal + + background: StyledRect { + radius: Appearance.rounding.small + border.width: 2 + border.color: Colours.palette.m3primary + opacity: root.session.bt.editingAdapterName ? 1 : 0 + + Behavior on border.color { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Behavior on opacity { + Anim {} + } + } + + Behavior on anchors.leftMargin { + Anim {} + } + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: Appearance.rounding.small + color: Colours.palette.m3secondaryContainer + opacity: root.session.bt.editingAdapterName ? 1 : 0 + scale: root.session.bt.editingAdapterName ? 1 : 0.5 + + StateLayer { + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingAdapterName + + function onClicked(): void { + root.session.bt.editingAdapterName = false; + adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter.name); + } + } + + MaterialIcon { + id: cancelEditIcon + + anchors.centerIn: parent + animate: true + text: "cancel" + color: Colours.palette.m3onSecondaryContainer + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 + + radius: root.session.bt.editingAdapterName ? Appearance.rounding.small : implicitHeight / 2 + color: root.session.bt.editingAdapterName ? Colours.palette.m3primary : "transparent" + + StateLayer { + color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + + function onClicked(): void { + root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; + if (root.session.bt.editingAdapterName) + adapterNameEdit.forceActiveFocus(); + else + adapterNameEdit.accepted(); + } + } + + MaterialIcon { + id: editIcon + + anchors.centerIn: parent + animate: true + text: root.session.bt.editingAdapterName ? "check_circle" : "edit" + color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + } + + Behavior on radius { + Anim {} + } + } + } + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: qsTr("Adapter information") + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + text: qsTr("Information about the default adapter") + color: Colours.palette.m3outline + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: adapterInfo.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.palette.m3surfaceContainer + + ColumnLayout { + id: adapterInfo + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("Adapter state") + } + + StyledText { + text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Dbus path") + } + + StyledText { + text: Bluetooth.defaultAdapter?.dbusPath ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Adapter id") + } + + StyledText { + text: Bluetooth.defaultAdapter?.adapterId ?? "" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + } + + 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 + } +} -- cgit v1.2.3-freya