From cada065b0027286c3cc9d99e9080e5f643a8d64d Mon Sep 17 00:00:00 2001 From: Turann_ Date: Tue, 10 Mar 2026 01:16:30 +0300 Subject: [PATCH] feat(network): improve UI consistency and connection info display --- Modules/Bar/Widgets/Network.qml | 56 ++-- Modules/Panels/Network/NetworkPanel.qml | 280 +++++++++++++----- .../Tabs/Connections/BluetoothSubTab.qml | 24 +- .../Settings/Tabs/Connections/WifiSubTab.qml | 51 ++-- Services/Networking/NetworkService.qml | 126 +++++--- 5 files changed, 341 insertions(+), 196 deletions(-) diff --git a/Modules/Bar/Widgets/Network.qml b/Modules/Bar/Widgets/Network.qml index 1fc143f7e..d03abdd2a 100644 --- a/Modules/Bar/Widgets/Network.qml +++ b/Modules/Bar/Widgets/Network.qml @@ -105,20 +105,24 @@ Item { } } text: { - try { - if (NetworkService.ethernetConnected) { - return ""; + let parts = []; + if (NetworkService.ethernetConnected) { + const d = NetworkService.activeEthernetDetails; + const name = d.connectionName || NetworkService.ethernetInterfaces[0]?.connectionName || ""; + const speed = d.speed || ""; + if (name) { + parts.push(speed ? (name + " - " + speed) : name); } - for (const net in NetworkService.networks) { - if (NetworkService.networks[net].connected) { - return net; - } - } - return ""; - } catch (error) { - Logger.e("Wi-Fi", "Error getting ssid:", error); - return "error"; } + if (NetworkService.activeWifiIf) { + const d = NetworkService.activeWifiDetails; + const name = d.connectionName || ""; + const speed = d.rateShort || d.rate || ""; + if (name) { + parts.push(speed ? (name + " - " + speed) : name); + } + } + return parts.join(isBarVertical ? "\n" : " | "); } autoHide: false forceOpen: !isBarVertical && root.displayMode === "alwaysShow" @@ -135,30 +139,16 @@ Item { return ""; } try { - if (NetworkService.ethernetConnected) { - const d = NetworkService.activeEthernetDetails || ({}); - let base = ""; - if (d.ifname && d.ifname.length > 0) - base = d.ifname; - else if (d.connectionName && d.connectionName.length > 0) - base = d.connectionName; - else if (NetworkService.activeEthernetIf && NetworkService.activeEthernetIf.length > 0) - base = NetworkService.activeEthernetIf; - else - base = I18n.tr("common.ethernet"); - const speed = (d.speed && d.speed.length > 0) ? d.speed : ""; - return speed ? (base + " — " + speed) : base; - } - // Wi‑Fi tooltip: SSID — link speed (if available) - if (pill.text !== "") { - const w = NetworkService.activeWifiDetails || ({}); - const rate = (w.rateShort && w.rateShort.length > 0) ? w.rateShort : (w.rate || ""); - return rate && rate.length > 0 ? (pill.text + " — " + rate) : pill.text; + const name = pill.text; + if (!name) { + return I18n.tr("common.wifi"); } + const d = NetworkService.ethernetConnected ? NetworkService.activeEthernetDetails : NetworkService.activeWifiDetails; + const speed = (d.speed && d.speed.length > 0) ? d.speed : ((d.rateShort && d.rateShort.length > 0) ? d.rateShort : (d.rate || "")); + return speed ? (name + " — " + speed) : name; } catch (e) { - // noop + return I18n.tr("common.wifi"); } - return I18n.tr("common.wifi"); } } } diff --git a/Modules/Panels/Network/NetworkPanel.qml b/Modules/Panels/Network/NetworkPanel.qml index 56582d4f5..3e1b84f2a 100644 --- a/Modules/Panels/Network/NetworkPanel.qml +++ b/Modules/Panels/Network/NetworkPanel.qml @@ -7,6 +7,7 @@ import qs.Commons import qs.Modules.MainScreen import qs.Modules.Panels.Settings import qs.Services.Networking +import qs.Services.System import qs.Services.UI import qs.Widgets @@ -20,6 +21,7 @@ SmartPanel { // Ethernet details UI state (mirrors Wi‑Fi info behavior) property bool ethernetInfoExpanded: false property bool ethernetDetailsGrid: (Settings.data.network.wifiDetailsViewMode === "grid") + property int ipVersion: 4 // Unified panel view mode: "wifi" | "ethernet" (persisted) property string panelViewMode: "wifi" @@ -45,6 +47,7 @@ SmartPanel { } onOpened: { + SystemStatService.registerComponent("network-panel"); NetworkService.scan(); // Preload active Wi‑Fi details so Info shows instantly NetworkService.refreshActiveWifiDetails(); @@ -68,6 +71,10 @@ SmartPanel { panelViewPersistEnabled = true; } + onClosed: { + SystemStatService.unregisterComponent("network-panel"); + } + panelContent: Rectangle { color: "transparent" @@ -95,7 +102,13 @@ SmartPanel { id: modeIcon icon: panelViewMode === "wifi" ? (Settings.data.network.wifiEnabled ? "wifi" : "wifi-off") : (NetworkService.hasEthernet() ? (NetworkService.ethernetConnected ? "ethernet" : "ethernet") : "ethernet-off") pointSize: Style.fontSizeXXL - color: panelViewMode === "wifi" ? (Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant) : (NetworkService.ethernetConnected ? Color.mPrimary : Color.mOnSurfaceVariant) + color: { + if (panelViewMode === "wifi") { + return Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant; + } else { + return NetworkService.ethernetConnected ? Color.mPrimary : Color.mOnSurfaceVariant; + } + } MouseArea { anchors.fill: parent hoverEnabled: true @@ -426,6 +439,10 @@ SmartPanel { delegate: NBox { id: ethItem + HoverHandler { + id: itemHover + } + Layout.fillWidth: true Layout.leftMargin: Style.marginXS Layout.rightMargin: Style.marginXS @@ -462,7 +479,7 @@ SmartPanel { spacing: 2 NText { - text: modelData.ifname + text: modelData.connectionName || modelData.ifname pointSize: Style.fontSizeM font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium color: Color.mOnSurface @@ -473,20 +490,75 @@ SmartPanel { RowLayout { spacing: Style.marginXS - // Connected badge (mirrors Wi‑Fi chip) - Rectangle { - visible: modelData.connected - color: Color.mPrimary - radius: height * 0.5 - width: ethConnectedText.implicitWidth + Style.margin2S - height: ethConnectedText.implicitHeight + (Style.margin2XXS) + NText { + text: { + if (modelData.connected) { + switch (NetworkService.networkConnectivity) { + case "full": + return I18n.tr("common.connected"); + case "limited": + return I18n.tr("wifi.panel.internet-limited"); + case "portal": + return I18n.tr("wifi.panel.action-required"); + default: + return NetworkService.networkConnectivity; + } + } + return I18n.tr("common.disconnected"); + } + pointSize: Style.fontSizeXXS + color: { + if (!modelData.connected) { + return Color.mError; + } + if (NetworkService.networkConnectivity === "limited" || NetworkService.networkConnectivity === "portal") { + return Color.mError; + } + return Color.mPrimary; + } + } + + // Network speed indicators (visible when connected and speed > 0) + RowLayout { + visible: modelData.connected && (SystemStatService.rxSpeed > 0 || SystemStatService.txSpeed > 0) + spacing: 2 + Layout.leftMargin: Style.marginXS + Layout.fillWidth: false + + NIcon { + visible: SystemStatService.rxSpeed > 0 + icon: "arrow-down" + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + } NText { - id: ethConnectedText - anchors.centerIn: parent - text: I18n.tr("common.connected") + visible: SystemStatService.rxSpeed > 0 + text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) pointSize: Style.fontSizeXXS - color: Color.mOnPrimary + color: Color.mOnSurfaceVariant + elide: Text.ElideNone + } + + Item { + visible: SystemStatService.rxSpeed > 0 && SystemStatService.txSpeed > 0 + width: Style.marginXS + height: 1 + } + + NIcon { + visible: SystemStatService.txSpeed > 0 + icon: "arrow-up" + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + } + + NText { + visible: SystemStatService.txSpeed > 0 + text: SystemStatService.formatSpeed(SystemStatService.txSpeed) + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + elide: Text.ElideNone } } } @@ -494,6 +566,7 @@ SmartPanel { // Info button on the right NIconButton { + visible: itemHover.hovered icon: "info" tooltipText: I18n.tr("common.info") baseSize: Style.baseWidgetSize * 0.8 @@ -563,6 +636,8 @@ SmartPanel { anchors.fill: parent anchors.margins: Style.marginS anchors.rightMargin: Style.baseWidgetSize + flow: ethernetDetailsGrid ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: ethernetDetailsGrid ? 3 : 6 columns: ethernetDetailsGrid ? 2 : 1 columnSpacing: Style.marginM rowSpacing: Style.marginXS @@ -575,13 +650,10 @@ SmartPanel { } // --- Item 1: Interface --- - // Grid: Row 0, Col 0 | List: Row 0 RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 spacing: Style.marginXS - Layout.row: 0 - Layout.column: 0 NIcon { icon: "ethernet" pointSize: Style.fontSizeXS @@ -618,55 +690,63 @@ SmartPanel { const value = (NetworkService.activeEthernetDetails.ifname && NetworkService.activeEthernetDetails.ifname.length > 0) ? NetworkService.activeEthernetDetails.ifname : (NetworkService.activeEthernetIf || ""); if (value.length > 0) { Quickshell.execDetached(["wl-copy", value]); - ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("toast.bluetooth.address-copied"), "ethernet"); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); } } } } } - // --- Item 2: Internet connectivity -- - // Grid: Row 1, Col 0 | List: Row 1 + // --- Item 2: Hardware Address --- RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: 1 - Layout.column: 0 spacing: Style.marginXS NIcon { - // If the selected Ethernet interface is disconnected, show an explicit disconnected state - icon: modelData.connected ? (NetworkService.internetConnectivity ? "world" : "world-off") : "world-off" + icon: "hash" pointSize: Style.fontSizeXS - color: modelData.connected ? (NetworkService.internetConnectivity ? Color.mOnSurface : Color.mError) : Color.mError + color: Color.mOnSurface Layout.alignment: Qt.AlignVCenter MouseArea { anchors.fill: parent hoverEnabled: true - onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.internet-status")) + onEntered: TooltipService.show(parent, I18n.tr("bluetooth.panel.device-address")) onExited: TooltipService.hide() } } NText { - // Show "Disconnected" when the interface itself is down - text: modelData.connected ? (NetworkService.internetConnectivity ? I18n.tr("wifi.panel.internet-connected") : I18n.tr("wifi.panel.internet-limited")) : I18n.tr("common.disconnected") + text: NetworkService.activeEthernetDetails.hwAddr || "-" pointSize: Style.fontSizeXS - color: modelData.connected ? (NetworkService.internetConnectivity ? Color.mOnSurface : Color.mError) : Color.mError + color: Color.mOnSurface Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone maximumLineCount: ethernetDetailsGrid ? 1 : 6 clip: true + + MouseArea { + anchors.fill: parent + enabled: (NetworkService.activeEthernetDetails.hwAddr || "").length > 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = NetworkService.activeEthernetDetails.hwAddr || ""; + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); + } + } + } } } - // --- Iterm 3: Link speed --- - // Grid: Row 2, Col 0 | List: Row 2 + // --- Item 3: Link speed --- RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: 2 - Layout.column: 0 spacing: Style.marginXS NIcon { icon: "gauge" @@ -693,46 +773,10 @@ SmartPanel { } } - // --- Item 4: Gateway --- - // Grid: Row 2, Col 1 | List: Row 5 (Last) + // --- Item 4: IPv4 || IPv6 --- RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: ethernetDetailsGrid ? 2 : 5 - Layout.column: ethernetDetailsGrid ? 1 : 0 - spacing: Style.marginXS - NIcon { - icon: "router" - pointSize: Style.fontSizeXS - color: Color.mOnSurface - Layout.alignment: Qt.AlignVCenter - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: TooltipService.show(parent, I18n.tr("common.gateway")) - onExited: TooltipService.hide() - } - } - NText { - text: NetworkService.activeEthernetDetails.gateway4 || "-" - pointSize: Style.fontSizeXS - color: Color.mOnSurface - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere - elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone - maximumLineCount: ethernetDetailsGrid ? 1 : 6 - clip: true - } - } - - // --- Item 5: IPv4 --- - // Grid: Row 0, Col 1 | List: Row 3 - RowLayout { - Layout.fillWidth: true - Layout.preferredWidth: 1 - Layout.row: ethernetDetailsGrid ? 0 : 3 - Layout.column: ethernetDetailsGrid ? 1 : 0 spacing: Style.marginXS NIcon { icon: "network" @@ -742,12 +786,16 @@ SmartPanel { MouseArea { anchors.fill: parent hoverEnabled: true - onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.ipv4")) + onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.ipv4") : I18n.tr("wifi.panel.ipv6")) onExited: TooltipService.hide() + onClicked: { + root.ipVersion = root.ipVersion === 4 ? 6 : 4; + TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.ipv4") : I18n.tr("wifi.panel.ipv6")); + } } } NText { - text: NetworkService.activeEthernetDetails.ipv4 || "-" + text: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "-") : ((NetworkService.activeEthernetDetails.ipv6 || []).join(", ") || "-") pointSize: Style.fontSizeXS color: Color.mOnSurface Layout.fillWidth: true @@ -757,33 +805,29 @@ SmartPanel { maximumLineCount: ethernetDetailsGrid ? 1 : 6 clip: true - // Click-to-copy Ethernet IPv4 address + // Click-to-copy Ethernet IP address MouseArea { anchors.fill: parent - // Normalize to string to avoid undefined -> bool assignment warnings - enabled: (NetworkService.activeEthernetDetails.ipv4 || "").length > 0 + enabled: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "").length > 0 : (NetworkService.activeEthernetDetails.ipv6 || []).length > 0 hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) onExited: TooltipService.hide() onClicked: { - const value = NetworkService.activeEthernetDetails.ipv4 || ""; + const value = root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "") : ((NetworkService.activeEthernetDetails.ipv6 || []).join(", ") || ""); if (value.length > 0) { Quickshell.execDetached(["wl-copy", value]); - ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("toast.bluetooth.address-copied"), "ethernet"); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); } } } } } - // --- Item 6: DNS --- - // Grid: Row 1, Col 1 | List: Row 4 + // --- Item 5: DNS --- RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: ethernetDetailsGrid ? 1 : 4 - Layout.column: ethernetDetailsGrid ? 1 : 0 spacing: Style.marginXS NIcon { icon: "world" @@ -793,12 +837,16 @@ SmartPanel { MouseArea { anchors.fill: parent hoverEnabled: true - onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.dns")) + onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv6") + ")") onExited: TooltipService.hide() + onClicked: { + root.ipVersion = root.ipVersion === 4 ? 6 : 4; + TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv6") + ")"); + } } } NText { - text: NetworkService.activeEthernetDetails.dns || "-" + text: root.ipVersion === 4 ? ((NetworkService.activeEthernetDetails.dns4 || []).join(", ") || "-") : ((NetworkService.activeEthernetDetails.dns6 || []).join(", ") || "-") pointSize: Style.fontSizeXS color: Color.mOnSurface Layout.fillWidth: true @@ -807,6 +855,74 @@ SmartPanel { elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone maximumLineCount: ethernetDetailsGrid ? 1 : 6 clip: true + + // Click-to-copy Ethernet DNS + MouseArea { + anchors.fill: parent + enabled: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.dns4 || []).length > 0 : (NetworkService.activeEthernetDetails.dns6 || []).length > 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = root.ipVersion === 4 ? ((NetworkService.activeEthernetDetails.dns4 || []).join(", ") || "") : ((NetworkService.activeEthernetDetails.dns6 || []).join(", ") || ""); + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); + } + } + } + } + } + + // --- Item 6: Gateway --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "router" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv6") + ")") + onExited: TooltipService.hide() + onClicked: { + root.ipVersion = root.ipVersion === 4 ? 6 : 4; + TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv6") + ")"); + } + } + } + NText { + text: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.gateway4 || "-") : ((NetworkService.activeEthernetDetails.gateway6 || []).join(", ") || "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere + elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone + maximumLineCount: ethernetDetailsGrid ? 1 : 6 + clip: true + + // Click-to-copy Ethernet Gateway + MouseArea { + anchors.fill: parent + enabled: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.gateway4 || "").length > 0 : (NetworkService.activeEthernetDetails.gateway6 || []).length > 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.gateway4 || "") : ((NetworkService.activeEthernetDetails.gateway6 || []).join(", ") || ""); + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); + } + } + } } } } diff --git a/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml b/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml index fa8318882..e00e0f381 100644 --- a/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml @@ -370,6 +370,10 @@ Item { NBox { id: device + HoverHandler { + id: itemHover + } + readonly property bool canConnect: BluetoothService.canConnect(modelData) readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData) readonly property bool canPair: BluetoothService.canPair(modelData) @@ -471,7 +475,7 @@ Item { spacing: Style.marginS NIconButton { - visible: modelData.connected + visible: itemHover.hovered && modelData.connected icon: "info" tooltipText: I18n.tr("common.info") baseSize: Style.baseWidgetSize * 0.8 @@ -482,7 +486,7 @@ Item { } NIconButton { - visible: !root.showOnlyLists && (modelData.paired || modelData.trusted) && !modelData.connected && !isBusy && !modelData.blocked + visible: itemHover.hovered && !root.showOnlyLists && (modelData.paired || modelData.trusted) && !modelData.connected && !isBusy && !modelData.blocked icon: "trash" tooltipText: I18n.tr("common.unpair") baseSize: Style.baseWidgetSize * 0.8 @@ -491,7 +495,7 @@ Item { NButton { id: button - visible: (modelData.state !== BluetoothDeviceState.Connecting) + visible: itemHover.hovered && (modelData.state !== BluetoothDeviceState.Connecting) enabled: (canConnect || canDisconnect || (root.showOnlyLists ? false : canPair)) && !isBusy outlined: !button.hovered fontSize: Style.fontSizeS @@ -552,6 +556,8 @@ Item { id: infoColumn anchors.fill: parent anchors.margins: Style.marginS + flow: root.detailsGrid ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: root.detailsGrid ? 3 : 6 columns: root.detailsGrid ? 2 : 1 columnSpacing: Style.marginM rowSpacing: Style.marginXS @@ -561,8 +567,6 @@ Item { Layout.fillWidth: true Layout.preferredWidth: 1 spacing: Style.marginXS - Layout.row: detailsGrid ? 0 : 0 - Layout.column: 0 NIcon { icon: BluetoothService.getSignalIcon(modelData) pointSize: Style.fontSizeXS @@ -580,8 +584,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 0 : 1 - Layout.column: detailsGrid ? 1 : 0 spacing: Style.marginXS NIcon { icon: { @@ -605,8 +607,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 1 : 2 - Layout.column: 0 spacing: Style.marginXS NIcon { icon: "link" @@ -624,8 +624,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 1 : 3 - Layout.column: detailsGrid ? 1 : 0 spacing: Style.marginXS NIcon { icon: "shield-check" @@ -643,8 +641,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 2 : 4 - Layout.column: 0 spacing: Style.marginXS NIcon { icon: "hash" @@ -662,8 +658,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 2 : 5 - Layout.column: detailsGrid ? 1 : 0 spacing: Style.marginXS visible: Settings.data.network.bluetoothAutoConnect diff --git a/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml b/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml index c2273409f..f9f04ab11 100644 --- a/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml @@ -510,6 +510,10 @@ Item { NBox { id: networkItem + HoverHandler { + id: itemHover + } + readonly property bool isBusy: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid readonly property bool isExpanded: root.infoSsid === modelData.ssid readonly property bool isEnterprise: NetworkService.isEnterprise(modelData.security) @@ -606,7 +610,15 @@ Item { return NetworkService.isSecured(modelData.security) ? modelData.security : "Open"; } pointSize: Style.fontSizeXXS - color: networkItem.getContentColor(Color.mOnSurfaceVariant) + color: { + if (modelData.connected) { + return (NetworkService.networkConnectivity === "full") ? Color.mPrimary : Color.mError; + } + if (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) { + return Color.mError; + } + return networkItem.getContentColor(Color.mOnSurfaceVariant); + } } // Network speed indicators (visible when connected and speed > 0) @@ -614,6 +626,7 @@ Item { visible: modelData.connected && (SystemStatService.rxSpeed > 0 || SystemStatService.txSpeed > 0) spacing: 2 Layout.leftMargin: Style.marginXS + Layout.fillWidth: false NIcon { visible: SystemStatService.rxSpeed > 0 @@ -627,6 +640,7 @@ Item { text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) pointSize: Style.fontSizeXXS color: networkItem.getContentColor(Color.mOnSurfaceVariant) + elide: Text.ElideNone } Item { @@ -647,6 +661,7 @@ Item { text: SystemStatService.formatSpeed(SystemStatService.txSpeed) pointSize: Style.fontSizeXXS color: networkItem.getContentColor(Color.mOnSurfaceVariant) + elide: Text.ElideNone } } } @@ -667,7 +682,7 @@ Item { } NIconButton { - visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid + visible: itemHover.hovered && modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid icon: "info" tooltipText: I18n.tr("common.info") baseSize: Style.baseWidgetSize * 0.8 @@ -682,7 +697,7 @@ Item { } NIconButton { - visible: !root.showOnlyLists && (modelData.existing || modelData.cached) && !modelData.connected && !networkItem.isBusy + visible: itemHover.hovered && !root.showOnlyLists && (modelData.existing || modelData.cached) && !modelData.connected && !networkItem.isBusy icon: "trash" tooltipText: I18n.tr("tooltips.forget-network") baseSize: Style.baseWidgetSize * 0.8 @@ -691,7 +706,7 @@ Item { NButton { id: button - visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && root.passwordSsid !== modelData.ssid + visible: itemHover.hovered && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && root.passwordSsid !== modelData.ssid enabled: !NetworkService.connecting && !networkItem.isBusy outlined: !button.hovered fontSize: Style.fontSizeS @@ -708,7 +723,7 @@ Item { NButton { id: disconnectButton - visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid + visible: itemHover.hovered && modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid text: I18n.tr("common.disconnect") outlined: !disconnectButton.hovered fontSize: Style.fontSizeS @@ -755,6 +770,8 @@ Item { id: infoColumn anchors.fill: parent anchors.margins: Style.marginS + flow: root.detailsGrid ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: root.detailsGrid ? 3 : 6 columns: root.detailsGrid ? 2 : 1 columnSpacing: Style.marginM rowSpacing: Style.marginXS @@ -771,8 +788,6 @@ Item { Layout.fillWidth: true Layout.preferredWidth: 1 spacing: Style.marginXS - Layout.row: 0 - Layout.column: 0 NIcon { icon: "network" pointSize: Style.fontSizeXS @@ -815,8 +830,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 1 : 1 - Layout.column: 0 spacing: Style.marginXS NIcon { icon: "router" @@ -839,8 +852,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 2 : 2 - Layout.column: 0 spacing: Style.marginXS NIcon { icon: "gauge" @@ -864,8 +875,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 0 : 3 - Layout.column: detailsGrid ? 1 : 0 spacing: Style.marginXS NIcon { icon: "network" @@ -883,7 +892,7 @@ Item { } } NText { - text: root.ipVersion === 4 ? (NetworkService.activeWifiDetails.ipv4 || "-") : (NetworkService.activeWifiDetails.ipv6 || "-") + text: root.ipVersion === 4 ? (NetworkService.activeWifiDetails.ipv4 || "-") : ((NetworkService.activeWifiDetails.ipv6 || []).join(", ") || "-") pointSize: Style.fontSizeXS color: Color.mOnSurface Layout.fillWidth: true @@ -896,7 +905,7 @@ Item { onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) onExited: TooltipService.hide() onClicked: { - const value = root.ipVersion === 4 ? (NetworkService.activeWifiDetails.ipv4 || "") : (NetworkService.activeWifiDetails.ipv6 || ""); + const value = root.ipVersion === 4 ? (NetworkService.activeWifiDetails.ipv4 || "") : ((NetworkService.activeWifiDetails.ipv6 || []).join(", ") || ""); if (value.length > 0) { Quickshell.execDetached(["wl-copy", value]); ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.bluetooth.address-copied"), "wifi"); @@ -909,8 +918,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 1 : 4 - Layout.column: detailsGrid ? 1 : 0 spacing: Style.marginXS NIcon { icon: "world" @@ -928,7 +935,7 @@ Item { } } NText { - text: root.ipVersion === 4 ? (NetworkService.activeWifiDetails.dns4 || "-") : (NetworkService.activeWifiDetails.dns6 || "-") + text: root.ipVersion === 4 ? ((NetworkService.activeWifiDetails.dns4 || []).join(", ") || "-") : ((NetworkService.activeWifiDetails.dns6 || []).join(", ") || "-") pointSize: Style.fontSizeXS color: Color.mOnSurface Layout.fillWidth: true @@ -941,7 +948,7 @@ Item { onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) onExited: TooltipService.hide() onClicked: { - const value = root.ipVersion === 4 ? (NetworkService.activeWifiDetails.dns4 || "") : (NetworkService.activeWifiDetails.dns6 || ""); + const value = root.ipVersion === 4 ? ((NetworkService.activeWifiDetails.dns4 || []).join(", ") || "") : ((NetworkService.activeWifiDetails.dns6 || []).join(", ") || ""); if (value.length > 0) { Quickshell.execDetached(["wl-copy", value]); ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.bluetooth.address-copied"), "wifi"); @@ -954,8 +961,6 @@ Item { RowLayout { Layout.fillWidth: true Layout.preferredWidth: 1 - Layout.row: detailsGrid ? 2 : 5 - Layout.column: detailsGrid ? 1 : 0 spacing: Style.marginXS NIcon { icon: "router" @@ -973,7 +978,7 @@ Item { } } NText { - text: root.ipVersion === 4 ? (NetworkService.activeWifiDetails.gateway4 || "-") : (NetworkService.activeWifiDetails.gateway6 || "-") + text: root.ipVersion === 4 ? (NetworkService.activeWifiDetails.gateway4 || "-") : ((NetworkService.activeWifiDetails.gateway6 || []).join(", ") || "-") pointSize: Style.fontSizeXS color: Color.mOnSurface Layout.fillWidth: true @@ -986,7 +991,7 @@ Item { onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) onExited: TooltipService.hide() onClicked: { - const value = root.ipVersion === 4 ? (NetworkService.activeWifiDetails.gateway4 || "") : (NetworkService.activeWifiDetails.gateway6 || ""); + const value = root.ipVersion === 4 ? (NetworkService.activeWifiDetails.gateway4 || "") : ((NetworkService.activeWifiDetails.gateway6 || []).join(", ") || ""); if (value.length > 0) { Quickshell.execDetached(["wl-copy", value]); ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.bluetooth.address-copied"), "wifi"); diff --git a/Services/Networking/NetworkService.qml b/Services/Networking/NetworkService.qml index d1149500c..b4c75e791 100644 --- a/Services/Networking/NetworkService.qml +++ b/Services/Networking/NetworkService.qml @@ -41,14 +41,38 @@ Singleton { // Supported Wi-Fi security types property var supportedSecurityTypes: [ - { key: "open", name: I18n.tr("wifi.panel.security-open") }, - { key: "wep", name: I18n.tr("wifi.panel.security-wep") }, - { key: "wpa-psk", name: I18n.tr("wifi.panel.security-wpa") }, - { key: "wpa2-psk", name: I18n.tr("wifi.panel.security-wpa23") }, - { key: "sae", name: I18n.tr("wifi.panel.security-wpa3") }, - { key: "wpa-eap", name: I18n.tr("wifi.panel.security-wpa-ent") }, - { key: "wpa2-eap", name: I18n.tr("wifi.panel.security-wpa2-ent") }, - { key: "wpa3-eap", name: I18n.tr("wifi.panel.security-wpa3-ent") } + { + key: "open", + name: I18n.tr("wifi.panel.security-open") + }, + { + key: "wep", + name: I18n.tr("wifi.panel.security-wep") + }, + { + key: "wpa-psk", + name: I18n.tr("wifi.panel.security-wpa") + }, + { + key: "wpa2-psk", + name: I18n.tr("wifi.panel.security-wpa23") + }, + { + key: "sae", + name: I18n.tr("wifi.panel.security-wpa3") + }, + { + key: "wpa-eap", + name: I18n.tr("wifi.panel.security-wpa-ent") + }, + { + key: "wpa2-eap", + name: I18n.tr("wifi.panel.security-wpa2-ent") + }, + { + key: "wpa3-eap", + name: I18n.tr("wifi.panel.security-wpa3-ent") + } ] // Active Wi‑Fi connection details (for info panel) @@ -476,14 +500,45 @@ Singleton { function parseIpDetails(text) { const details = { + connectionName: "", ipv4: "", gateway4: "", - ipv6: "", - gateway6: "", dns4: [], + ipv6: [], + gateway6: [], dns6: [], - dns: "", - connectionName: "" + hwAddr: "" + }; + const addUnique = (arr, val) => { + if (val && arr.indexOf(val) === -1) { + arr.push(val); + } + }; + const handlers = { + "GENERAL.CONNECTION": v => { + details.connectionName = v; + }, + "GENERAL.HWADDR": v => { + details.hwAddr = v; + }, + "IP4.ADDRESS": v => { + details.ipv4 = v.split("/")[0]; + }, + "IP4.GATEWAY": v => { + details.gateway4 = v; + }, + "IP6.ADDRESS": v => { + addUnique(details.ipv6, v.split("/")[0]); + }, + "IP6.GATEWAY": v => { + addUnique(details.gateway6, v); + }, + "IP4.DNS": v => { + addUnique(details.dns4, v); + }, + "IP6.DNS": v => { + addUnique(details.dns6, v); + } }; const lines = text.split("\n"); for (let i = 0; i < lines.length; i++) { @@ -495,31 +550,12 @@ Singleton { if (idx === -1) { continue; } - const key = line.substring(0, idx); - const val = line.substring(idx + 1); - if (key === "GENERAL.CONNECTION") { - details.connectionName = val; - } else if (key.indexOf("IP4.ADDRESS") === 0) { - details.ipv4 = val.split("/")[0]; - } else if (key === "IP4.GATEWAY") { - details.gateway4 = val; - } else if (key.indexOf("IP6.ADDRESS") === 0) { - details.ipv6 = val.split("/")[0]; - } else if (key === "IP6.GATEWAY") { - details.gateway6 = val; - } else if (key.indexOf("IP4.DNS") === 0) { - if (val && details.dns4.indexOf(val) === -1) { - details.dns4.push(val); - } - } else if (key.indexOf("IP6.DNS") === 0) { - if (val && details.dns6.indexOf(val) === -1) { - details.dns6.push(val); - } + const key = line.substring(0, idx).replace(/\[\d+\]$/, ""); + const val = line.substring(idx + 1).trim(); + if (handlers[key]) { + handlers[key](val); } } - details.dns4 = details.dns4.join(", "); - details.dns6 = details.dns6.join(", "); - details.dns = [].concat(details.dns4 ? [details.dns4] : [], details.dns6 ? [details.dns6] : []).join(", "); return details; } @@ -527,7 +563,7 @@ Singleton { Process { id: ethernetStateProcess running: ProgramCheckerService.nmcliAvailable - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"] stdout: StdioCollector { onStreamFinished: { @@ -540,11 +576,13 @@ Singleton { if (parts.length >= 3 && parts[1] === "ethernet" && parts[2] !== "unmanaged") { var ifname = parts[0]; var state = parts[2]; + var conName = parts.slice(3).join(":") || ""; var isConn = state === "connected"; ethList.push({ ifname: ifname, state: state, - connected: isConn + connected: isConn, + connectionName: conName }); if (isConn && !connected) { connected = true; @@ -592,7 +630,7 @@ Singleton { Process { id: ethernetDeviceListProcess running: false - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"] stdout: StdioCollector { onStreamFinished: { @@ -605,6 +643,7 @@ Singleton { const dev = parts[0]; const type = parts[1]; const state = parts[2]; + const conName = parts.slice(3).join(":") || ""; if (state === "unmanaged") { continue; } @@ -615,7 +654,8 @@ Singleton { ethList.push({ ifname: dev, state: state, - connected: state === "connected" + connected: state === "connected", + connectionName: conName }); } } @@ -658,7 +698,7 @@ Singleton { property string ifname: "" running: false // Speed is resolved via ethtool fallback below to avoid stderr warnings - command: ["nmcli", "-t", "-f", "GENERAL.CONNECTION,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS", "device", "show", ifname] + command: ["nmcli", "-t", "-f", "GENERAL.CONNECTION,GENERAL.HWADDR,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS,IP6.ADDRESS,IP6.GATEWAY,IP6.DNS", "device", "show", ifname] stdout: StdioCollector { onStreamFinished: { @@ -675,7 +715,7 @@ Singleton { details.gateway6 = parsed.gateway6; details.dns4 = parsed.dns4; details.dns6 = parsed.dns6; - details.dns = parsed.dns; + details.hwAddr = parsed.hwAddr; root.activeEthernetDetails = details; // If speed missing, try sysfs first, then fallback to ethtool @@ -825,20 +865,20 @@ Singleton { id: wifiDeviceShowProcess property string ifname: "" running: false - command: ["nmcli", "-t", "-f", "IP4.ADDRESS,IP4.GATEWAY,IP4.DNS,IP6.ADDRESS,IP6.GATEWAY,IP6.DNS", "device", "show", ifname] + command: ["nmcli", "-t", "-f", "GENERAL.CONNECTION,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS,IP6.ADDRESS,IP6.GATEWAY,IP6.DNS", "device", "show", ifname] stdout: StdioCollector { onStreamFinished: { const details = root.activeWifiDetails || ({}); const parsed = root.parseIpDetails(text); + details.connectionName = parsed.connectionName; details.ipv4 = parsed.ipv4; details.gateway4 = parsed.gateway4; details.ipv6 = parsed.ipv6; details.gateway6 = parsed.gateway6; details.dns4 = parsed.dns4; details.dns6 = parsed.dns6; - details.dns = parsed.dns; root.activeWifiDetails = details; // Try to get link rate (best effort)