From 1d63e6755496e2d8c6db22426f8160d12a2717b4 Mon Sep 17 00:00:00 2001 From: inorishio Date: Sat, 13 Jun 2026 18:22:03 +0200 Subject: [PATCH 01/15] removed network module and moved it to tray, have a popout window without content! everything still in wi.i.p. --- Helpers/Network.qml | 16 +++++++++ Modules/Bar/Bar.qml | 10 ------ Modules/Bar/BarLoader.qml | 1 - Modules/Content.qml | 1 - Modules/Network/NetworkPopout.qml | 36 -------------------- Modules/Network/NetworkWidget.qml | 25 -------------- Modules/SysTray/Popouts/NetworkPopout.qml | 41 +++++++++++++++++++++++ Modules/SysTray/TrayWidget.qml | 9 +++++ Modules/SysTray/Widgets/NetworkWidget.qml | 24 +++++++++++++ 9 files changed, 90 insertions(+), 73 deletions(-) create mode 100644 Helpers/Network.qml delete mode 100644 Modules/Network/NetworkPopout.qml delete mode 100644 Modules/Network/NetworkWidget.qml create mode 100644 Modules/SysTray/Popouts/NetworkPopout.qml create mode 100644 Modules/SysTray/Widgets/NetworkWidget.qml diff --git a/Helpers/Network.qml b/Helpers/Network.qml new file mode 100644 index 0000000..97f7c8f --- /dev/null +++ b/Helpers/Network.qml @@ -0,0 +1,16 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Networking +import qs.Components +import qs.Config +import qs.Modules +import qs.Helpers as Helpers + +item { + id: root + + function networkScan() { + Networking.scanForNetworks(); + } +} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 9ac299b..5f2f687 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -9,7 +9,6 @@ import qs.Config import qs.Helpers import qs.Modules.SysTray import qs.Modules.SysTray.Widgets -import qs.Modules.Network import qs.Modules.Updates RowLayout { @@ -176,15 +175,6 @@ RowLayout { } } - DelegateChoice { - roleValue: "network" - - delegate: WrappedLoader { - sourceComponent: NetworkWidget { - } - } - } - DelegateChoice { roleValue: "media" diff --git a/Modules/Bar/BarLoader.qml b/Modules/Bar/BarLoader.qml index b5e0d75..914d64a 100644 --- a/Modules/Bar/BarLoader.qml +++ b/Modules/Bar/BarLoader.qml @@ -10,7 +10,6 @@ import qs.Config import qs.Helpers import qs.Modules.SysTray import qs.Modules.SysTray.Widgets -import qs.Modules.Network Item { id: root diff --git a/Modules/Content.qml b/Modules/Content.qml index f3035d3..77c5fa0 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -6,7 +6,6 @@ import QtQuick import qs.Config import qs.Components import qs.Modules.WSOverview -import qs.Modules.Network import qs.Modules.SysTray.Popouts import qs.Modules.Updates diff --git a/Modules/Network/NetworkPopout.qml b/Modules/Network/NetworkPopout.qml deleted file mode 100644 index 519bc6c..0000000 --- a/Modules/Network/NetworkPopout.qml +++ /dev/null @@ -1,36 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import Quickshell.Networking -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Modules -import qs.Helpers as Helpers - -Item { - id: root - - required property var wrapper - - ColumnLayout { - id: layout - - spacing: 8 - - Repeater { - model: Helpers.Network.devices - - CustomRadioButton { - id: network - - required property NetworkDevice modelData - - checked: Helpers.Network.activeDevice?.name === modelData.name - text: modelData.description - visible: modelData.name !== "lo" - } - } - } -} diff --git a/Modules/Network/NetworkWidget.qml b/Modules/Network/NetworkWidget.qml deleted file mode 100644 index 53463ee..0000000 --- a/Modules/Network/NetworkWidget.qml +++ /dev/null @@ -1,25 +0,0 @@ -import Quickshell -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Modules - -Item { - id: root - - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitWidth: layout.implicitWidth - - RowLayout { - id: layout - - anchors.bottom: parent.bottom - anchors.top: parent.top - - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - text: "android_wifi_4_bar" - } - } -} diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml new file mode 100644 index 0000000..65a7127 --- /dev/null +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -0,0 +1,41 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Networking +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config +import qs.Modules +import qs.Helpers as Helpers + +Item { + id: root + + required property var wrapper + + implicitHeight: networkPop.implicitHeight + implicitWidth: networkPop.implicitWidth + + CustomRect { + id: networkPop + + anchors.horizontalCenter: parent.horizontalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: 500 + 5 * 2 + implicitWidth: 500 + 8 * 2 + radius: (20 - Appearance.padding.small) * Appearance.rounding.scale + + Item { + id: networkPopContent + + anchors.fill: parent + anchors.margins: Appearance.padding.small + + // text { + // font.pixelSize: 20 + // text: qsTr("Network Settings") + // } + } + } +} diff --git a/Modules/SysTray/TrayWidget.qml b/Modules/SysTray/TrayWidget.qml index 2d3a768..f6fd3ea 100644 --- a/Modules/SysTray/TrayWidget.qml +++ b/Modules/SysTray/TrayWidget.qml @@ -28,6 +28,11 @@ RowLayout { id: "audio", item: child }; + if (child.objectName === "networkWidget" && Config.barConfig.popouts.network) + return { + id: "network", + item: child + }; if (child.objectName === "upowerWidget" && Config.barConfig.popouts.upower) return { id: "upower", @@ -113,6 +118,10 @@ RowLayout { objectName: "audioWidget" } + NetworkWidget { + objectName: "networkWidget" + } + UPowerWidget { height: parent.height objectName: "upowerWidget" diff --git a/Modules/SysTray/Widgets/NetworkWidget.qml b/Modules/SysTray/Widgets/NetworkWidget.qml new file mode 100644 index 0000000..b03f9c0 --- /dev/null +++ b/Modules/SysTray/Widgets/NetworkWidget.qml @@ -0,0 +1,24 @@ +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" + } +} -- 2.47.3 From 0e0f8555ede925c008f40b324edb2d5ee9970ad3 Mon Sep 17 00:00:00 2001 From: inorishio Date: Mon, 15 Jun 2026 20:48:01 +0200 Subject: [PATCH 02/15] fixing singleton in helpers for network + getting output from systray --- Config/BarConfig.qml | 4 --- Helpers/Network.qml | 12 ++----- Modules/SysTray/Popouts/NetworkPopout.qml | 43 ++++++++++++----------- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/Config/BarConfig.qml b/Config/BarConfig.qml index 0f489ef..a35e94b 100644 --- a/Config/BarConfig.qml +++ b/Config/BarConfig.qml @@ -40,10 +40,6 @@ JsonObject { id: "tray", enabled: true }, - { - id: "network", - enabled: false - }, { id: "clock", enabled: true diff --git a/Helpers/Network.qml b/Helpers/Network.qml index 97f7c8f..78aecd3 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -1,16 +1,10 @@ -pragma ComponentBehavior: Bound +pragma Singleton import Quickshell import Quickshell.Networking -import qs.Components -import qs.Config -import qs.Modules -import qs.Helpers as Helpers -item { +Singleton { id: root - function networkScan() { - Networking.scanForNetworks(); - } + readonly property list network: Networking.devices.values } diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index 65a7127..a7a512a 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -1,41 +1,44 @@ pragma ComponentBehavior: Bound import Quickshell -import Quickshell.Networking import QtQuick import QtQuick.Layouts import qs.Components import qs.Config import qs.Modules -import qs.Helpers as Helpers +import qs.Helpers -Item { +CustomClippingRect { id: root required property var wrapper - implicitHeight: networkPop.implicitHeight - implicitWidth: networkPop.implicitWidth + anchors.horizontalCenter: parent.horizontalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: 500 + 5 * 2 + implicitWidth: 500 + 8 * 2 + radius: (20 - Appearance.padding.small) * Appearance.rounding.scale - CustomRect { - id: networkPop + Item { + id: networkPopContent - anchors.horizontalCenter: parent.horizontalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: 500 + 5 * 2 - implicitWidth: 500 + 8 * 2 - radius: (20 - Appearance.padding.small) * Appearance.rounding.scale + anchors.fill: parent + anchors.margins: Appearance.padding.small - Item { - id: networkPopContent + CustomText { + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.top: parent.top + font.pointSize: Appearance.font.size.large + text: qsTr("Network") + } - anchors.fill: parent - anchors.margins: Appearance.padding.small + StateLayer { + id: buttonScan - // text { - // font.pixelSize: 20 - // text: qsTr("Network Settings") - // } + onClicked: { + console.log(Network.network.length); + } } } } -- 2.47.3 From 1adcb9841c30b893991f033b1deca495a7c16528 Mon Sep 17 00:00:00 2001 From: inorishio Date: Mon, 22 Jun 2026 23:34:58 +0200 Subject: [PATCH 03/15] Added network properties, nicNames, netDevices, networkName < reminder I should use layouts. --- Helpers/Network.qml | 37 ++++++++++++++++++++++- Modules/SysTray/Popouts/NetworkPopout.qml | 15 ++++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/Helpers/Network.qml b/Helpers/Network.qml index 78aecd3..1bf124e 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -6,5 +6,40 @@ import Quickshell.Networking Singleton { id: root - readonly property list network: Networking.devices.values + readonly property list netDevice: Networking.devices.values + readonly property string networkName: getNetworkName() + readonly property list nicNames: networkInterfaceCardNames() + + // Useless will prob remove + function getConnectedDevices() { + let connectedDevices = []; + + for (var i = 0; i < netDevice.length; i++) { + if (netDevice[i].connected === true) { + connectedDevices.push(netDevice[i].name); + } + } + return connectedDevices; + } + + // SHOULD retrieve network names of connected devices + // Currently only gives connected nic name + function getNetworkName() { + const devices = netDevice.filter(device => device.connected === true); + for (var i = 0; i < devices.length; i++) { + return devices[i].name; + } + return "Failed network name"; + } + + // Searches wired/wireless devices and sets them in a list + function networkInterfaceCardNames() { + let nicList = []; + + for (let i = 0; i < netDevice.length; ++i) { + nicList.push(netDevice[i].name); + } + + return nicList; + } } diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index a7a512a..8e9b82e 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -1,11 +1,8 @@ pragma ComponentBehavior: Bound -import Quickshell import QtQuick -import QtQuick.Layouts import qs.Components import qs.Config -import qs.Modules import qs.Helpers CustomClippingRect { @@ -33,11 +30,21 @@ CustomClippingRect { text: qsTr("Network") } + CustomText { + anchors.margins: Appearance.padding.large + anchors.right: parent.right + anchors.top: parent.top + font.pointSize: Appearance.font.size.large + text: Network.networkName + } + StateLayer { id: buttonScan onClicked: { - console.log(Network.network.length); + console.log("Network devices: " + Network.netDevice); + console.log("Network interface names: " + Network.nicNames); + console.log("Connected devices: " + Network.networkName); } } } -- 2.47.3 From f27c9afc2514a8927a0db0f4fe6e93b52b62bb85 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 26 Jun 2026 15:15:03 +0200 Subject: [PATCH 04/15] more methods for wifi and scanning + layout --- .gitignore | 1 + Helpers/Network.qml | 80 +++++++++ Modules/SysTray/Popouts/NetworkPopout.qml | 206 ++++++++++++++++++++-- 3 files changed, 268 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 39a44e6..48e59ee 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ dist/ **/target/ **/test-plugins/ **/Charts/ +**/network-dev/ diff --git a/Helpers/Network.qml b/Helpers/Network.qml index 1bf124e..5d608c0 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -2,10 +2,19 @@ pragma Singleton import Quickshell import Quickshell.Networking +import QtQuick Singleton { id: root + readonly property list devices: Networking.devices.values + readonly property list networks: initialBuildNetworks() + property bool scanning: false + readonly property list wifiDevices: devices.filter(d => wifiDevice(d)) + readonly property bool wifiEnabled: Networking.wifiEnabled + + // Original code + readonly property list netDevice: Networking.devices.values readonly property string networkName: getNetworkName() readonly property list nicNames: networkInterfaceCardNames() @@ -42,4 +51,75 @@ Singleton { return nicList; } + // + + function initialBuildNetworks(): void { + const init = []; + for (const d of wifiDevices) { + d.scannerEnabled = true; + init.push(...d.networks.values); + d.scannerEnabled = false; + } + + networks = init; + } + + function isSecure(security): bool { + return !(security === WifiSecurityType.Open); + } + + function rebuildNetworks(): void { + if (!scanning) + return; + + const next = []; + for (const d of wifiDevices) { + next.push(...d.networks.values); + } + + networks = next; + } + + function rescanWifi(): void { + scanning = true; + setScan(true); + scanTimer.restart(); + } + + function setScan(value: bool): void { + for (const d of wifiDevices) { + d.scannerEnabled = value; + } + } + + function setWifi(value: bool): void { + Networking.wifiEnabled = value; + } + + function wifiDevice(dev): bool { + return dev.type === DeviceType.Wifi; + } + + Timer { + id: scanTimer + + interval: 5000 + repeat: false + + onTriggered: { + root.rebuildNetworks(); + root.setScan(false); + root.scanning = false; + } + } + + Timer { + interval: 500 + repeat: true + running: root.scanning + + onTriggered: { + root.rebuildNetworks(); + } + } } diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index 8e9b82e..63ce794 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -1,6 +1,8 @@ pragma ComponentBehavior: Bound import QtQuick +import QtQuick.Layouts +import Quickshell import qs.Components import qs.Config import qs.Helpers @@ -12,40 +14,206 @@ CustomClippingRect { anchors.horizontalCenter: parent.horizontalCenter color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: 500 + 5 * 2 + implicitHeight: networkPopContent.height + networks.implicitHeight + networkPopContent.anchors.margins + networks.anchors.margins * 2 implicitWidth: 500 + 8 * 2 radius: (20 - Appearance.padding.small) * Appearance.rounding.scale - Item { + ColumnLayout { id: networkPopContent - anchors.fill: parent - anchors.margins: Appearance.padding.small + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.right: parent.right + anchors.top: parent.top CustomText { - anchors.left: parent.left - anchors.margins: Appearance.padding.large - anchors.top: parent.top + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.rightMargin: Appearance.padding.extraSmall font.pointSize: Appearance.font.size.large text: qsTr("Network") } - CustomText { - anchors.margins: Appearance.padding.large - anchors.right: parent.right - anchors.top: parent.top - font.pointSize: Appearance.font.size.large - text: Network.networkName + Toggle { + Layout.preferredHeight: visible ? implicitHeight : 0 + checked: Network.wifiEnabled + label: qsTr("WiFi enabled") + + toggle.onToggled: Network.setWifi(checked) } - StateLayer { - id: buttonScan + CustomText { + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.rightMargin: Appearance.padding.extraSmall + Layout.topMargin: visible ? Appearance.spacing.small : 0 + color: DynamicColors.palette.m3onSurfaceVariant + text: qsTr("%1 networks available").arg(Network.networks.length) // qmllint disable missing-property + } + } - onClicked: { - console.log("Network devices: " + Network.netDevice); - console.log("Network interface names: " + Network.nicNames); - console.log("Connected devices: " + Network.networkName); + ColumnLayout { + id: networks + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.right: parent.right + anchors.top: networkPopContent.bottom + + Repeater { + model: ScriptModel { + values: [...Network.networks] + } + + RowLayout { + id: networkItem + + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.rightMargin: Appearance.padding.extraSmall + opacity: 0 + scale: 0.7 + spacing: Appearance.spacing.small + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + Behavior on scale { + Anim { + } + } + + Component.onCompleted: { + opacity = 1; + scale = 1; + } + + MaterialIcon { + color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant + text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, Network.isSecure(networkItem.modelData.security)) + } + + CustomText { + Layout.fillWidth: true + Layout.leftMargin: Appearance.spacing.extraSmall + Layout.rightMargin: Appearance.spacing.extraSmall + color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface + elide: Text.ElideRight + text: networkItem.modelData.name + } + + CustomRect { + color: Qt.alpha(DynamicColors.palette.m3primary, networkItem.modelData.active ? 1 : 0) + implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.extraSmall + implicitWidth: implicitHeight + radius: Appearance.rounding.full + + // CircularIndicator { + // anchors.fill: parent + // running: networkItem.loading + // } + + StateLayer { + color: networkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + + onClicked: { + console.log(Network.devices[1].scannerEnabled, Network.devices[2].scannerEnabled, Network.devices[3].scannerEnabled, Network.devices[4].scannerEnabled); + } + } + + MaterialIcon { + id: wirelessConnectIcon + + anchors.centerIn: parent + animate: true + color: networkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + text: networkItem.modelData.active ? "link_off" : "link" + + // opacity: networkItem.loading ? 0 : 1 + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + } + } + } + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.topMargin: visible ? Appearance.spacing.small : 0 + color: DynamicColors.palette.m3primaryContainer + implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small + radius: Appearance.rounding.full + + StateLayer { + color: DynamicColors.palette.m3onPrimaryContainer + enabled: !Network.scanning + + onClicked: Network.rescanWifi() + } + + RowLayout { + id: rescanBtn + + anchors.centerIn: parent + opacity: Network.scanning ? 0 : 1 + spacing: Appearance.spacing.small + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + + MaterialIcon { + id: scanIcon + + Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) + animate: true + color: DynamicColors.palette.m3onPrimaryContainer + text: "wifi_find" + } + + CustomText { + Layout.topMargin: -Math.round(scanIcon.fontInfo.pointSize * 0.0575) + color: DynamicColors.palette.m3onPrimaryContainer + text: qsTr("Rescan networks") + } + } + + CircularIndicator { + anchors.centerIn: parent + bgColor: "transparent" + implicitSize: parent.implicitHeight - Appearance.padding.large + running: Network.scanning + strokeWidth: Appearance.padding.extraSmall / 2 } } } + + component Toggle: RowLayout { + property alias checked: toggle.checked + required property string label + property alias toggle: toggle + + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.extraSmall + spacing: Appearance.spacing.small + + CustomText { + Layout.fillWidth: true + text: parent.label + } + + CustomSwitch { + id: toggle + } + } } -- 2.47.3 From f309debea510a59f9b2a82f2157da2ba82bf23e4 Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 2 Jul 2026 16:06:52 +0200 Subject: [PATCH 05/15] set scannerEnabled when popout is visible + remove scan button --- Helpers/Network.qml | 67 ++------ .../Pages/Panels/Bar/BarStatusIcons.qml | 23 ++- Modules/SysTray/Popouts/NetworkPopout.qml | 157 +++++++++++------- 3 files changed, 130 insertions(+), 117 deletions(-) diff --git a/Helpers/Network.qml b/Helpers/Network.qml index 5d608c0..7b3a0bc 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -8,10 +8,21 @@ Singleton { id: root readonly property list devices: Networking.devices.values - readonly property list networks: initialBuildNetworks() + readonly property list networks: { + const list = [] + for (const d of wifiDevices) + list.push(...d.networks.values) + return list; + } property bool scanning: false readonly property list wifiDevices: devices.filter(d => wifiDevice(d)) readonly property bool wifiEnabled: Networking.wifiEnabled + property bool active: false + + onActiveChanged: { + for (const d of wifiDevices) + d.scannerEnabled = active; + } // Original code @@ -53,37 +64,8 @@ Singleton { } // - function initialBuildNetworks(): void { - const init = []; - for (const d of wifiDevices) { - d.scannerEnabled = true; - init.push(...d.networks.values); - d.scannerEnabled = false; - } - - networks = init; - } - function isSecure(security): bool { - return !(security === WifiSecurityType.Open); - } - - function rebuildNetworks(): void { - if (!scanning) - return; - - const next = []; - for (const d of wifiDevices) { - next.push(...d.networks.values); - } - - networks = next; - } - - function rescanWifi(): void { - scanning = true; - setScan(true); - scanTimer.restart(); + return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae); } function setScan(value: bool): void { @@ -99,27 +81,4 @@ Singleton { function wifiDevice(dev): bool { return dev.type === DeviceType.Wifi; } - - Timer { - id: scanTimer - - interval: 5000 - repeat: false - - onTriggered: { - root.rebuildNetworks(); - root.setScan(false); - root.scanning = false; - } - } - - Timer { - interval: 500 - repeat: true - running: root.scanning - - onTriggered: { - root.rebuildNetworks(); - } - } } diff --git a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml index b2655d5..8a73337 100644 --- a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml +++ b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml @@ -39,13 +39,14 @@ PageBase { onToggled: Config.bar.tray.showMicrophone = checked } + ToggleRow { + checked: Config.bar.tray.showNetwork + text: qsTr("Network") + + onToggled: Config.bar.tray.showNetwork = checked + } + //////// FOR LATER: - // ToggleRow { - // checked: Config.bar.tray.showNetwork - // text: qsTr("Network") - // - // onToggled: Config.bar.tray.showNetwork = checked - // } // // ToggleRow { // checked: Config.bar.tray.showWifi @@ -94,5 +95,15 @@ PageBase { onToggled: Config.bar.popouts.upower = checked } + + ToggleRow { + checked: Config.bar.popouts.network + last: true + settingAnchor: "bar-status-network-popout" + subtext: qsTr("Show a details popout when hovering the network icon") + text: qsTr("Network popout on hover") + + onToggled: Config.bar.popouts.network = checked + } } } diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index 63ce794..12c63ef 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -18,6 +18,9 @@ CustomClippingRect { implicitWidth: 500 + 8 * 2 radius: (20 - Appearance.padding.small) * Appearance.rounding.scale + Component.onCompleted: Network.active = true + Component.onDestruction: Network.active = false + ColumnLayout { id: networkPopContent @@ -59,9 +62,104 @@ CustomClippingRect { anchors.right: parent.right anchors.top: networkPopContent.bottom + CustomText { + text: qsTr("Known networks") + } + Repeater { model: ScriptModel { - values: [...Network.networks] + values: [...Network.networks].filter(n => n.known) + } + + RowLayout { + id: knownNetworkItem + + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.rightMargin: Appearance.padding.extraSmall + opacity: 0 + scale: 0.7 + spacing: Appearance.spacing.small + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + Behavior on scale { + Anim { + } + } + + Component.onCompleted: { + opacity = 1; + scale = 1; + } + + MaterialIcon { + color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant + text: Icons.getNetworkIcon(knownNetworkItem.modelData.signalStrength * 100, Network.isSecure(knownNetworkItem.modelData.security)) + } + + CustomText { + Layout.fillWidth: true + Layout.leftMargin: Appearance.spacing.extraSmall + Layout.rightMargin: Appearance.spacing.extraSmall + color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface + elide: Text.ElideRight + text: knownNetworkItem.modelData.name + } + + CustomRect { + color: Qt.alpha(DynamicColors.palette.m3primary, knownNetworkItem.modelData.active ? 1 : 0) + implicitHeight: knownWirelessConnectIcon.implicitHeight + Appearance.padding.extraSmall + implicitWidth: implicitHeight + radius: Appearance.rounding.full + + // CircularIndicator { + // anchors.fill: parent + // running: knownNetworkItem.loading + // } + + StateLayer { + color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + + onClicked: {} + } + + MaterialIcon { + id: knownWirelessConnectIcon + + anchors.centerIn: parent + animate: true + color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + text: knownNetworkItem.modelData.active ? "link_off" : "link" + + // opacity: knownNetworkItem.loading ? 0 : 1 + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + } + } + } + + Item { + id: spacer + + Layout.preferredHeight: networkRepeater.count > 0 ? Appearance.spacing.normal : 0 + } + + Repeater { + id: networkRepeater + + model: ScriptModel { + values: [...Network.networks].filter(n => !n.known) } RowLayout { @@ -119,9 +217,7 @@ CustomClippingRect { StateLayer { color: networkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - onClicked: { - console.log(Network.devices[1].scannerEnabled, Network.devices[2].scannerEnabled, Network.devices[3].scannerEnabled, Network.devices[4].scannerEnabled); - } + onClicked: {} } MaterialIcon { @@ -143,59 +239,6 @@ CustomClippingRect { } } } - - CustomRect { - Layout.fillWidth: true - Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.spacing.small : 0 - color: DynamicColors.palette.m3primaryContainer - implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small - radius: Appearance.rounding.full - - StateLayer { - color: DynamicColors.palette.m3onPrimaryContainer - enabled: !Network.scanning - - onClicked: Network.rescanWifi() - } - - RowLayout { - id: rescanBtn - - anchors.centerIn: parent - opacity: Network.scanning ? 0 : 1 - spacing: Appearance.spacing.small - - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } - - MaterialIcon { - id: scanIcon - - Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) - animate: true - color: DynamicColors.palette.m3onPrimaryContainer - text: "wifi_find" - } - - CustomText { - Layout.topMargin: -Math.round(scanIcon.fontInfo.pointSize * 0.0575) - color: DynamicColors.palette.m3onPrimaryContainer - text: qsTr("Rescan networks") - } - } - - CircularIndicator { - anchors.centerIn: parent - bgColor: "transparent" - implicitSize: parent.implicitHeight - Appearance.padding.large - running: Network.scanning - strokeWidth: Appearance.padding.extraSmall / 2 - } - } } component Toggle: RowLayout { -- 2.47.3 From e0873304d16a6fb5492b290b8512d51f5773b9fb Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 2 Jul 2026 21:53:42 +0200 Subject: [PATCH 06/15] add sorting by strength --- Helpers/Network.qml | 43 ++++++++++++----------- Modules/SysTray/Popouts/NetworkPopout.qml | 8 +++-- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/Helpers/Network.qml b/Helpers/Network.qml index 7b3a0bc..593c34d 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -7,28 +7,25 @@ import QtQuick Singleton { id: root - readonly property list devices: Networking.devices.values - readonly property list networks: { - const list = [] - for (const d of wifiDevices) - list.push(...d.networks.values) - return list; - } - property bool scanning: false - readonly property list wifiDevices: devices.filter(d => wifiDevice(d)) - readonly property bool wifiEnabled: Networking.wifiEnabled property bool active: false - - onActiveChanged: { - for (const d of wifiDevices) - d.scannerEnabled = active; - } + readonly property list devices: Networking.devices.values + readonly property list knownNetworks: networks.filter(n => n.known) // Original code readonly property list netDevice: Networking.devices.values readonly property string networkName: getNetworkName() + readonly property list networks: { + const list = []; + for (const d of wifiDevices) + list.push(...d.networks.values); + return list; + } readonly property list nicNames: networkInterfaceCardNames() + property bool scanning: false + readonly property list unknownNetworks: networks.filter(n => !n.known) + readonly property list wifiDevices: devices.filter(d => wifiDevice(d)) + readonly property bool wifiEnabled: Networking.wifiEnabled // Useless will prob remove function getConnectedDevices() { @@ -52,6 +49,12 @@ Singleton { return "Failed network name"; } + // + + function isSecure(security): bool { + return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae); + } + // Searches wired/wireless devices and sets them in a list function networkInterfaceCardNames() { let nicList = []; @@ -62,11 +65,6 @@ Singleton { return nicList; } - // - - function isSecure(security): bool { - return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae); - } function setScan(value: bool): void { for (const d of wifiDevices) { @@ -81,4 +79,9 @@ Singleton { function wifiDevice(dev): bool { return dev.type === DeviceType.Wifi; } + + onActiveChanged: { + for (const d of wifiDevices) + d.scannerEnabled = active; + } } diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index 12c63ef..853ced4 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -68,7 +68,9 @@ CustomClippingRect { Repeater { model: ScriptModel { - values: [...Network.networks].filter(n => n.known) + values: [...Network.knownNetworks].sort((a, b) => { + return a.signalStrength - b.signalStrength; + }) } RowLayout { @@ -159,7 +161,9 @@ CustomClippingRect { id: networkRepeater model: ScriptModel { - values: [...Network.networks].filter(n => !n.known) + values: [...Network.unknownNetworks].sort((a, b) => { + return a.signalStrength - b.signalStrength; + }) } RowLayout { -- 2.47.3 From f75f51ef50f75b2ac77fd034661899239d1b086b Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 2 Jul 2026 21:54:58 +0200 Subject: [PATCH 07/15] fix: sort is now correct --- Modules/SysTray/Popouts/NetworkPopout.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index 853ced4..516ec8a 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -69,7 +69,7 @@ CustomClippingRect { Repeater { model: ScriptModel { values: [...Network.knownNetworks].sort((a, b) => { - return a.signalStrength - b.signalStrength; + return b.signalStrength - a.signalStrength; }) } @@ -162,7 +162,7 @@ CustomClippingRect { model: ScriptModel { values: [...Network.unknownNetworks].sort((a, b) => { - return a.signalStrength - b.signalStrength; + return b.signalStrength - a.signalStrength; }) } -- 2.47.3 From 2a4bd3dfdd757d99bdbf5cb30719f109aaa13dfc Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 2 Jul 2026 21:58:01 +0200 Subject: [PATCH 08/15] chore: remove old code --- Helpers/Network.qml | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/Helpers/Network.qml b/Helpers/Network.qml index 593c34d..695fa0f 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -10,62 +10,21 @@ Singleton { property bool active: false readonly property list devices: Networking.devices.values readonly property list knownNetworks: networks.filter(n => n.known) - - // Original code - - readonly property list netDevice: Networking.devices.values - readonly property string networkName: getNetworkName() readonly property list networks: { const list = []; for (const d of wifiDevices) list.push(...d.networks.values); return list; } - readonly property list nicNames: networkInterfaceCardNames() property bool scanning: false readonly property list unknownNetworks: networks.filter(n => !n.known) readonly property list wifiDevices: devices.filter(d => wifiDevice(d)) readonly property bool wifiEnabled: Networking.wifiEnabled - // Useless will prob remove - function getConnectedDevices() { - let connectedDevices = []; - - for (var i = 0; i < netDevice.length; i++) { - if (netDevice[i].connected === true) { - connectedDevices.push(netDevice[i].name); - } - } - return connectedDevices; - } - - // SHOULD retrieve network names of connected devices - // Currently only gives connected nic name - function getNetworkName() { - const devices = netDevice.filter(device => device.connected === true); - for (var i = 0; i < devices.length; i++) { - return devices[i].name; - } - return "Failed network name"; - } - - // - function isSecure(security): bool { return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae); } - // Searches wired/wireless devices and sets them in a list - function networkInterfaceCardNames() { - let nicList = []; - - for (let i = 0; i < netDevice.length; ++i) { - nicList.push(netDevice[i].name); - } - - return nicList; - } - function setScan(value: bool): void { for (const d of wifiDevices) { d.scannerEnabled = value; -- 2.47.3 From 8dc9753e2b16548a631198b2a4332c92ef88ae12 Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 2 Jul 2026 22:09:25 +0200 Subject: [PATCH 09/15] feat: don't add the same network twice --- Helpers/Network.qml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Helpers/Network.qml b/Helpers/Network.qml index 695fa0f..d4678f4 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -12,8 +12,12 @@ Singleton { readonly property list knownNetworks: networks.filter(n => n.known) readonly property list networks: { const list = []; - for (const d of wifiDevices) - list.push(...d.networks.values); + for (const d of wifiDevices) { + for (const n of d.networks.values) { + if (!list.includes(n)) + list.push(n); + } + } return list; } property bool scanning: false -- 2.47.3 From d9856c9409ef20c2256ddb8ffcddbe8ccf397975 Mon Sep 17 00:00:00 2001 From: inorishio Date: Thu, 2 Jul 2026 22:46:09 +0200 Subject: [PATCH 10/15] Chore: created NetworkItem component, renamed network header to Wifi --- Modules/SysTray/Popouts/NetworkPopout.qml | 232 ++++++++-------------- 1 file changed, 79 insertions(+), 153 deletions(-) diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index 516ec8a..0d72ae4 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -33,7 +33,7 @@ CustomClippingRect { Layout.preferredHeight: visible ? implicitHeight : 0 Layout.rightMargin: Appearance.padding.extraSmall font.pointSize: Appearance.font.size.large - text: qsTr("Network") + text: qsTr("Wifi") } Toggle { @@ -58,11 +58,12 @@ CustomClippingRect { anchors.bottom: parent.bottom anchors.left: parent.left - anchors.margins: Appearance.padding.large + anchors.margins: Appearance.padding.normal anchors.right: parent.right anchors.top: networkPopContent.bottom CustomText { + Layout.leftMargin: Appearance.padding.normal text: qsTr("Known networks") } @@ -73,88 +74,19 @@ CustomClippingRect { }) } - RowLayout { - id: knownNetworkItem - - required property var modelData - - Layout.fillWidth: true - Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.rightMargin: Appearance.padding.extraSmall - opacity: 0 - scale: 0.7 - spacing: Appearance.spacing.small - - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } - Behavior on scale { - Anim { - } - } - - Component.onCompleted: { - opacity = 1; - scale = 1; - } - - MaterialIcon { - color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant - text: Icons.getNetworkIcon(knownNetworkItem.modelData.signalStrength * 100, Network.isSecure(knownNetworkItem.modelData.security)) - } - - CustomText { - Layout.fillWidth: true - Layout.leftMargin: Appearance.spacing.extraSmall - Layout.rightMargin: Appearance.spacing.extraSmall - color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface - elide: Text.ElideRight - text: knownNetworkItem.modelData.name - } - - CustomRect { - color: Qt.alpha(DynamicColors.palette.m3primary, knownNetworkItem.modelData.active ? 1 : 0) - implicitHeight: knownWirelessConnectIcon.implicitHeight + Appearance.padding.extraSmall - implicitWidth: implicitHeight - radius: Appearance.rounding.full - - // CircularIndicator { - // anchors.fill: parent - // running: knownNetworkItem.loading - // } - - StateLayer { - color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - - onClicked: {} - } - - MaterialIcon { - id: knownWirelessConnectIcon - - anchors.centerIn: parent - animate: true - color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - text: knownNetworkItem.modelData.active ? "link_off" : "link" - - // opacity: knownNetworkItem.loading ? 0 : 1 - - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } - } - } + NetworkItem { } } Item { id: spacer - Layout.preferredHeight: networkRepeater.count > 0 ? Appearance.spacing.normal : 0 + Layout.preferredHeight: networkRepeater.count > 0 ? Appearance.spacing.extraSmall : 0 + } + + CustomText { + Layout.leftMargin: Appearance.padding.normal + text: qsTr("Networks") } Repeater { @@ -166,85 +98,79 @@ CustomClippingRect { }) } - RowLayout { - id: networkItem - - required property var modelData - - Layout.fillWidth: true - Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.rightMargin: Appearance.padding.extraSmall - opacity: 0 - scale: 0.7 - spacing: Appearance.spacing.small - - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } - Behavior on scale { - Anim { - } - } - - Component.onCompleted: { - opacity = 1; - scale = 1; - } - - MaterialIcon { - color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant - text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, Network.isSecure(networkItem.modelData.security)) - } - - CustomText { - Layout.fillWidth: true - Layout.leftMargin: Appearance.spacing.extraSmall - Layout.rightMargin: Appearance.spacing.extraSmall - color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface - elide: Text.ElideRight - text: networkItem.modelData.name - } - - CustomRect { - color: Qt.alpha(DynamicColors.palette.m3primary, networkItem.modelData.active ? 1 : 0) - implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.extraSmall - implicitWidth: implicitHeight - radius: Appearance.rounding.full - - // CircularIndicator { - // anchors.fill: parent - // running: networkItem.loading - // } - - StateLayer { - color: networkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - - onClicked: {} - } - - MaterialIcon { - id: wirelessConnectIcon - - anchors.centerIn: parent - animate: true - color: networkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - text: networkItem.modelData.active ? "link_off" : "link" - - // opacity: networkItem.loading ? 0 : 1 - - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } - } - } + NetworkItem { } } } + component NetworkItem: CustomRect { + id: knownNetworkItem + + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: visible ? knownNetworkRow.implicitHeight + Appearance.padding.smaller * 2 : 0 + Layout.rightMargin: Appearance.padding.extraSmall + radius: Appearance.rounding.small + + RowLayout { + id: knownNetworkRow + + anchors.fill: parent + anchors.leftMargin: Appearance.padding.larger + anchors.rightMargin: Appearance.padding.normal + opacity: 0 + scale: 0.7 + spacing: Appearance.spacing.small + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + Behavior on scale { + Anim { + } + } + + Component.onCompleted: { + opacity = 1; + scale = 1; + } + + MaterialIcon { + color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant + text: Icons.getNetworkIcon(knownNetworkItem.modelData.signalStrength * 100, Network.isSecure(knownNetworkItem.modelData.security)) + } + + CustomText { + Layout.fillWidth: true + Layout.leftMargin: Appearance.spacing.extraSmall + Layout.rightMargin: Appearance.spacing.extraSmall + color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface + elide: Text.ElideRight + text: knownNetworkItem.modelData.name + } + + CustomRect { + color: Qt.alpha(DynamicColors.palette.m3primary, knownNetworkItem.modelData.active ? 1 : 0) + implicitHeight: knownWirelessConnectIcon.implicitHeight + Appearance.padding.extraSmall + implicitWidth: implicitHeight + radius: Appearance.rounding.full + + // CircularIndicator { + // anchors.fill: parent + // running: knownNetworkItem.loading + // } + } + } + + StateLayer { + color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + + onClicked: {} + } + } component Toggle: RowLayout { property alias checked: toggle.checked required property string label -- 2.47.3 From aadecbfe443daa8e0298e9eb39e6bfa7e6367b1f Mon Sep 17 00:00:00 2001 From: inorishio Date: Wed, 8 Jul 2026 22:22:03 +0200 Subject: [PATCH 11/15] Chore: created Spacer component, moved x available networks, added connected networks --- Helpers/Network.qml | 3 +- Modules/SysTray/Popouts/NetworkPopout.qml | 59 +++++++++++++++++------ 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/Helpers/Network.qml b/Helpers/Network.qml index d4678f4..a73c00d 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -8,8 +8,9 @@ Singleton { id: root property bool active: false + readonly property list connectedNetworks: networks.filter(n => n.connected) readonly property list devices: Networking.devices.values - readonly property list knownNetworks: networks.filter(n => n.known) + readonly property list knownNetworks: networks.filter(n => n.known && !n.known.ConnectionState.Connected) readonly property list networks: { const list = []; for (const d of wifiDevices) { diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index 0d72ae4..9ff2f5d 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -43,14 +43,6 @@ CustomClippingRect { toggle.onToggled: Network.setWifi(checked) } - - CustomText { - Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.rightMargin: Appearance.padding.extraSmall - Layout.topMargin: visible ? Appearance.spacing.small : 0 - color: DynamicColors.palette.m3onSurfaceVariant - text: qsTr("%1 networks available").arg(Network.networks.length) // qmllint disable missing-property - } } ColumnLayout { @@ -64,10 +56,36 @@ CustomClippingRect { CustomText { Layout.leftMargin: Appearance.padding.normal - text: qsTr("Known networks") + text: qsTr("Connected") + visible: Network.connectedNetworks.length > 0 } Repeater { + visible: Network.connectedNetworks.length > 0 + + model: ScriptModel { + values: [...Network.connectedNetworks].sort((a, b) => { + return b.signalStrength - a.signalStrength; + }) + } + + NetworkItem { + } + } + + Spacer { + visible: Network.connectedNetworks.length > 0 + } + + CustomText { + Layout.leftMargin: Appearance.padding.normal + text: qsTr("Known networks") + visible: Network.knownNetworks.length > 0 + } + + Repeater { + visible: Network.knownNetworks.length > 0 + model: ScriptModel { values: [...Network.knownNetworks].sort((a, b) => { return b.signalStrength - a.signalStrength; @@ -78,10 +96,8 @@ CustomClippingRect { } } - Item { - id: spacer - - Layout.preferredHeight: networkRepeater.count > 0 ? Appearance.spacing.extraSmall : 0 + Spacer { + visible: Network.knownNetworks.length > 0 } CustomText { @@ -89,6 +105,14 @@ CustomClippingRect { text: qsTr("Networks") } + CustomText { + Layout.leftMargin: Appearance.padding.normal + Layout.preferredHeight: visible ? implicitHeight : 0 + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.extraSmall + text: qsTr("%1 networks available").arg(Network.networks.length) // qmllint disable missing-property + } + Repeater { id: networkRepeater @@ -168,9 +192,16 @@ CustomClippingRect { StateLayer { color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - onClicked: {} + onClicked: { + NetworkPassword.requestOpen(); + } } } + component Spacer: Item { + id: spacer + + Layout.preferredHeight: networkRepeater.count > 0 ? Appearance.spacing.extraSmall : 0 + } component Toggle: RowLayout { property alias checked: toggle.checked required property string label -- 2.47.3 From a64eadd14a65ce91463263d20d162136c50426dc Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 9 Jul 2026 17:33:13 +0200 Subject: [PATCH 12/15] feat: add password overlay for network popout + redesign --- Components/ButtonBase.qml | 4 +- Components/CircularIndicator.qml | 10 +- Components/CustomTextField.qml | 4 +- Config/DockConfig.qml | 2 +- Drawers/Interactions.qml | 2 +- Drawers/Windows.qml | 13 +- Helpers/Network.qml | 65 +++- Modules/Dock/Wrapper.qml | 2 +- .../Popouts/Network/NetworkOverlay.qml | 70 ++++ .../SysTray/Popouts/Network/PSKOverlay.qml | 102 ++++++ .../SysTray/Popouts/Network/PopupManager.qml | 22 ++ Modules/SysTray/Popouts/NetworkPopout.qml | 309 +++++++++++++----- network-dev/wifi-sim.sh | 183 +++++++++-- 13 files changed, 665 insertions(+), 123 deletions(-) create mode 100644 Modules/SysTray/Popouts/Network/NetworkOverlay.qml create mode 100644 Modules/SysTray/Popouts/Network/PSKOverlay.qml create mode 100644 Modules/SysTray/Popouts/Network/PopupManager.qml diff --git a/Components/ButtonBase.qml b/Components/ButtonBase.qml index 5eb4bb7..58f7aca 100644 --- a/Components/ButtonBase.qml +++ b/Components/ButtonBase.qml @@ -14,7 +14,7 @@ CustomRect { property color activeOnColor property bool checked property real checkedRadius: Appearance.rounding.medium - property real defaultRadius: Appearance.rounding.large + property real defaultRadius: Math.min(width, height) / 2 property color disabledColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1) property color disabledOnColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38) property bool fillWidth @@ -35,7 +35,7 @@ CustomRect { readonly property color onColor: !enabled ? disabledOnColor : internalChecked ? activeOnColor : inactiveOnColor property real padding readonly property alias pressed: stateLayer.pressed - property real pressedRadius: Appearance.rounding.small + property real pressedRadius: Appearance.rounding.smallest readonly property alias radiusAnim: radiusAnim property bool radiusMorph: true property alias shapeMorph: stateLayer.shapeMorph diff --git a/Components/CircularIndicator.qml b/Components/CircularIndicator.qml index 60466e5..54bc5bd 100644 --- a/Components/CircularIndicator.qml +++ b/Components/CircularIndicator.qml @@ -1,7 +1,7 @@ -import qs.Config -import ZShell.Internal import QtQuick import QtQuick.Templates +import ZShell.Internal +import qs.Config BusyIndicator { id: root @@ -19,10 +19,10 @@ BusyIndicator { property int animState property color bgColor: DynamicColors.palette.m3secondaryContainer property color fgColor: DynamicColors.palette.m3primary - property real implicitSize: Appearance.font.size.normal * 3 + property real implicitSize: Appearance.font.size.medium * 3 property real internalStrokeWidth: strokeWidth readonly property alias progress: manager.progress - property real strokeWidth: Appearance.padding.small * 0.8 + property real strokeWidth: Appearance.padding.extraSmall property alias type: manager.indeterminateAnimationType implicitHeight: implicitSize @@ -33,6 +33,7 @@ BusyIndicator { anchors.fill: parent bgColor: root.bgColor fgColor: root.fgColor + hasEndIndicator: false padding: root.padding rotation: manager.rotation startAngle: manager.startFraction * 360 @@ -52,6 +53,7 @@ BusyIndicator { Anim { duration: manager.completeEndDuration * Appearance.anim.durations.scale properties: "opacity,internalStrokeWidth" + type: Anim.DefaultEffects } } diff --git a/Components/CustomTextField.qml b/Components/CustomTextField.qml index ae4f052..023f265 100644 --- a/Components/CustomTextField.qml +++ b/Components/CustomTextField.qml @@ -22,6 +22,7 @@ TextFieldBase { property string leadingIcon readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0 property int radius: Appearance.rounding.small + property alias sLayer: stateLayer readonly property real smallFontScale: smallFontSize / font.pointSize property int smallFontSize: Appearance.font.size.small property string supportingText @@ -139,11 +140,12 @@ TextFieldBase { active: root.leadingIcon anchors.left: parent.left - anchors.leftMargin: Appearance.padding.normal + anchors.leftMargin: Appearance.padding.larger anchors.verticalCenter: parent.verticalCenter sourceComponent: MaterialIcon { color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.larger text: root.leadingIcon } } diff --git a/Config/DockConfig.qml b/Config/DockConfig.qml index eeaf1de..6b364f2 100644 --- a/Config/DockConfig.qml +++ b/Config/DockConfig.qml @@ -1,7 +1,7 @@ import Quickshell.Io JsonObject { - property bool enable: false + property bool enabled: false property real height: 60 property bool hoverToReveal: true property list ignoredAppRegexes: [] diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index c72f55f..f9d591d 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -216,7 +216,7 @@ Item { } } - if (Config.dock.enable && Config.dock.hoverToReveal && !root.visibilities.dock && !root.visibilities.launcher && root.inBottomPanel(root.panels.dock, x, y)) + if (Config.dock.enabled && Config.dock.hoverToReveal && !root.visibilities.dock && !root.visibilities.launcher && root.inBottomPanel(root.panels.dock, x, y)) root.visibilities.dock = true; if (y < root.bar.implicitHeight) diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index 23f9028..3993c72 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -8,6 +8,7 @@ import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland import ZShell.Blobs +import qs.Modules.SysTray.Popouts.Network as N import qs.Daemons import qs.Components import qs.Modules.Bar @@ -128,7 +129,17 @@ CustomWindow { HyprlandFocusGrab { id: focusGrab - active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!Config.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1)) + active: { + const v = visibilities; + const conf = Config; + if (!N.PopupManager.closed && panels.popouts.hasCurrent) + return true; + if ((v.launcher && conf.launcher.enabled) || (v.sidebar && conf.sidebar.enabled) || (v.resources && conf.dashboard.performance.enabled) || (v.dashboard && conf.dashboard.enabled) || v.settings || (v.clipboard && conf.clipboard.enabled) || (v.dock && conf.dock.enabled)) + return true; + if (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!conf.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1)) + return true; + return false; + } windows: [root] onCleared: { diff --git a/Helpers/Network.qml b/Helpers/Network.qml index a73c00d..a8e56bb 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -9,8 +9,10 @@ Singleton { property bool active: false readonly property list connectedNetworks: networks.filter(n => n.connected) + property Network connectingNetwork readonly property list devices: Networking.devices.values - readonly property list knownNetworks: networks.filter(n => n.known && !n.known.ConnectionState.Connected) + property Network failedNetwork + readonly property list knownNetworks: networks.filter(n => n.known && !n.connected) readonly property list networks: { const list = []; for (const d of wifiDevices) { @@ -26,10 +28,21 @@ Singleton { readonly property list wifiDevices: devices.filter(d => wifiDevice(d)) readonly property bool wifiEnabled: Networking.wifiEnabled + signal networkConnected + function isSecure(security): bool { return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae); } + function requestConnect(network: Network, secret = undefined): void { + connectingNetwork = network; + if (secret === undefined || secret === "") { + network.connect(); + } else { + (network as WifiNetwork).connectWithPsk(secret); + } + } + function setScan(value: bool): void { for (const d of wifiDevices) { d.scannerEnabled = value; @@ -48,4 +61,54 @@ Singleton { for (const d of wifiDevices) d.scannerEnabled = active; } + + Timer { + id: flushFailed + + interval: 5000 + running: root.failedNetwork + + onTriggered: { + root.failedNetwork = null; + } + } + + Timer { + id: checkConnection + + interval: 500 + repeat: true + running: root.connectingNetwork + + onTriggered: { + var completedNetwork; + + switch (root.connectingNetwork.state) { + case ConnectionState.Connecting: + break; + case ConnectionState.Connected: + completedNetwork = root.connectingNetwork; + root.networkConnected(); + break; + case ConnectionState.Disconnected: + root.failedNetwork = root.connectingNetwork; + completedNetwork = root.connectingNetwork; + break; + } + + if (completedNetwork) { + root.connectingNetwork = null; + } + } + } + + Timer { + interval: 10000 + running: checkConnection.running + + onTriggered: { + root.failedNetwork = root.connectingNetwork; + root.connectingNetwork = null; + } + } } diff --git a/Modules/Dock/Wrapper.qml b/Modules/Dock/Wrapper.qml index d4d5aa3..24f530a 100644 --- a/Modules/Dock/Wrapper.qml +++ b/Modules/Dock/Wrapper.qml @@ -12,7 +12,7 @@ Item { property real offsetScale: shouldBeActive ? 0 : 1 required property var panels required property ShellScreen screen - readonly property bool shouldBeActive: visibilities.dock && Config.dock.enable + readonly property bool shouldBeActive: visibilities.dock && Config.dock.enabled required property PersistentProperties visibilities anchors.bottomMargin: (-implicitHeight - 5) * offsetScale diff --git a/Modules/SysTray/Popouts/Network/NetworkOverlay.qml b/Modules/SysTray/Popouts/Network/NetworkOverlay.qml new file mode 100644 index 0000000..29eca7a --- /dev/null +++ b/Modules/SysTray/Popouts/Network/NetworkOverlay.qml @@ -0,0 +1,70 @@ +import QtQuick +import qs.Helpers +import qs.Config +import qs.Components + +CustomRect { + id: root + + readonly property bool closing: PopupManager.closed + readonly property url currentPopup: PopupManager.currentPopup + property bool shouldBeVisible: loader.status === Loader.Ready + + color: Qt.alpha(DynamicColors.palette.m3shadow, 0.3) + opacity: shouldBeVisible && !closing ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + Anim { + } + } + + onVisibleChanged: { + if (!visible) { + PopupManager.requestClose(); + PopupManager.currentPopup = ""; + } + } + + CustomMouseArea { + anchors.fill: parent + hoverEnabled: true + preventStealing: true + propagateComposedEvents: false + + onClicked: { + const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x; + const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y; + if (insideItemHeight && insideItemWidth) + return; + + PopupManager.requestClose(); + } + } + + Loader { + id: loader + + anchors.centerIn: parent + + Connections { + function onCurrentPopupChanged(): void { + if (PopupManager.currentPopup) { + loader.setSource(PopupManager.currentPopup, PopupManager.currentPopupProps); + } else { + loader.source = null; + } + } + + target: PopupManager + } + } + + Connections { + function onNetworkConnected(): void { + PopupManager.requestClose(); + } + + target: Network + } +} diff --git a/Modules/SysTray/Popouts/Network/PSKOverlay.qml b/Modules/SysTray/Popouts/Network/PSKOverlay.qml new file mode 100644 index 0000000..b8f2fdc --- /dev/null +++ b/Modules/SysTray/Popouts/Network/PSKOverlay.qml @@ -0,0 +1,102 @@ +import Quickshell +import QtQuick +import QtQuick.Layouts +import ZShell.Components +import qs.Config +import qs.Components +import qs.Helpers + +CustomClippingRect { + id: root + + required property var network + + color: DynamicColors.palette.m3surfaceContainer + implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 + implicitWidth: layout.implicitWidth + layout.anchors.margins * 2 + radius: Appearance.rounding.large + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: Appearance.padding.extraLarge + spacing: Appearance.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + font.pointSize: Appearance.font.size.extraLarge * 2 + text: "lock" + } + + CustomText { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3onSurfaceVariant + text: qsTr("Connect to %1").arg(root.network.name) + } + + CustomTextField { + id: passwordField + + Layout.fillWidth: true + Layout.preferredWidth: 400 + echoMode: CustomTextField.Password + errorText: qsTr("Failed to connect with given password") + isError: Network.failedNetwork + leadingIcon: "lock" + passwordMaskDelay: 5000 + placeholderText: qsTr("Input password") + } + + ButtonRow { + id: buttonRow + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + IconTextButton { + fillWidth: true + font.pointSize: Appearance.font.size.normal + icon: "close" + inactiveColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) + inactiveOnColor: DynamicColors.palette.m3onSurfaceVariant + isRound: true + isToggle: false + shapeMorph: true + text: "Cancel" + + onClicked: PopupManager.requestClose() + } + + IconTextButton { + fillWidth: true + font.pointSize: Appearance.font.size.normal + icon: Network.connectingNetwork ? "" : "check" + inactiveColor: Network.connectingNetwork ? DynamicColors.palette.m3primaryContainer : Network.failedNetwork ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary + inactiveOnColor: Network.connectingNetwork ? DynamicColors.palette.m3onPrimaryContainer : Network.failedNetwork ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onPrimary + isRound: true + isToggle: false + shapeMorph: true + text: Network.connectingNetwork ? "" : "Apply" + + onClicked: Network.requestConnect(root.network, passwordField.text) + + Loader { + active: opacity > 0 + anchors.centerIn: parent + opacity: Network.connectingNetwork ? 1 : 0 + + Behavior on opacity { + Anim { + } + } + sourceComponent: CircularIndicator { + bgColor: "transparent" + implicitSize: buttonRow.height - Appearance.padding.normal + running: true + } + } + } + } + } +} diff --git a/Modules/SysTray/Popouts/Network/PopupManager.qml b/Modules/SysTray/Popouts/Network/PopupManager.qml new file mode 100644 index 0000000..e0e5834 --- /dev/null +++ b/Modules/SysTray/Popouts/Network/PopupManager.qml @@ -0,0 +1,22 @@ +pragma Singleton + +import Quickshell +import QtQuick + +Singleton { + id: root + + property bool closed: true + property url currentPopup: "" + property var currentPopupProps: ({}) + + function requestClose(): void { + closed = true; + } + + function requestOpen(source: url, properties = {}): void { + currentPopupProps = properties; + currentPopup = source; + closed = false; + } +} diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index 9ff2f5d..82549bc 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import qs.Modules.SysTray.Popouts.Network import qs.Components import qs.Config import qs.Helpers @@ -13,7 +14,6 @@ CustomClippingRect { required property var wrapper anchors.horizontalCenter: parent.horizontalCenter - color: DynamicColors.tPalette.m3surfaceContainer implicitHeight: networkPopContent.height + networks.implicitHeight + networkPopContent.anchors.margins + networks.anchors.margins * 2 implicitWidth: 500 + 8 * 2 radius: (20 - Appearance.padding.small) * Appearance.rounding.scale @@ -28,6 +28,7 @@ CustomClippingRect { anchors.margins: Appearance.padding.large anchors.right: parent.right anchors.top: parent.top + spacing: Appearance.spacing.extraSmall / 2 CustomText { Layout.preferredHeight: visible ? implicitHeight : 0 @@ -39,28 +40,17 @@ CustomClippingRect { Toggle { Layout.preferredHeight: visible ? implicitHeight : 0 checked: Network.wifiEnabled - label: qsTr("WiFi enabled") + first: true + last: connected.count === 0 + subtext: qsTr("Toggle WiFi on or off") + text: qsTr("Set WiFi") - toggle.onToggled: Network.setWifi(checked) - } - } - - ColumnLayout { - id: networks - - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.margins: Appearance.padding.normal - anchors.right: parent.right - anchors.top: networkPopContent.bottom - - CustomText { - Layout.leftMargin: Appearance.padding.normal - text: qsTr("Connected") - visible: Network.connectedNetworks.length > 0 + onToggled: Network.setWifi(checked) } Repeater { + id: connected + visible: Network.connectedNetworks.length > 0 model: ScriptModel { @@ -70,20 +60,40 @@ CustomClippingRect { } NetworkItem { + first: false + known: true + repeater: connected } } + } + + ColumnLayout { + id: networks + + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.right: parent.right + anchors.top: networkPopContent.bottom + spacing: Appearance.spacing.extraSmall / 2 Spacer { + repeater: known visible: Network.connectedNetworks.length > 0 } CustomText { Layout.leftMargin: Appearance.padding.normal - text: qsTr("Known networks") + Layout.preferredHeight: visible ? implicitHeight : 0 + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + text: Network.knownNetworks.length > 1 ? qsTr("%1 known networks available").arg(Network.knownNetworks.length) : qsTr("1 known network available") // qmllint disable missing-property visible: Network.knownNetworks.length > 0 } Repeater { + id: known + visible: Network.knownNetworks.length > 0 model: ScriptModel { @@ -93,28 +103,27 @@ CustomClippingRect { } NetworkItem { + known: true + repeater: known } } Spacer { + repeater: unknown visible: Network.knownNetworks.length > 0 } - CustomText { - Layout.leftMargin: Appearance.padding.normal - text: qsTr("Networks") - } - CustomText { Layout.leftMargin: Appearance.padding.normal Layout.preferredHeight: visible ? implicitHeight : 0 color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.extraSmall - text: qsTr("%1 networks available").arg(Network.networks.length) // qmllint disable missing-property + font.pointSize: Appearance.font.size.small + text: Network.unknownNetworks.length > 1 ? qsTr("%1 networks available").arg(Network.unknownNetworks.length) : qsTr("1 network available") // qmllint disable missing-property + visible: Network.unknownNetworks.length > 0 } Repeater { - id: networkRepeater + id: unknown model: ScriptModel { values: [...Network.unknownNetworks].sort((a, b) => { @@ -123,101 +132,227 @@ CustomClippingRect { } NetworkItem { + known: false + repeater: unknown } } } - component NetworkItem: CustomRect { - id: knownNetworkItem + NetworkOverlay { + anchors.fill: parent + } + 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 NetworkItem: ConnectedRect { + id: networkItem + + readonly property bool connecting: Network.connectingNetwork === modelData + property int horizontalPadding: Appearance.padding.largeIncreased + required property int index + readonly property bool isSecure: Network.isSecure(modelData.security) + required property bool known required property var modelData + required property Repeater repeater Layout.fillWidth: true - Layout.preferredHeight: visible ? knownNetworkRow.implicitHeight + Appearance.padding.smaller * 2 : 0 - Layout.rightMargin: Appearance.padding.extraSmall + Layout.preferredHeight: visible ? rowLayout.implicitHeight + Appearance.padding.smaller * 2 : 0 + first: index === 0 + last: index === (repeater.count - 1) radius: Appearance.rounding.small RowLayout { - id: knownNetworkRow + id: rowLayout anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.normal - opacity: 0 - scale: 0.7 - spacing: Appearance.spacing.small + anchors.leftMargin: networkItem.horizontalPadding + anchors.rightMargin: networkItem.horizontalPadding - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } - Behavior on scale { - Anim { - } - } + Column { + id: column - Component.onCompleted: { - opacity = 1; - scale = 1; - } - - MaterialIcon { - color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant - text: Icons.getNetworkIcon(knownNetworkItem.modelData.signalStrength * 100, Network.isSecure(knownNetworkItem.modelData.security)) - } - - CustomText { Layout.fillWidth: true - Layout.leftMargin: Appearance.spacing.extraSmall - Layout.rightMargin: Appearance.spacing.extraSmall - color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface - elide: Text.ElideRight - text: knownNetworkItem.modelData.name + + CustomText { + Layout.fillWidth: true + Layout.leftMargin: Appearance.spacing.extraSmall + Layout.rightMargin: Appearance.spacing.extraSmall + color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface + elide: Text.ElideRight + text: networkItem.modelData.name + } + + 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: qsTr("%1 strength").arg((networkItem.modelData.signalStrength * 100) + "%") + } } - CustomRect { - color: Qt.alpha(DynamicColors.palette.m3primary, knownNetworkItem.modelData.active ? 1 : 0) - implicitHeight: knownWirelessConnectIcon.implicitHeight + Appearance.padding.extraSmall - implicitWidth: implicitHeight - radius: Appearance.rounding.full + Item { + Layout.alignment: Qt.AlignRight + implicitHeight: icon.height + implicitWidth: icon.width - // CircularIndicator { - // anchors.fill: parent - // running: knownNetworkItem.loading - // } + MaterialIcon { + id: icon + + color: networkItem.modelData.active ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + opacity: networkItem.connecting ? 0 : 1 + text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure) + + Behavior on opacity { + Anim { + } + } + } + + Loader { + active: opacity > 0 + anchors.fill: parent + opacity: networkItem.connecting ? 1 : 0 + + Behavior on opacity { + Anim { + } + } + sourceComponent: CircularIndicator { + bgColor: "transparent" + running: true + } + } } } StateLayer { - color: knownNetworkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + color: networkItem.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface onClicked: { - NetworkPassword.requestOpen(); + if (networkItem.isSecure && !networkItem.known) { + PopupManager.requestOpen(Qt.resolvedUrl("Network/PSKOverlay.qml"), { + network: networkItem.modelData + }); + } else { + Network.requestConnect(networkItem.modelData); + } } } + + 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: networkItem.known + + onClicked: networkItem.modelData.forget() + } } component Spacer: Item { id: spacer - Layout.preferredHeight: networkRepeater.count > 0 ? Appearance.spacing.extraSmall : 0 + required property Repeater repeater + + Layout.preferredHeight: repeater.count > 0 ? Appearance.spacing.extraSmall : 0 } - component Toggle: RowLayout { - property alias checked: toggle.checked - required property string label - property alias toggle: toggle + 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 - Layout.rightMargin: Appearance.padding.extraSmall - spacing: Appearance.spacing.small + 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 - CustomText { - Layout.fillWidth: true - text: parent.label + 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 + } + } } - CustomSwitch { - id: toggle - } + onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY) } } diff --git a/network-dev/wifi-sim.sh b/network-dev/wifi-sim.sh index f3b2cea..58512c4 100755 --- a/network-dev/wifi-sim.sh +++ b/network-dev/wifi-sim.sh @@ -1,16 +1,29 @@ #!/bin/bash + # ============================================================================= + # wifi-sim.sh — Virtual WiFi environment for development & testing + # -# Creates 3 dummy networks using mac80211_hwsim + hostapd: -# - FreeWifi (open, strong signal, ch 1) -# - OfficeNetwork (open, medium signal, ch 6) -# - HomeNetwork (WPA2, weak signal, ch 11) + +# Creates 3 dummy networks using mac80211_hwsim + hostapd + dnsmasq: + +# - FreeWifi (open, strong signal, ch 1) + +# - OfficeNetwork (open, medium signal, ch 6) + +# - HomeNetwork (WPA2, weak signal, ch 11) + # + # Usage: -# sudo ./wifi-sim.sh start -# sudo ./wifi-sim.sh stop -# sudo ./wifi-sim.sh status + +# sudo ./wifi-sim.sh start + +# sudo ./wifi-sim.sh stop + +# sudo ./wifi-sim.sh status + # ============================================================================= set -euo pipefail @@ -23,11 +36,27 @@ SSID_SECURE="HomeNetwork" # WPA2 network, weak signal WIFI_PASSWORD="testpassword123" # TX power levels in mBm (millibel-milliwatts). Higher = stronger signal. + # These affect the RSSI reported to scanning clients. + TXPOWER_STRONG=3000 # 30 dBm TXPOWER_MEDIUM=1500 # 15 dBm TXPOWER_WEAK=100 # 1 dBm +# Per-network IP plan for DHCP. + +AP1_IP="10.10.1.1" +AP1_RANGE_START="10.10.1.50" +AP1_RANGE_END="10.10.1.150" + +AP2_IP="10.10.2.1" +AP2_RANGE_START="10.10.2.50" +AP2_RANGE_END="10.10.2.150" + +AP3_IP="10.10.3.1" +AP3_RANGE_START="10.10.3.50" +AP3_RANGE_END="10.10.3.150" + RUN_DIR="/tmp/wifi-sim" # --- Helpers ----------------------------------------------------------------- @@ -36,8 +65,14 @@ die() { echo "ERROR: $*" >&2 exit 1 } -info() { echo "[*] $*"; } -ok() { echo "[+] $*"; } + +info() { + echo "[*] $*" +} + +ok() { + echo "[+] $*" +} require_cmds() { local missing=() @@ -46,7 +81,7 @@ require_cmds() { done if [[ ${#missing[@]} -gt 0 ]]; then die "Missing required tools: ${missing[*]} - Install with: sudo apt install hostapd wpasupplicant iw" +Install with: sudo apt install hostapd dnsmasq wpasupplicant iw" fi } @@ -54,11 +89,60 @@ get_phy_for_iface() { iw dev "$1" info | awk '/wiphy/{print "phy"$2}' } +start_dnsmasq() { + local name="$1" + local iface="$2" + local gw_ip="$3" + local dhcp_start="$4" + local dhcp_end="$5" + + dnsmasq \ + --interface="$iface" \ + --bind-interfaces \ + --except-interface=lo \ + --port=0 \ + --no-resolv \ + --no-hosts \ + --dhcp-authoritative \ + --dhcp-range="${dhcp_start},${dhcp_end},255.255.255.0,12h" \ + --dhcp-option=3,"$gw_ip" \ + --dhcp-option=6,"$gw_ip" \ + --pid-file="$RUN_DIR/${name}.dnsmasq.pid" \ + --dhcp-leasefile="$RUN_DIR/${name}.leases" \ + >"$RUN_DIR/${name}.dnsmasq.log" 2>&1 || + { + cat "$RUN_DIR/${name}.dnsmasq.log" + die "dnsmasq failed for $name. See log above." + } + +} + +stop_pidfile() { + local pidfile="$1" + local label="$2" + + if [[ -f "$pidfile" ]]; then + local pid + pid="$(cat "$pidfile")" + if kill "$pid" 2>/dev/null; then + info "Stopped $label (PID $pid)" + fi + rm -f "$pidfile" + fi + +} + +remove_iface_addr() { + local iface="$1" + local cidr="$2" + ip addr del "$cidr" dev "$iface" 2>/dev/null || true +} + # --- Start ------------------------------------------------------------------- cmd_start() { [[ $EUID -ne 0 ]] && die "Please run as root: sudo $0 start" - require_cmds hostapd iw ip + require_cmds hostapd dnsmasq iw ip if [[ -d "$RUN_DIR" && -f "$RUN_DIR/interfaces" ]]; then die "wifi-sim appears to already be running. Run 'sudo $0 stop' first." @@ -75,7 +159,7 @@ cmd_start() { die "mac80211_hwsim is already loaded. Run 'sudo $0 stop' or 'sudo rmmod mac80211_hwsim' first." fi modprobe mac80211_hwsim radios=4 - sleep 1 # Give the kernel a moment to register all interfaces + sleep 1 # Find the interfaces created by our modprobe AFTER=($(iw dev 2>/dev/null | awk '/Interface/{print $2}' | grep -v '^hwsim')) @@ -97,30 +181,31 @@ cmd_start() { info "Interfaces assigned: AP1=$AP1 AP2=$AP2 AP3=$AP3 Client=$CLIENT" echo "$AP1 $AP2 $AP3 $CLIENT" >"$RUN_DIR/interfaces" - # Prevent NetworkManager from fighting us over these interfaces + # Prevent NetworkManager from fighting us over the AP interfaces. + # Leave the client managed so your app can use NetworkManager normally. if command -v nmcli &>/dev/null && nmcli general status &>/dev/null 2>&1; then - info "Marking interfaces as unmanaged in NetworkManager..." + info "Marking AP interfaces as unmanaged in NetworkManager..." for iface in "$AP1" "$AP2" "$AP3"; do nmcli device set "$iface" managed no 2>/dev/null || true done - nmcli device set "$CLIENT" managed yes + nmcli device set "$CLIENT" managed yes 2>/dev/null || true fi # Set TX power on each physical radio to simulate signal strength differences. # mac80211_hwsim factors TX power into the RSSI reported to scanning clients. - # Note: for precise RSSI control, consider 'wmediumd' (a wireless medium daemon). info "Setting TX power levels for signal strength simulation..." PHY1=$(get_phy_for_iface "$AP1") PHY2=$(get_phy_for_iface "$AP2") PHY3=$(get_phy_for_iface "$AP3") - iw phy "$PHY1" set txpower fixed $TXPOWER_STRONG - iw phy "$PHY2" set txpower fixed $TXPOWER_MEDIUM - iw phy "$PHY3" set txpower fixed $TXPOWER_WEAK + iw phy "$PHY1" set txpower fixed "$TXPOWER_STRONG" + iw phy "$PHY2" set txpower fixed "$TXPOWER_MEDIUM" + iw phy "$PHY3" set txpower fixed "$TXPOWER_WEAK" # --- hostapd config: AP1 (open, strong, channel 1) ----------------------- cat >"$RUN_DIR/ap1.conf" <"$RUN_DIR/ap2.conf" <"$RUN_DIR/ap3.conf" < $AP1_IP/24" + echo " $SSID_MEDIUM -> $AP2_IP/24" + echo " $SSID_SECURE -> $AP3_IP/24" + echo "" echo " Useful commands:" echo " Scan for networks : sudo iw dev $CLIENT scan | grep -E 'SSID|signal'" echo " Connect (open) : sudo iw dev $CLIENT connect \"$SSID_STRONG\"" - echo " Connect (WPA2) : sudo wpa_supplicant -Dnl80211 -i$CLIENT \\" - echo " -c <(wpa_passphrase \"$SSID_SECURE\" \"$WIFI_PASSWORD\")" + echo " Connect (WPA2) : sudo nmcli device wifi connect \"$SSID_SECURE\" password \"$WIFI_PASSWORD\" ifname \"$CLIENT\"" echo " Stop simulation : sudo $0 stop" echo "" - echo " Logs: $RUN_DIR/ap{1,2,3}.log" + echo " Logs: $RUN_DIR/ap{1,2,3}.log and $RUN_DIR/ap{1,2,3}.dnsmasq.log" + } # --- Stop -------------------------------------------------------------------- @@ -218,6 +324,19 @@ cmd_stop() { info "Stopping virtual WiFi environment..." local had_something=false + # Stop dnsmasq instances first. + for ap in ap1 ap2 ap3; do + PID_FILE="$RUN_DIR/$ap.dnsmasq.pid" + if [[ -f "$PID_FILE" ]]; then + PID=$(cat "$PID_FILE") + if kill "$PID" 2>/dev/null; then + info "Stopped dnsmasq for $ap (PID $PID)" + fi + rm -f "$PID_FILE" + had_something=true + fi + done + # Kill hostapd processes via PID files for ap in ap1 ap2 ap3; do PID_FILE="$RUN_DIR/$ap.pid" @@ -231,9 +350,14 @@ cmd_stop() { fi done - # Re-enable NetworkManager management for these interfaces + # Restore addresses if we still know the interface names. if [[ -f "$RUN_DIR/interfaces" ]]; then read -r AP1 AP2 AP3 CLIENT <"$RUN_DIR/interfaces" + + remove_iface_addr "$AP1" "$AP1_IP/24" + remove_iface_addr "$AP2" "$AP2_IP/24" + remove_iface_addr "$AP3" "$AP3_IP/24" + if command -v nmcli &>/dev/null; then for iface in "$AP1" "$AP2" "$AP3" "$CLIENT"; do nmcli device set "$iface" managed yes 2>/dev/null || true @@ -255,6 +379,7 @@ cmd_stop() { else echo "Nothing was running." fi + } # --- Status ------------------------------------------------------------------ @@ -286,6 +411,16 @@ cmd_status() { echo " hostapd ($ap) PID=$PID DEAD (check $RUN_DIR/$ap.log)" fi fi + + DNSMASQ_PID_FILE="$RUN_DIR/$ap.dnsmasq.pid" + if [[ -f "$DNSMASQ_PID_FILE" ]]; then + PID=$(cat "$DNSMASQ_PID_FILE") + if kill -0 "$PID" 2>/dev/null; then + echo " dnsmasq ($ap) PID=$PID running" + else + echo " dnsmasq ($ap) PID=$PID DEAD (check $RUN_DIR/$ap.dnsmasq.log)" + fi + fi done } @@ -298,7 +433,7 @@ status) cmd_status ;; *) echo "Usage: sudo $0 {start|stop|status}" echo "" - echo " start Load mac80211_hwsim and bring up 3 virtual APs" + echo " start Load mac80211_hwsim and bring up 3 virtual APs with DHCP" echo " stop Tear down all virtual interfaces and unload module" echo " status Show current state of interfaces and processes" echo "" -- 2.47.3 From 33b08523e9d86530d9eb6dac1dced07900193b57 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 10 Jul 2026 16:17:47 +0200 Subject: [PATCH 13/15] add preliminary bluetooth widget + popout --- Helpers/Icons.qml | 22 +- Modules/Content.qml | 7 + Modules/SysTray/Popouts/BluetoothPopout.qml | 306 ++++++++++++++++++++ Modules/SysTray/Popouts/NetworkPopout.qml | 12 +- Modules/SysTray/TrayWidget.qml | 9 + Modules/SysTray/Widgets/BluetoothWidget.qml | 13 + Modules/SysTray/Widgets/NetworkWidget.qml | 25 +- 7 files changed, 356 insertions(+), 38 deletions(-) create mode 100644 Modules/SysTray/Popouts/BluetoothPopout.qml create mode 100644 Modules/SysTray/Widgets/BluetoothWidget.qml 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" } -- 2.47.3 From 954dc10fbbd22f5f8e27c8aa841c7f509dd5810b Mon Sep 17 00:00:00 2001 From: zach Date: Sat, 11 Jul 2026 14:00:08 +0200 Subject: [PATCH 14/15] bluetooth model delegate animations --- Modules/SysTray/Popouts/BluetoothPopout.qml | 205 ++++++++++++++------ Modules/SysTray/Popouts/NetworkPopout.qml | 13 -- 2 files changed, 143 insertions(+), 75 deletions(-) diff --git a/Modules/SysTray/Popouts/BluetoothPopout.qml b/Modules/SysTray/Popouts/BluetoothPopout.qml index 4547f32..bbea15d 100644 --- a/Modules/SysTray/Popouts/BluetoothPopout.qml +++ b/Modules/SysTray/Popouts/BluetoothPopout.qml @@ -2,8 +2,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import QtQml.Models import Quickshell import Quickshell.Bluetooth +import ZShell.Components import qs.Components import qs.Helpers import qs.Config @@ -24,7 +26,7 @@ CustomClippingRect { CustomText { Layout.rightMargin: Appearance.padding.extraSmall Layout.topMargin: Appearance.padding.normal - font.pointSize: Appearance.font.size.medium + font.pointSize: Appearance.font.size.large text: qsTr("Bluetooth") } @@ -43,6 +45,7 @@ CustomClippingRect { Toggle { checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type + last: rows.sortedConnected.length === 0 subtext: qsTr("Enable scanning for new devices") text: qsTr("Discover") @@ -53,43 +56,83 @@ CustomClippingRect { } } - Repeater { - id: connected + ListView { + id: rows + readonly property var sortedConnected: { + const byName = (a, b) => a.name.localeCompare(b.name); + return [...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5); + } + + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + interactive: false + section.property: "connected" + spacing: Appearance.spacing.extraSmall / 2 + + add: Transition { + Anim { + from: 0 + property: "opacity" + to: 1 + } + } + delegate: DelegateChooser { + role: "isDivider" + + DelegateChoice { + roleValue: true + + delegate: StatusDivider { + } + } + + DelegateChoice { + delegate: BluetoothItem { + list: rows + width: rows.width + } + } + } + displaced: Transition { + Anim { + easing.type: Easing.OutCubic + property: "y" + } + } 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 + objectProp: "address" + values: { + const byName = (a, b) => a.name.localeCompare(b.name); + return [...[...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5), + { + isDivider: true + }, + ...[...Bluetooth.devices.values].filter(d => !d.connected).sort(byName).slice(0, 5)]; + } } - - BluetoothItem { - first: false - repeater: connected + move: Transition { + Anim { + easing.type: Easing.OutCubic + property: "y" + } } - } - - 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; + remove: Transition { + Anim { + property: "opacity" + to: 0 + } } - } + section.delegate: Rectangle { + required property string section - Repeater { - id: disconnected + color: "transparent" + height: childrenRect.height + width: ListView.view.width - 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 + CustomText { + text: parent.section + } } } } @@ -97,21 +140,35 @@ CustomClippingRect { component BluetoothItem: ConnectedRect { id: btItem + readonly property int connectedCount: rows.sortedConnected.length readonly property bool connecting: Network.connectingNetwork === modelData property int horizontalPadding: Appearance.padding.largeIncreased + readonly property bool inConnectedGroup: index < connectedCount required property int index + required property ListView list readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type required property BluetoothDevice modelData - required property Repeater repeater + property int verticalPadding: Appearance.padding.smaller - Layout.fillWidth: true - Layout.preferredHeight: visible ? rowLayout.implicitHeight + Appearance.padding.smaller * 2 : 0 - first: index === 0 - last: index === (repeater.count - 1) + first: !inConnectedGroup && index === connectedCount + 1 + implicitHeight: rowLayout.implicitHeight + verticalPadding * 2 + last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1 radius: Appearance.rounding.small + Timer { + interval: 50 + running: btItem.ListView.delayRemove + + onTriggered: btItem.ListView.delayRemove = false + } + Connections { + function onConnectedChanged(): void { + if (!btItem.modelData.connected) + btItem.ListView.delayRemove = true; + } + function onPairedChanged(): void { if (btItem.modelData.paired) btItem.modelData.connect(); @@ -148,7 +205,7 @@ CustomClippingRect { 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) + "%") + text: btItem.modelData.connected ? (btItem.modelData.batteryAvailable ? qsTr("%1 battery left").arg(Math.round(btItem.modelData.battery * 100) + "%") : qsTr("Battery status unknown")) : btItem.modelData.address } } @@ -162,7 +219,7 @@ CustomClippingRect { font.pointSize: Appearance.font.size.large opacity: btItem.connecting ? 0 : 1 - text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_alert" + text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_unknown" visible: btItem.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type Behavior on opacity { @@ -186,33 +243,28 @@ CustomClippingRect { } } } - } - 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(); + ButtonRow { + spacing: Appearance.spacing.small + + IconTextButton { + font.pointSize: Appearance.font.size.small + icon: btItem.modelData.connected ? "link_off" : btItem.modelData.paired ? "link" : "add_link" + text: btItem.modelData.connected ? qsTr("Disconnect") : btItem.modelData.paired ? qsTr("Connect") : qsTr("Pair") + + onClicked: { + if (btItem.modelData.connected) + btItem.modelData.disconnect(); + else if (btItem.modelData.paired) + btItem.modelData.connect(); + else if (!btItem.modelData.paired) + btItem.modelData.pair(); + else if (btItem.modelData.pairing) + btItem.modelData.cancelPair(); + } + } } } - - 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 @@ -225,6 +277,35 @@ CustomClippingRect { color: DynamicColors.tPalette.m3surfaceContainer topLeftRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall topRightRadius: first ? Appearance.rounding.large : Appearance.rounding.extraSmall + + Behavior on bottomLeftRadius { + Anim { + } + } + Behavior on bottomRightRadius { + Anim { + } + } + Behavior on topLeftRadius { + Anim { + } + } + Behavior on topRightRadius { + Anim { + } + } + } + component StatusDivider: CustomText { + bottomPadding: Appearance.spacing.extraSmall + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.smaller + text: { + const devices = Bluetooth.devices.values; // qmllint disable unresolved-type + const connectedCount = devices.filter(d => d.connected).length; + let available = qsTr("%1 device%2 available").arg(devices.length - connectedCount).arg(devices.length === 1 ? "" : "s"); + return available; + } + topPadding: Appearance.spacing.small } component Toggle: CustomSwitch { id: toggleItem diff --git a/Modules/SysTray/Popouts/NetworkPopout.qml b/Modules/SysTray/Popouts/NetworkPopout.qml index c8b0463..3904f71 100644 --- a/Modules/SysTray/Popouts/NetworkPopout.qml +++ b/Modules/SysTray/Popouts/NetworkPopout.qml @@ -253,19 +253,6 @@ CustomClippingRect { } } } - - 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: networkItem.known - - onClicked: networkItem.modelData.forget() - } } component Spacer: Item { id: spacer -- 2.47.3 From 5a8939bc1e4cc859d576ab785a5afba0cdd25bbd Mon Sep 17 00:00:00 2001 From: inorishio Date: Mon, 17 Aug 2026 22:04:49 +0200 Subject: [PATCH 15/15] changed the repetaer for the networkpopout to be a listview, changed some variables in the bluetooth popout --- Modules/Bar/Popouts/BluetoothPopout.qml | 11 +- Modules/Bar/Popouts/NetworkPopout.qml | 200 +++++++++++++----------- 2 files changed, 114 insertions(+), 97 deletions(-) diff --git a/Modules/Bar/Popouts/BluetoothPopout.qml b/Modules/Bar/Popouts/BluetoothPopout.qml index d82bee1..6680bb4 100644 --- a/Modules/Bar/Popouts/BluetoothPopout.qml +++ b/Modules/Bar/Popouts/BluetoothPopout.qml @@ -14,6 +14,8 @@ import qs.Helpers CustomClippingRect { id: root + readonly property list btConnected: Bluetooth.devices.values.filter(d => d.connected) + implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 implicitWidth: 500 @@ -46,7 +48,7 @@ CustomClippingRect { Toggle { checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type - last: rows.sortedConnected.length === 0 + last: root.btConnected.length === 0 subtext: qsTr("Enable scanning for new devices") text: qsTr("Discover") @@ -60,11 +62,6 @@ CustomClippingRect { ListView { id: rows - readonly property var sortedConnected: { - const byName = (a, b) => a.name.localeCompare(b.name); - return [...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5); - } - Layout.fillWidth: true Layout.preferredHeight: contentHeight interactive: false @@ -141,7 +138,7 @@ CustomClippingRect { component BluetoothItem: ConnectedRect { id: btItem - readonly property int connectedCount: rows.sortedConnected.length + readonly property int connectedCount: root.btConnected.length readonly property bool connecting: Network.connectingNetwork === modelData property int horizontalPadding: Tokens.padding.largeIncreased readonly property bool inConnectedGroup: index < connectedCount diff --git a/Modules/Bar/Popouts/NetworkPopout.qml b/Modules/Bar/Popouts/NetworkPopout.qml index b35b6f5..a005570 100644 --- a/Modules/Bar/Popouts/NetworkPopout.qml +++ b/Modules/Bar/Popouts/NetworkPopout.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import ZShell.Components import ZShell.Config import qs.Services import qs.Modules.Bar.Popouts.Network @@ -15,7 +16,7 @@ CustomClippingRect { required property var wrapper anchors.horizontalCenter: parent.horizontalCenter - implicitHeight: networkPopContent.height + networks.implicitHeight + networkPopContent.anchors.margins + networks.anchors.margins * 2 + implicitHeight: networkPopContent.height + networkPopContent.anchors.margins * 2 implicitWidth: 500 + 8 * 2 radius: (20 - Tokens.padding.small) * Tokens.rounding.scale @@ -42,99 +43,94 @@ CustomClippingRect { Layout.preferredHeight: visible ? implicitHeight : 0 checked: Network.wifiEnabled first: true - last: connected.count === 0 + last: Network.connectedNetworks.length === 0 subtext: qsTr("Toggle WiFi device state") text: checked ? qsTr("Enabled") : qsTr("Disabled") onToggled: Network.setWifi(checked) } - Repeater { - id: connected + ListView { + id: rows - visible: Network.connectedNetworks.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + interactive: false + section.property: "connected" + spacing: Tokens.spacing.extraSmall / 2 + add: Transition { + Anim { + from: 0 + property: "opacity" + to: 1 + } + } + delegate: DelegateChooser { + role: "isDivider" + + DelegateChoice { + roleValue: true + + delegate: StatusDivider { + } + } + + DelegateChoice { + delegate: NetworkItem { + list: rows + width: rows.width + } + } + } + displaced: Transition { + Anim { + easing.type: Easing.OutCubic + property: "y" + } + } model: ScriptModel { - values: [...Network.connectedNetworks].sort((a, b) => { - return b.signalStrength - a.signalStrength; - }) + values: { + const byStrengthName = (a, b) => b.signalStrength - a.signalStrength || a.name.localeCompare(b.name); + const cNetworks = [...Network.connectedNetworks]; + const kNetworks = [...Network.knownNetworks]; + const uNetworks = [...Network.unknownNetworks]; + + return [...cNetworks.sort(byStrengthName), ...(kNetworks.length > 0 ? [ + { + isDivider: true, + type: "known" + }, + ...kNetworks.sort(byStrengthName)] : []), ...(uNetworks.length > 0 ? [ + { + isDivider: true, + type: "unknown" + }, + ...uNetworks.sort(byStrengthName)] : [])]; + } } - - NetworkItem { - first: false - known: true - repeater: connected + move: Transition { + Anim { + easing.type: Easing.OutCubic + property: "y" + } } - } - } - - ColumnLayout { - id: networks - - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.margins: Tokens.padding.large - anchors.right: parent.right - anchors.top: networkPopContent.bottom - spacing: Tokens.spacing.extraSmall / 2 - - Spacer { - repeater: known - visible: Network.connectedNetworks.length > 0 - } - - CustomText { - Layout.leftMargin: Tokens.padding.normal - Layout.preferredHeight: visible ? implicitHeight : 0 - color: Colors.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - text: Network.knownNetworks.length > 1 ? qsTr("%1 known networks available").arg(Network.knownNetworks.length) : qsTr("1 known network available") // qmllint disable missing-property - visible: Network.knownNetworks.length > 0 - } - - Repeater { - id: known - - visible: Network.knownNetworks.length > 0 - - model: ScriptModel { - values: [...Network.knownNetworks].sort((a, b) => { - return b.signalStrength - a.signalStrength; - }) + remove: Transition { + Anim { + property: "opacity" + to: 0 + } } + section.delegate: Rectangle { + required property string section - NetworkItem { - known: true - repeater: known - } - } + color: "transparent" + height: childrenRect.height + width: ListView.view.width - Spacer { - repeater: unknown - visible: Network.knownNetworks.length > 0 - } - - CustomText { - Layout.leftMargin: Tokens.padding.normal - Layout.preferredHeight: visible ? implicitHeight : 0 - color: Colors.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - text: Network.unknownNetworks.length > 1 ? qsTr("%1 networks available").arg(Network.unknownNetworks.length) : qsTr("1 network available") // qmllint disable missing-property - visible: Network.unknownNetworks.length > 0 - } - - Repeater { - id: unknown - - model: ScriptModel { - values: [...Network.unknownNetworks].sort((a, b) => { - return b.signalStrength - a.signalStrength; - }) - } - - NetworkItem { - known: false - repeater: unknown + CustomText { + text: parent.section + } } } } @@ -158,23 +154,31 @@ CustomClippingRect { component NetworkItem: ConnectedRect { id: networkItem + readonly property int connectedCount: Network.connectedNetworks.length readonly property bool connecting: Network.connectingNetwork === modelData property int horizontalPadding: Tokens.padding.largeIncreased + readonly property bool inConnectedGroup: index < connectedCount + // sectioning first / last item in list reminder + // if index === network.connectedNetworks.length - 1 + // if index === network.connectedNetworks.length - 1 + network.knownNetworks.length - 2 + // if index === network.connectedNetworks.length - 1 + network.knownNetworks.length + // list.model.values.length - 1 required property int index readonly property bool isSecure: Network.isSecure(modelData.security) - required property bool known + required property ListView list required property var modelData - required property Repeater repeater + property int verticalPadding: Tokens.padding.smaller Layout.fillWidth: true - Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0 - first: index === 0 - last: index === (repeater.count - 1) + first: !inConnectedGroup && index === connectedCount + 1 + implicitHeight: rowLayout.implicitHeight + verticalPadding * 2 + last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1 radius: Tokens.rounding.small RowLayout { id: rowLayout + Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0 anchors.fill: parent anchors.leftMargin: networkItem.horizontalPadding anchors.rightMargin: networkItem.horizontalPadding @@ -201,7 +205,7 @@ CustomClippingRect { color: Colors.palette.m3outline elide: Text.ElideRight font.pointSize: Tokens.font.size.small - text: qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") + text: networkItem.modelData.connected ? qsTr("Connected: %1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") : qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") } } @@ -255,12 +259,28 @@ CustomClippingRect { } } } - component Spacer: Item { - id: spacer + component StatusDivider: CustomText { + required property var modelData - required property Repeater repeater + bottomPadding: Tokens.spacing.extraSmall + color: Colors.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.smaller + text: { + if (modelData.type === "known") { + const count = Network.knownNetworks.filter(n => !n.connected).length; - Layout.preferredHeight: repeater.count > 0 ? Tokens.spacing.extraSmall : 0 + return qsTr("%1 known network%2").arg(count).arg(count === 1 ? "" : "s"); + } + + if (modelData.type === "unknown") { + const count = Network.unknownNetworks.filter(n => !n.connected).length; + + return qsTr("%1 network%2 available").arg(count).arg(count === 1 ? "" : "s"); + } + + return qsTr("object unknown"); + } + topPadding: Tokens.spacing.small } component Toggle: CustomSwitch { id: toggleItem -- 2.47.3