diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 262d6a7604..40d7b71691 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -385,7 +385,6 @@ "network": { "bluetoothRssiPollingEnabled": false, "bluetoothRssiPollIntervalMs": 60000, - "networkPanelView": "wifi", "wifiDetailsViewMode": "grid", "bluetoothDetailsViewMode": "grid", "bluetoothHideUnnamedDevices": false, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index aecf878359..b2f8f5f43e 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -611,7 +611,6 @@ Singleton { property JsonObject network: JsonObject { property bool bluetoothRssiPollingEnabled: false // Opt-in Bluetooth RSSI polling (uses bluetoothctl) property int bluetoothRssiPollIntervalMs: 60000 // Polling interval in milliseconds for RSSI queries - property string networkPanelView: "wifi" property string wifiDetailsViewMode: "grid" // "grid" or "list" property string bluetoothDetailsViewMode: "grid" // "grid" or "list" property bool bluetoothHideUnnamedDevices: false diff --git a/Modules/Panels/Network/NetworkPanel.qml b/Modules/Panels/Network/NetworkPanel.qml index 2918c61828..862b89e3a8 100644 --- a/Modules/Panels/Network/NetworkPanel.qml +++ b/Modules/Panels/Network/NetworkPanel.qml @@ -17,31 +17,66 @@ SmartPanel { preferredWidth: Math.round(440 * Style.uiScaleRatio) preferredHeight: Math.round(500 * Style.uiScaleRatio) - // Info panel collapsed by default, view mode persisted in settings // 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) + // Panel view mode: "wifi" | "ethernet" | "vpn" (chosen dynamically on open) property string panelViewMode: "wifi" - property bool panelViewPersistEnabled: false + // Reference set by modeTabBar after creation (avoids ReferenceError in onPanelViewModeChanged) + property var _modeTabBar: null + + // If VPN configs vanish while on VPN tab, switch away + Connections { + target: VPNService + function onHasConnectionsChanged() { + if (!VPNService.hasConnections && root.panelViewMode === "vpn") { + root.panelViewMode = NetworkService.wifiAvailable ? "wifi" : "ethernet"; + } + } + } + + // If an adapter disappears while its tab is active, switch to the best available + Connections { + target: NetworkService + function onEthernetAvailableChanged() { + if (!NetworkService.ethernetAvailable && root.panelViewMode === "ethernet") { + root.panelViewMode = NetworkService.wifiAvailable ? "wifi" : VPNService.hasConnections ? "vpn" : "wifi"; + } + } + function onWifiAvailableChanged() { + if (!NetworkService.wifiAvailable && root.panelViewMode === "wifi") { + root.panelViewMode = NetworkService.ethernetAvailable ? "ethernet" : VPNService.hasConnections ? "vpn" : "ethernet"; + } + } + } onPanelViewModeChanged: { - // Persist last view (only after restored the initial value) - if (panelViewPersistEnabled) { - Settings.data.network.networkPanelView = panelViewMode; + // Sync tab bar (imperative – avoids declarative binding being broken by NTabButton clicks) + let _tabIdx = 0; + if (panelViewMode === "ethernet") { + _tabIdx = 1; + } else if (panelViewMode === "vpn") { + _tabIdx = 2; + } + if (_modeTabBar && _modeTabBar.currentIndex !== _tabIdx) { + _modeTabBar.currentIndex = _tabIdx; } + + ethernetInfoExpanded = false; + if (panelViewMode === "wifi") { - ethernetInfoExpanded = false; if (NetworkService.wifiEnabled && !NetworkService.scanningActive) { NetworkService.scan(); NetworkService.refreshActiveWifiDetails(); } - } else { + } else if (panelViewMode === "ethernet") { if (NetworkService.ethernetConnected) { NetworkService.refreshActiveEthernetDetails(); } + } else if (panelViewMode === "vpn") { + VPNService.refresh(); } } @@ -58,28 +93,26 @@ SmartPanel { if (NetworkService.ethernetConnected) { NetworkService.refreshActiveEthernetDetails(); } + if (VPNService.hasConnections) { + VPNService.refresh(); + } } else { SystemStatService.unregisterComponent("network-panel"); } } onOpened: { - // Restore last view if valid, otherwise choose what's available (prefer Wi‑Fi when both exist) - if (Settings.data.network.networkPanelView) { - const last = Settings.data.network.networkPanelView; - if (last === "ethernet" && NetworkService.ethernetAvailable) { - panelViewMode = "ethernet"; - } else { - panelViewMode = "wifi"; - } + if (NetworkService.wifiAvailable && NetworkService.wifiEnabled) { + panelViewMode = "wifi"; + } else if (NetworkService.ethernetAvailable) { + panelViewMode = "ethernet"; + } else if (VPNService.hasConnections) { + panelViewMode = "vpn"; + } else if (NetworkService.wifiAvailable) { + panelViewMode = "wifi"; } else { - if (!NetworkService.wifiEnabled && NetworkService.ethernetAvailable) { - panelViewMode = "ethernet"; - } else { - panelViewMode = "wifi"; - } + panelViewMode = "ethernet"; // fallback } - panelViewPersistEnabled = true; } panelContent: Rectangle { @@ -107,13 +140,23 @@ SmartPanel { RowLayout { NIcon { id: modeIcon - icon: panelViewMode === "wifi" ? (NetworkService.wifiEnabled ? "wifi" : "wifi-off") : (NetworkService.ethernetAvailable ? (NetworkService.ethernetConnected ? "ethernet" : "ethernet") : "ethernet-off") + icon: { + if (panelViewMode === "wifi") { + return NetworkService.wifiEnabled ? "wifi" : "wifi-off"; + } else if (panelViewMode === "ethernet") { + return NetworkService.ethernetAvailable ? "ethernet" : "ethernet-off"; + } else { + return VPNService.hasActiveConnection ? "shield-lock" : "shield"; + } + } pointSize: Style.fontSizeXXL color: { if (panelViewMode === "wifi") { return NetworkService.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant; - } else { + } else if (panelViewMode === "ethernet") { return NetworkService.ethernetConnected ? Color.mPrimary : Color.mOnSurfaceVariant; + } else { + return VPNService.hasActiveConnection ? Color.mPrimary : Color.mOnSurfaceVariant; } } MouseArea { @@ -126,17 +169,19 @@ SmartPanel { } else { TooltipService.show(parent, I18n.tr("wifi.panel.no-ethernet-devices")); } + } else if (panelViewMode === "ethernet") { + panelViewMode = "wifi"; } else { panelViewMode = "wifi"; } } - onEntered: TooltipService.show(parent, panelViewMode === "wifi" ? I18n.tr("common.wifi") : I18n.tr("common.ethernet")) + onEntered: TooltipService.show(parent, panelViewMode === "wifi" ? I18n.tr("common.wifi") : panelViewMode === "ethernet" ? I18n.tr("common.ethernet") : "VPN") onExited: TooltipService.hide() } } NLabel { - label: panelViewMode === "wifi" ? I18n.tr("common.wifi") : I18n.tr("common.ethernet") + label: panelViewMode === "wifi" ? I18n.tr("common.wifi") : panelViewMode === "ethernet" ? I18n.tr("common.ethernet") : "VPN" Layout.fillWidth: true } @@ -149,6 +194,24 @@ SmartPanel { baseSize: Style.baseWidgetSize * 0.7 // Slightly smaller } + NIconButton { + icon: "refresh" + visible: panelViewMode === "wifi" + tooltipText: I18n.tr("common.refresh") + baseSize: Style.baseWidgetSize * 0.8 + enabled: NetworkService.wifiEnabled && !NetworkService.scanningActive + onClicked: NetworkService.scan() + } + + NIconButton { + icon: "refresh" + visible: panelViewMode === "vpn" + tooltipText: I18n.tr("common.refresh") + baseSize: Style.baseWidgetSize * 0.8 + enabled: !VPNService.refreshing + onClicked: VPNService.refresh() + } + NIconButton { icon: "settings" tooltipText: I18n.tr("tooltips.open-settings") @@ -164,29 +227,44 @@ SmartPanel { } } - // Mode switch (Wi‑Fi / Ethernet) + // Mode switch (Wi‑Fi / Ethernet / VPN) NTabBar { id: modeTabBar - visible: NetworkService.ethernetAvailable && NetworkService.wifiAvailable - margins: Style.marginS + Component.onCompleted: root._modeTabBar = modeTabBar + visible: { + let count = 0; + if (NetworkService.wifiAvailable) count++; + if (NetworkService.ethernetAvailable) count++; + if (VPNService.hasConnections) count++; + return count > 1; + } Layout.fillWidth: true - spacing: Style.marginM + margins: Style.marginS distributeEvenly: true - currentIndex: root.panelViewMode === "wifi" ? 0 : 1 + currentIndex: (root.panelViewMode === "wifi") ? 0 : (root.panelViewMode === "ethernet") ? 1 : 2 onCurrentIndexChanged: { - root.panelViewMode = (currentIndex === 0) ? "wifi" : "ethernet"; + root.panelViewMode = (currentIndex === 0) ? "wifi" : (currentIndex === 1) ? "ethernet" : "vpn"; } NTabButton { text: I18n.tr("common.wifi") tabIndex: 0 checked: modeTabBar.currentIndex === 0 + visible: NetworkService.wifiAvailable } NTabButton { text: I18n.tr("common.ethernet") tabIndex: 1 checked: modeTabBar.currentIndex === 1 + visible: NetworkService.ethernetAvailable + } + + NTabButton { + text: "VPN" + tabIndex: 2 + checked: modeTabBar.currentIndex === 2 + visible: VPNService.hasConnections } } } @@ -195,7 +273,6 @@ SmartPanel { // Unified scrollable content (Wi‑Fi or Ethernet view) ColumnLayout { id: wifiSectionContainer - visible: true Layout.fillWidth: true spacing: Style.marginM @@ -394,23 +471,26 @@ SmartPanel { ColumnLayout { id: ethernetColumn anchors.fill: parent - anchors.margins: Style.marginM + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.leftMargin: Style.marginL + anchors.rightMargin: Style.marginL spacing: Style.marginM // Section label NLabel { label: I18n.tr("wifi.panel.available-interfaces") - visible: (NetworkService.ethernetInterfaces && NetworkService.ethernetInterfaces.length > 0) + visible: NetworkService.ethernetInterfaces.length > 0 + Layout.leftMargin: Style.marginS } // Empty state when no Ethernet devices ColumnLayout { id: emptyEthColumn - Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.preferredHeight: emptyEthColumn.implicitHeight + Style.margin2M - visible: !(NetworkService.ethernetInterfaces && NetworkService.ethernetInterfaces.length > 0) + Layout.preferredHeight: emptyEthColumn.implicitHeight + visible: NetworkService.ethernetInterfaces.length === 0 spacing: Style.marginL Item { @@ -437,168 +517,144 @@ SmartPanel { } // Interfaces list - ColumnLayout { - id: ethIfacesList - visible: NetworkService.ethernetInterfaces && NetworkService.ethernetInterfaces.length > 0 - width: parent.width - spacing: Style.marginXS - - Repeater { - model: NetworkService.ethernetInterfaces || [] - delegate: NBox { - id: ethItem - - function getContentColors(defaultColors = [Color.mSurface, Color.mOnSurface]) { - if (modelData.connected) { - return [Color.mPrimary, Color.mOnPrimary]; - } - return defaultColors; + Repeater { + visible: NetworkService.ethernetInterfaces.length > 0 + model: NetworkService.ethernetInterfaces + delegate: NBox { + id: ethItem + + function getContentColors(defaultColors = [Color.mSurface, Color.mOnSurface]) { + if (modelData.connected) { + return [Color.mPrimary, Color.mOnPrimary]; } + return defaultColors; + } - Layout.fillWidth: true - Layout.leftMargin: Style.marginXS - Layout.rightMargin: Style.marginXS - implicitHeight: ethItemColumn.implicitHeight + Style.margin2M - radius: Style.radiusM - forceOpaque: true - color: ethItem.getContentColors()[0] - - ColumnLayout { - id: ethItemColumn - width: parent.width - Style.margin2M - x: Style.marginM - y: Style.marginM - spacing: Style.marginS - - // Main row matching Wi‑Fi card style - // Click handling for the whole header row is provided by a sibling MouseArea - // anchored to this row (defined right after this RowLayout). - RowLayout { - id: ethHeaderRow + Layout.fillWidth: true + Layout.preferredHeight: ethItemColumn.implicitHeight + Style.marginXL + radius: Style.radiusM + clip: true + forceOpaque: true + color: ethItem.getContentColors()[0] + + ColumnLayout { + id: ethItemColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + // Main row matching Wi‑Fi card style + // Click handling for the whole header row is provided by a sibling MouseArea + // anchored to this row (defined right after this RowLayout). + RowLayout { + id: ethHeaderRow + Layout.fillWidth: true + spacing: Style.marginM + Layout.alignment: Qt.AlignVCenter + + NIcon { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + horizontalAlignment: Text.AlignLeft + icon: NetworkService.getIcon(true) + pointSize: Style.fontSizeXXL + color: ethItem.getContentColors()[1] + } + + ColumnLayout { Layout.fillWidth: true - spacing: Style.marginS + spacing: Style.marginXXS - NIcon { - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - horizontalAlignment: Text.AlignLeft - icon: NetworkService.getIcon(true) - pointSize: Style.fontSizeXXL + NText { + text: modelData.connectionName || modelData.ifname + pointSize: Style.fontSizeM + font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium color: ethItem.getContentColors()[1] + elide: Text.ElideRight + Layout.fillWidth: true } - ColumnLayout { - Layout.fillWidth: true - spacing: 2 + RowLayout { + spacing: Style.marginXS NText { - text: modelData.connectionName || modelData.ifname - pointSize: Style.fontSizeM - font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium - color: ethItem.getContentColors()[1] - elide: Text.ElideRight - Layout.fillWidth: true + text: { + if (modelData.connected) { + switch (NetworkService.networkConnectivity) { + case "full": + return I18n.tr("common.connected"); + case "limited": + case "unknown": + 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: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) } + // Network speed indicators (visible when connected and speed > 0) RowLayout { - spacing: Style.marginXS + visible: (modelData.connected && NetworkService.networkConnectivity === "full") && (SystemStatService.rxSpeed > 0 || SystemStatService.txSpeed > 0) + spacing: 2 + Layout.leftMargin: Style.marginXS + Layout.fillWidth: false - NText { - text: { - if (modelData.connected) { - switch (NetworkService.networkConnectivity) { - case "full": - return I18n.tr("common.connected"); - case "limited": - case "unknown": - 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"); - } + NIcon { + visible: SystemStatService.rxSpeed > 0 + icon: "arrow-down" pointSize: Style.fontSizeXXS color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) } - // Network speed indicators (visible when connected and speed > 0) - RowLayout { - visible: (modelData.connected && NetworkService.networkConnectivity === "full") && (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: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) - } - - NText { - visible: SystemStatService.rxSpeed > 0 - text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) - pointSize: Style.fontSizeXXS - color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) - 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: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) - } + NText { + visible: SystemStatService.rxSpeed > 0 + text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) + pointSize: Style.fontSizeXXS + color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) + elide: Text.ElideNone + } - NText { - visible: SystemStatService.txSpeed > 0 - text: SystemStatService.formatSpeed(SystemStatService.txSpeed) - pointSize: Style.fontSizeXXS - color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) - elide: Text.ElideNone - } + Item { + visible: SystemStatService.rxSpeed > 0 && SystemStatService.txSpeed > 0 + width: Style.marginXS + height: 1 } - } - } - // Info button on the right - NIconButton { - icon: "info" - tooltipText: I18n.tr("common.info") - baseSize: Style.baseWidgetSize * 0.75 - colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface - colorBorder: "transparent" - colorBorderHover: "transparent" - enabled: true - visible: NetworkService.ethernetConnected - onClicked: { - if (NetworkService.activeEthernetIf === modelData.ifname && ethernetInfoExpanded) { - ethernetInfoExpanded = false; - return; + NIcon { + visible: SystemStatService.txSpeed > 0 + icon: "arrow-up" + pointSize: Style.fontSizeXXS + color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) } - if (NetworkService.activeEthernetIf !== modelData.ifname) { - NetworkService.activeEthernetIf = modelData.ifname; - NetworkService.activeEthernetDetailsTimestamp = 0; + + NText { + visible: SystemStatService.txSpeed > 0 + text: SystemStatService.formatSpeed(SystemStatService.txSpeed) + pointSize: Style.fontSizeXXS + color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) + elide: Text.ElideNone } - ethernetInfoExpanded = true; - NetworkService.refreshActiveEthernetDetails(); } } } - // Click handling without anchors in a Layout-managed item - TapHandler { - target: ethHeaderRow - onTapped: { + // Info button on the right + NIconButton { + icon: "info" + tooltipText: I18n.tr("common.info") + baseSize: Style.baseWidgetSize * 0.75 + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: "transparent" + colorBorderHover: "transparent" + enabled: true + visible: NetworkService.ethernetConnected + onClicked: { if (NetworkService.activeEthernetIf === modelData.ifname && ethernetInfoExpanded) { ethernetInfoExpanded = false; return; @@ -611,330 +667,590 @@ SmartPanel { NetworkService.refreshActiveEthernetDetails(); } } + } - // Inline Ethernet details - Rectangle { - id: ethInfoInline - visible: ethernetInfoExpanded && NetworkService.activeEthernetIf === modelData.ifname - Layout.fillWidth: true - color: Color.mSurfaceVariant - radius: Style.radiusXS - border.width: Style.borderS - border.color: Style.boxBorderColor - implicitHeight: ethInfoGrid.implicitHeight + Style.margin2S - clip: true - Layout.topMargin: Style.marginXS - - // Grid/List toggle - NIconButton { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Style.marginS - icon: ethernetDetailsGrid ? "layout-list" : "layout-grid" - tooltipText: ethernetDetailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view") - baseSize: Style.baseWidgetSize * 0.65 - onClicked: { - ethernetDetailsGrid = !ethernetDetailsGrid; - Settings.data.network.wifiDetailsViewMode = ethernetDetailsGrid ? "grid" : "list"; + // Click handling without anchors in a Layout-managed item + TapHandler { + target: ethHeaderRow + onTapped: { + if (NetworkService.activeEthernetIf === modelData.ifname && ethernetInfoExpanded) { + ethernetInfoExpanded = false; + return; + } + if (NetworkService.activeEthernetIf !== modelData.ifname) { + NetworkService.activeEthernetIf = modelData.ifname; + NetworkService.activeEthernetDetailsTimestamp = 0; + } + ethernetInfoExpanded = true; + NetworkService.refreshActiveEthernetDetails(); + } + } + + // Inline Ethernet details + Rectangle { + id: ethInfoInline + visible: ethernetInfoExpanded && NetworkService.activeEthernetIf === modelData.ifname + Layout.fillWidth: true + color: Color.mSurfaceVariant + radius: Style.radiusXS + border.width: Style.borderS + border.color: Style.boxBorderColor + implicitHeight: ethInfoGrid.implicitHeight + Style.margin2S + clip: true + Layout.topMargin: Style.marginXS + + // Grid/List toggle + NIconButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginS + icon: ethernetDetailsGrid ? "layout-list" : "layout-grid" + tooltipText: ethernetDetailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view") + baseSize: Style.baseWidgetSize * 0.65 + onClicked: { + ethernetDetailsGrid = !ethernetDetailsGrid; + Settings.data.network.wifiDetailsViewMode = ethernetDetailsGrid ? "grid" : "list"; + } + z: 1 + } + + GridLayout { + id: ethInfoGrid + 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 + onColumnsChanged: { + if (ethInfoGrid.forceLayout) { + Qt.callLater(function () { + ethInfoGrid.forceLayout(); + }); } - z: 1 } - GridLayout { - id: ethInfoGrid - 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 - onColumnsChanged: { - if (ethInfoGrid.forceLayout) { - Qt.callLater(function () { - ethInfoGrid.forceLayout(); - }); + // --- Item 1: Interface --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "ethernet" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.interface")) + onExited: TooltipService.hide() } } - - // --- Item 1: Interface --- - RowLayout { + NText { + text: (NetworkService.activeEthernetDetails.ifname && NetworkService.activeEthernetDetails.ifname.length > 0) ? NetworkService.activeEthernetDetails.ifname : (NetworkService.activeEthernetIf || "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface Layout.fillWidth: true - Layout.preferredWidth: 1 - spacing: Style.marginXS - NIcon { - icon: "ethernet" - pointSize: Style.fontSizeXS - color: Color.mOnSurface - Layout.alignment: Qt.AlignVCenter - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.interface")) - onExited: TooltipService.hide() - } - } - NText { - text: (NetworkService.activeEthernetDetails.ifname && NetworkService.activeEthernetDetails.ifname.length > 0) ? NetworkService.activeEthernetDetails.ifname : (NetworkService.activeEthernetIf || "-") - 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 interface name - MouseArea { - anchors.fill: parent - // Guard against undefined by normalizing to empty strings - enabled: ((NetworkService.activeEthernetDetails.ifname || "").length > 0) || ((NetworkService.activeEthernetIf || "").length > 0) - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) - onExited: TooltipService.hide() - onClicked: { - 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("common.copied-to-clipboard"), "ethernet"); - } + 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 interface name + MouseArea { + anchors.fill: parent + // Guard against undefined by normalizing to empty strings + enabled: ((NetworkService.activeEthernetDetails.ifname || "").length > 0) || ((NetworkService.activeEthernetIf || "").length > 0) + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + 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("common.copied-to-clipboard"), "ethernet"); } } } } + } - // --- Item 2: Hardware Address --- - RowLayout { - Layout.fillWidth: true - Layout.preferredWidth: 1 - spacing: Style.marginXS - NIcon { - icon: "hash" - pointSize: Style.fontSizeXS - color: Color.mOnSurface - Layout.alignment: Qt.AlignVCenter - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: TooltipService.show(parent, I18n.tr("bluetooth.panel.device-address")) - onExited: TooltipService.hide() - } + // --- Item 2: Hardware Address --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "hash" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, I18n.tr("bluetooth.panel.device-address")) + onExited: TooltipService.hide() } - NText { - text: NetworkService.activeEthernetDetails.hwAddr || "-" - 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 - - 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"); - } + } + NText { + text: NetworkService.activeEthernetDetails.hwAddr || "-" + 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 + + 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"); } } } } + } - // --- Item 3: Link speed --- - RowLayout { - Layout.fillWidth: true - Layout.preferredWidth: 1 - spacing: Style.marginXS - NIcon { - icon: "gauge" - pointSize: Style.fontSizeXS - color: Color.mOnSurface - Layout.alignment: Qt.AlignVCenter - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.link-speed")) - onExited: TooltipService.hide() - } - } - NText { - text: (NetworkService.activeEthernetDetails.speed && NetworkService.activeEthernetDetails.speed.length > 0) ? NetworkService.activeEthernetDetails.speed : "-" - 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 3: Link speed --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "gauge" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.link-speed")) + onExited: TooltipService.hide() } } - - // --- Item 4: IPv4 || IPv6 --- - RowLayout { + NText { + text: (NetworkService.activeEthernetDetails.speed && NetworkService.activeEthernetDetails.speed.length > 0) ? NetworkService.activeEthernetDetails.speed : "-" + pointSize: Style.fontSizeXS + color: Color.mOnSurface Layout.fillWidth: true - Layout.preferredWidth: 1 - spacing: Style.marginXS - NIcon { - icon: "network" - 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("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")); - } + Layout.alignment: Qt.AlignVCenter + wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere + elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone + maximumLineCount: ethernetDetailsGrid ? 1 : 6 + clip: true + } + } + + // --- Item 4: IPv4 || IPv6 --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "network" + 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("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: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "-") : ((NetworkService.activeEthernetDetails.ipv6 || []).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 IP address - MouseArea { - anchors.fill: parent - 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 = 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("common.copied-to-clipboard"), "ethernet"); - } + } + NText { + text: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "-") : ((NetworkService.activeEthernetDetails.ipv6 || []).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 IP address + MouseArea { + anchors.fill: parent + 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 = 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("common.copied-to-clipboard"), "ethernet"); } } } } + } - // --- Item 5: DNS --- - RowLayout { - Layout.fillWidth: true - Layout.preferredWidth: 1 - spacing: Style.marginXS - NIcon { - icon: "world" - 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("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") + ")"); - } + // --- Item 5: DNS --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "world" + 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("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: root.ipVersion === 4 ? ((NetworkService.activeEthernetDetails.dns4 || []).join(", ") || "-") : ((NetworkService.activeEthernetDetails.dns6 || []).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 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"); - } + } + NText { + text: root.ipVersion === 4 ? ((NetworkService.activeEthernetDetails.dns4 || []).join(", ") || "-") : ((NetworkService.activeEthernetDetails.dns6 || []).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 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") + ")"); - } + // --- 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"); - } + } + 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"); } } } } } + + } + } + } + } + } + } + } + + // VPN error message + Rectangle { + visible: panelViewMode === "vpn" && VPNService.lastError.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: vpnErrorRow.implicitHeight + Style.margin2M + color: Qt.alpha(Color.mError, 0.1) + radius: Style.radiusS + border.width: Style.borderS + border.color: Color.mError + + RowLayout { + id: vpnErrorRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NIcon { + icon: "warning" + pointSize: Style.fontSizeL + color: Color.mError + } + + NText { + text: VPNService.lastError + color: Color.mError + pointSize: Style.fontSizeS + wrapMode: Text.Wrap + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.6 + onClicked: VPNService.lastError = "" + } + } + } + + // VPN Connected section + NBox { + visible: panelViewMode === "vpn" && VPNService.activeConnections.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: vpnConnectedColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: vpnConnectedColumn + anchors.fill: parent + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.leftMargin: Style.marginL + anchors.rightMargin: Style.marginL + spacing: Style.marginM + + NLabel { + label: I18n.tr("common.connected") + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + } + + Repeater { + model: VPNService.activeConnections + + delegate: NBox { + id: vpnItem + + function getContentColors(defaultColors = [Color.mPrimary, Color.mOnPrimary]) { + if (VPNService.disconnectingUuid === modelData.uuid) { + return [Color.mError, Color.mOnError]; + } + return defaultColors; + } + + Layout.fillWidth: true + Layout.preferredHeight: vpnActiveColumn.implicitHeight + Style.marginXL + radius: Style.radiusM + clip: true + forceOpaque: true + color: vpnItem.getContentColors()[0] + + ColumnLayout { + id: vpnActiveColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + RowLayout { + id: vpnActiveRow + Layout.fillWidth: true + spacing: Style.marginM + Layout.alignment: Qt.AlignVCenter + + NIcon { + icon: "shield-lock" + pointSize: Style.fontSizeXXL + color: vpnItem.getContentColors()[1] + Layout.alignment: Qt.AlignVCenter + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + + NText { + text: modelData.name + pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + color: vpnItem.getContentColors()[1] + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + text: (VPNService.disconnecting && VPNService.disconnectingUuid === modelData.uuid) ? I18n.tr("common.disconnecting") : I18n.tr("common.connected") + pointSize: Style.fontSizeXXS + color: Qt.alpha(vpnItem.getContentColors()[1], Style.opacityHeavy) + } + } + + NBusyIndicator { + visible: VPNService.disconnecting && VPNService.disconnectingUuid === modelData.uuid + running: visible && root.effectivelyVisible + color: vpnItem.getContentColors()[1] + size: Style.baseWidgetSize * 0.5 + } + + NButton { + text: I18n.tr("common.disconnect") + fontSize: Style.fontSizeS + backgroundColor: Color.mSurfaceVariant + textColor: Color.mOnSurface + enabled: !VPNService.disconnecting + onClicked: VPNService.disconnect(modelData.uuid) + } + } + } + } + } + } + } + + // VPN Available (inactive) section + NBox { + visible: panelViewMode === "vpn" && VPNService.inactiveConnections.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: vpnAvailableColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: vpnAvailableColumn + anchors.fill: parent + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.leftMargin: Style.marginL + anchors.rightMargin: Style.marginL + spacing: Style.marginM + + NLabel { + label: I18n.tr("common.disconnected") + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + } + + Repeater { + model: VPNService.inactiveConnections + + delegate: NBox { + id: vpnItem + + function getContentColors(defaultColors = [Color.mSurface, Color.mOnSurface]) { + if (VPNService.connectingUuid === modelData.uuid) { + return [Color.mPrimary, Color.mOnPrimary]; + } + return defaultColors; + } + + Layout.fillWidth: true + Layout.preferredHeight: vpnInactiveColumn.implicitHeight + Style.marginXL + radius: Style.radiusM + clip: true + forceOpaque: true + color: vpnItem.getContentColors()[0] + + ColumnLayout { + id: vpnInactiveColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + RowLayout { + id: vpnInactiveRow + Layout.fillWidth: true + spacing: Style.marginM + Layout.alignment: Qt.AlignVCenter + + NIcon { + icon: "shield" + pointSize: Style.fontSizeXXL + color: vpnItem.getContentColors()[1] + Layout.alignment: Qt.AlignVCenter + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + + NText { + text: modelData.name + pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + color: vpnItem.getContentColors()[1] + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + text: modelData.type || "vpn" + pointSize: Style.fontSizeXXS + color: Qt.alpha(vpnItem.getContentColors()[1], Style.opacityHeavy) + } + } + + NBusyIndicator { + visible: VPNService.connecting && VPNService.connectingUuid === modelData.uuid + running: visible && root.effectivelyVisible + color: vpnItem.getContentColors()[1] + size: Style.baseWidgetSize * 0.5 + } + + NButton { + text: I18n.tr("common.connect") + fontSize: Style.fontSizeS + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + enabled: !VPNService.connecting + onClicked: VPNService.connect(modelData.uuid) } } } @@ -948,3 +1264,4 @@ SmartPanel { } } } + diff --git a/Services/Networking/NetworkService.qml b/Services/Networking/NetworkService.qml index 8f1f106b23..56c3282fef 100644 --- a/Services/Networking/NetworkService.qml +++ b/Services/Networking/NetworkService.qml @@ -697,6 +697,7 @@ Singleton { newActiveWifiDetails.signal = signal; } + let wifiWasAvailable = root._wifiAvailable; root._wifiAvailable = wifiAvailable; root._ethernetAvailable = ethernetAvailable; root.ethernetConnected = (activeEthIf !== ""); @@ -704,6 +705,12 @@ Singleton { Logger.d("Network", "Device sync: wifiAvailable: " + wifiAvailable + ", ethAvailable: " + ethernetAvailable + ", wifiConnected: " + root.wifiConnected + " (" + activeWifiIf + "), ethConnected: " + root.ethernetConnected + " (" + activeEthIf + ")"); + // Adapter (re-)appeared: trigger a scan so the network list populates + if (wifiAvailable && !wifiWasAvailable && root.wifiEnabled && !root.scanningActive) { + delayedScanTimer.interval = 2000; + delayedScanTimer.restart(); + } + ethList.sort((a, b) => (a.connected !== b.connected) ? (a.connected ? -1 : 1) : a.ifname.localeCompare(b.ifname)); root.ethernetInterfaces = ethList; @@ -1154,6 +1161,9 @@ Singleton { Logger.d("Network", "State changed: " + data); deviceStatusProcess.running = true; connectivityCheckProcess.running = true; + } else if (data.endsWith(": unavailable") || data.indexOf(": device removed") !== -1 || data.indexOf(": device created") !== -1) { + Logger.d("Network", "Device event: " + data); + deviceStatusProcess.running = true; } } } diff --git a/Services/Networking/VPNService.qml b/Services/Networking/VPNService.qml index d8673076b9..a783f7b9a9 100644 --- a/Services/Networking/VPNService.qml +++ b/Services/Networking/VPNService.qml @@ -43,6 +43,7 @@ Singleton { } readonly property bool hasActiveConnection: activeConnections.length > 0 + readonly property bool hasConnections: Object.keys(connections).length > 0 Timer { id: refreshTimer @@ -87,6 +88,7 @@ Singleton { lastError = ""; connectProcess.uuid = uuid; connectProcess.name = conn.name; + connectProcess._stderrText = ""; connectProcess.running = true; } @@ -103,6 +105,7 @@ Singleton { lastError = ""; disconnectProcess.uuid = uuid; disconnectProcess.name = conn.name; + disconnectProcess._stderrText = ""; disconnectProcess.running = true; } @@ -176,6 +179,7 @@ Singleton { map[uuid] = { "uuid": uuid, "name": name, + "type": type, "device": device, "active": active }; @@ -210,39 +214,38 @@ Singleton { id: connectProcess property string uuid: "" property string name: "" + property string _stderrText: "" running: false command: ["nmcli", "connection", "up", "uuid", uuid] - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (!output || (!output.includes("successfully activated") && !output.includes("Connection successfully"))) { - return; - } + onExited: function (exitCode) { + if (exitCode === 0) { setConnection(connectProcess.uuid, { "active": true }); - connecting = false; - connectingUuid = ""; lastError = ""; Logger.i("VPN", "Connected to " + connectProcess.name); ToastService.showNotice(connectProcess.name, I18n.tr("toast.vpn.connected", { - "name": connectProcess.name - }), "shield-lock"); + "name": connectProcess.name + }), "shield-lock"); scheduleRefresh(1000); + } else { + const errText = connectProcess._stderrText.trim(); + if (errText) { + lastError = errText.split("\n")[0].trim(); + } else { + lastError = "Connection failed (exit code " + exitCode + ")"; + } + Logger.w("VPN", "Connect error (exit " + exitCode + "): " + lastError); + ToastService.showWarning(connectProcess.name, lastError); } + connecting = false; + connectingUuid = ""; } stderr: StdioCollector { onStreamFinished: { - const trimmed = text.trim(); - if (trimmed) { - lastError = trimmed.split("\n")[0].trim(); - Logger.w("VPN", "Connect error: " + trimmed); - ToastService.showWarning(connectProcess.name, lastError); - } - connecting = false; - connectingUuid = ""; + connectProcess._stderrText = text; } } } @@ -251,36 +254,39 @@ Singleton { id: disconnectProcess property string uuid: "" property string name: "" + property string _stderrText: "" running: false command: ["nmcli", "connection", "down", "uuid", uuid] - stdout: StdioCollector { - onStreamFinished: { + onExited: function (exitCode) { + if (exitCode === 0) { Logger.i("VPN", "Disconnected from " + disconnectProcess.name); setConnection(disconnectProcess.uuid, { "active": false, "device": "" }); - disconnecting = false; - disconnectingUuid = ""; lastError = ""; ToastService.showNotice(disconnectProcess.name, I18n.tr("toast.vpn.disconnected", { - "name": disconnectProcess.name - }), "shield-off"); + "name": disconnectProcess.name + }), "shield-off"); scheduleRefresh(1000); + } else { + const errText = disconnectProcess._stderrText.trim(); + if (errText) { + lastError = errText.split("\n")[0].trim(); + } else { + lastError = "Disconnect failed (exit code " + exitCode + ")"; + } + Logger.w("VPN", "Disconnect error (exit " + exitCode + "): " + lastError); + ToastService.showWarning(disconnectProcess.name, lastError); } + disconnecting = false; + disconnectingUuid = ""; } stderr: StdioCollector { onStreamFinished: { - const trimmed = text.trim(); - if (trimmed) { - lastError = trimmed.split("\n")[0].trim(); - Logger.w("VPN", "Disconnect error: " + trimmed); - ToastService.showWarning(disconnectProcess.name, lastError); - } - disconnecting = false; - disconnectingUuid = ""; + disconnectProcess._stderrText = text; } } }