diff --git a/components/AnimLoader.qml b/components/AnimLoader.qml index 343dfa12..a3ef4ddc 100644 --- a/components/AnimLoader.qml +++ b/components/AnimLoader.qml @@ -5,6 +5,8 @@ Loader { property Component sourceComp property bool isComplete + property int outAnimType: Anim.FastEffects + property int inAnimType: Anim.DefaultEffects asynchronous: true Component.onCompleted: { @@ -25,7 +27,7 @@ Loader { target: root property: "opacity" to: 0 - type: Anim.FastEffects + type: root.outAnimType } ScriptAction { script: root.sourceComponent = root.sourceComp @@ -34,7 +36,7 @@ Loader { target: root property: "opacity" to: 1 - type: Anim.DefaultEffects + type: root.inAnimType } } } diff --git a/modules/nexus/NexusState.qml b/modules/nexus/NexusState.qml index ddcb3e55..818f35c0 100644 --- a/modules/nexus/NexusState.qml +++ b/modules/nexus/NexusState.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Bluetooth QtObject { property ShellScreen screen @@ -9,6 +10,7 @@ QtObject { property bool searchOpen property string selectedWallpaperCategory + property BluetoothDevice selectedBtDevice signal close signal subPageOpened(idx: int) diff --git a/modules/nexus/PageCompRegistry.qml b/modules/nexus/PageCompRegistry.qml index 4ac3d2c3..c7d34f69 100644 --- a/modules/nexus/PageCompRegistry.qml +++ b/modules/nexus/PageCompRegistry.qml @@ -3,6 +3,7 @@ pragma Singleton import QtQuick import qs.modules.nexus.common import qs.modules.nexus.pages +import qs.modules.nexus.pages.bluetooth import qs.modules.nexus.pages.wallandstyle QtObject { @@ -43,6 +44,9 @@ QtObject { Component { BluetoothPage {} } + Component { + BtDeviceInfo {} + } } } ] diff --git a/modules/nexus/pages/BluetoothPage.qml b/modules/nexus/pages/BluetoothPage.qml index 090f163b..60b179a3 100644 --- a/modules/nexus/pages/BluetoothPage.qml +++ b/modules/nexus/pages/BluetoothPage.qml @@ -155,7 +155,10 @@ PageBase { inactiveOnColour: device.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant label.fill: 0 - onClicked: ; // TODO + onClicked: { + root.nState.selectedBtDevice = device.modelData; + root.nState.openSubPage(1); // Per device info page + } } } diff --git a/modules/nexus/pages/bluetooth/BtDeviceInfo.qml b/modules/nexus/pages/bluetooth/BtDeviceInfo.qml new file mode 100644 index 00000000..604973b6 --- /dev/null +++ b/modules/nexus/pages/bluetooth/BtDeviceInfo.qml @@ -0,0 +1,260 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth +import Caelestia.Components +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus.common + +PageBase { + id: root + + readonly property BluetoothDevice device: nState.selectedBtDevice + readonly property bool connected: device?.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type + readonly property bool loading: device?.state === BluetoothDeviceState.Connecting || device?.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type + + readonly property string statusText: { + if (!device) + return ""; + let s = connected ? qsTr("Connected") : (device.bonded ? qsTr("Paired") : qsTr("Not paired")); + if (connected && device.batteryAvailable) + s += " • " + Math.round(device.battery * 100) + "%"; + return s; + } + + onDeviceChanged: { + // Auto close when device lost + if (!device) + nState.closeSubPage(); + } + + title: device?.name ?? qsTr("Device") + isSubPage: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // Big buttons + ButtonRow { + Layout.bottomMargin: Tokens.spacing.large - parent.spacing + Layout.alignment: Qt.AlignHCenter + Layout.minimumWidth: Math.round(root.cappedWidth * 0.7) + spacing: Tokens.spacing.small + + ButtonBase { + id: forgetBtn + + fillWidth: true + shapeMorph: true + isRound: true + + inactiveColour: Colours.palette.m3errorContainer + inactiveOnColour: Colours.palette.m3onErrorContainer + + implicitWidth: forgetBtnLayout.implicitWidth + Tokens.padding.extraLarge * 2 + implicitHeight: forgetBtnLayout.implicitHeight + Tokens.padding.medium * 2 + + onClicked: { + root.device?.forget(); + root.nState.closeSubPage(); + } + + ColumnLayout { + id: forgetBtnLayout + + anchors.centerIn: parent + spacing: 0 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "delete" + color: forgetBtn.onColour + font: Tokens.font.icon.medium + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Forget") + color: forgetBtn.onColour + } + } + } + + ButtonBase { + id: connectBtn + + fillWidth: true + shapeMorph: true + isRound: true + + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + stateLayer.disabled: root.loading + + implicitWidth: connectBtnContent.implicitWidth + Tokens.padding.extraLarge * 2 + implicitHeight: connectBtnContent.implicitHeight + Tokens.padding.medium * 2 + + onClicked: root.device.connected = !root.connected + + AnimLoader { + id: connectBtnContent + + anchors.centerIn: parent + sourceComp: root.loading ? loadingComp : textComp + outAnimType: Anim.SlowEffects + inAnimType: Anim.SlowEffects + } + + Component { + id: loadingComp + + LoadingIndicator { + implicitSize: connectBtn.height - Tokens.padding.large * 2 + } + } + + Component { + id: textComp + + ColumnLayout { + spacing: 0 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: root.connected ? "close" : "add" + color: connectBtn.inactiveOnColour + font: Tokens.font.icon.medium + animate: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.connected ? qsTr("Disconnect") : qsTr("Connect") + color: connectBtn.inactiveOnColour + animate: true + } + } + } + } + } + + // Connection group + ToggleRow { + Layout.fillWidth: true + verticalPadding: Tokens.padding.large + first: true + text: qsTr("Trusted") + subtext: qsTr("Allow this device to connect automatically") + checked: root.device?.trusted ?? false + onToggled: { + if (root.device) + root.device.trusted = checked; + } + } + + ToggleRow { + Layout.fillWidth: true + verticalPadding: Tokens.padding.large + text: qsTr("Blocked") + subtext: qsTr("Prevent this device from connecting") + checked: root.device?.blocked ?? false + onToggled: { + if (root.device) + root.device.blocked = checked; + } + } + + ToggleRow { + Layout.fillWidth: true + verticalPadding: Tokens.padding.large + last: true + text: qsTr("Wake allowed") + subtext: qsTr("Allow this device to wake the system") + checked: root.device?.wakeAllowed ?? false + onToggled: { + if (root.device) + root.device.wakeAllowed = checked; + } + } + + // Information + ConnectedRect { + Layout.topMargin: Tokens.spacing.large - parent.spacing + Layout.fillWidth: true + implicitHeight: batteryLayout.implicitHeight + Tokens.padding.large * 2 + first: true + + ColumnLayout { + id: batteryLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.medium + + StyledText { + Layout.fillWidth: true + text: qsTr("Battery") + } + + StyledText { + text: root.device?.batteryAvailable ? Math.round(root.device.battery * 100) + "%" : qsTr("Unavailable") + color: Colours.palette.m3outline + font: Tokens.font.body.small + } + } + + Loader { + Layout.fillWidth: true + active: root.device?.batteryAvailable ?? false + visible: active + asynchronous: true + + sourceComponent: StyledProgressBar { + implicitHeight: Tokens.padding.medium + value: root.device.battery + } + } + } + } + + ConnectedRect { + Layout.fillWidth: true + implicitHeight: addrLayout.implicitHeight + Tokens.padding.large * 2 + last: true + + RowLayout { + id: addrLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.medium + + StyledText { + Layout.fillWidth: true + text: qsTr("Address") + } + + StyledText { + text: root.device?.address ?? "" + color: Colours.palette.m3outline + font: Tokens.font.body.small + } + } + } + } +}