diff --git a/Helpers/Icons.qml b/Helpers/Icons.qml index 32df434..4661700 100644 --- a/Helpers/Icons.qml +++ b/Helpers/Icons.qml @@ -1,9 +1,8 @@ pragma Singleton -import qs.Config +import QtQuick import Quickshell import Quickshell.Services.Notifications -import QtQuick Singleton { id: root @@ -78,16 +77,6 @@ Singleton { "99": "thunderstorm" }) - function getAppCategoryIcon(name: string, fallback: string): string { - const categories = DesktopEntries.heuristicLookup(name)?.categories; - - if (categories) - for (const [key, value] of Object.entries(categoryIcons)) - if (categories.includes(key)) - return value; - return fallback; - } - function getAppIcon(name: string, fallback: string): string { const icon = DesktopEntries.heuristicLookup(name)?.icon; if (fallback !== "undefined") @@ -95,6 +84,15 @@ Singleton { return Quickshell.iconPath(icon); } + function getBatteryIcon(percentage: real, charging = false): string { + if (percentage === 1) + return charging ? "battery_charging_full" : "battery_full"; + let level = Math.floor(percentage * 7); + if (charging && (level === 4 || level === 1)) + level--; + return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; + } + function getBluetoothIcon(icon: string): string { if (icon.includes("headset") || icon.includes("headphones")) return "headphones"; diff --git a/Modules/Content.qml b/Modules/Content.qml index ac9e7e6..d59852a 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -83,6 +83,13 @@ Item { } } + Popout { + name: "bluetooth" + + sourceComponent: BluetoothPopout { + } + } + Popout { name: "updates" diff --git a/Modules/SysTray/Popouts/BluetoothPopout.qml b/Modules/SysTray/Popouts/BluetoothPopout.qml new file mode 100644 index 0000000..4547f32 --- /dev/null +++ b/Modules/SysTray/Popouts/BluetoothPopout.qml @@ -0,0 +1,306 @@ +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) + } +} diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index 82549bc..c8b0463 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -42,8 +42,8 @@ CustomClippingRect { checked: Network.wifiEnabled first: true last: connected.count === 0 - subtext: qsTr("Toggle WiFi on or off") - text: qsTr("Set WiFi") + subtext: qsTr("Toggle WiFi device state") + text: checked ? qsTr("Enabled") : qsTr("Disabled") onToggled: Network.setWifi(checked) } @@ -199,12 +199,8 @@ CustomClippingRect { 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: qsTr("%1 strength").arg((networkItem.modelData.signalStrength * 100) + "%") + font.pointSize: Appearance.font.size.small + text: qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") } } diff --git a/Modules/SysTray/TrayWidget.qml b/Modules/SysTray/TrayWidget.qml index a9c7ea8..977bdb2 100644 --- a/Modules/SysTray/TrayWidget.qml +++ b/Modules/SysTray/TrayWidget.qml @@ -62,6 +62,11 @@ RowLayout { id: "upower", item: child }; + if (child.objectName === "bluetoothWidget") + return { + id: "bluetooth", + item: child + }; } } @@ -155,6 +160,10 @@ RowLayout { objectName: "networkWidget" } + BluetoothWidget { + objectName: "bluetoothWidget" + } + UPowerWidget { Layout.fillHeight: true objectName: "upowerWidget" diff --git a/Modules/SysTray/Widgets/BluetoothWidget.qml b/Modules/SysTray/Widgets/BluetoothWidget.qml new file mode 100644 index 0000000..8f43f8b --- /dev/null +++ b/Modules/SysTray/Widgets/BluetoothWidget.qml @@ -0,0 +1,13 @@ +import QtQuick +import QtQuick.Layouts +import qs.Config +import qs.Components + +MaterialIcon { + Layout.alignment: Qt.AlignVCenter + animate: true + color: DynamicColors.palette.m3onSurface // Network.connected ? root.textColor : DynamicColors.palette.m3error + fill: 1 + font.pointSize: Appearance.font.size.larger + text: "bluetooth" +} diff --git a/Modules/SysTray/Widgets/NetworkWidget.qml b/Modules/SysTray/Widgets/NetworkWidget.qml index b03f9c0..3793fdd 100644 --- a/Modules/SysTray/Widgets/NetworkWidget.qml +++ b/Modules/SysTray/Widgets/NetworkWidget.qml @@ -1,24 +1,13 @@ import QtQuick import QtQuick.Layouts -import Quickshell.Io -import Quickshell.Services.Pipewire -import qs.Daemons -import qs.Modules import qs.Config import qs.Components -RowLayout { - id: root - - // property color barColor: DynamicColors.palette.m3primary - property color textColor: DynamicColors.palette.m3onSurface - - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - animate: true - color: root.textColor // Network.connected ? root.textColor : DynamicColors.palette.m3error - fill: 1 - font.pointSize: Appearance.font.size.larger - text: "android_wifi_4_bar" - } +MaterialIcon { + Layout.alignment: Qt.AlignVCenter + animate: true + color: DynamicColors.palette.m3onSurface // Network.connected ? root.textColor : DynamicColors.palette.m3error + fill: 1 + font.pointSize: Appearance.font.size.larger + text: "android_wifi_4_bar" }