From 507843be212bfc73e94055a48c71fd13a4374d96 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 08:54:13 -0400 Subject: [PATCH] --amend --- Modules/WiFiPanel/WiFiPanel.qml | 109 +++++------ Services/NetworkService.qml | 337 +++++++++++++++----------------- 2 files changed, 213 insertions(+), 233 deletions(-) diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 61216cc..c35ea1e 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -139,8 +139,7 @@ NPanel { ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: Settings.data.network.wifiEnabled && NetworkService.isLoading && Object.keys( - NetworkService.networks).length === 0 + visible: Settings.data.network.wifiEnabled && NetworkService.isLoading && Object.keys(NetworkService.networks).length === 0 spacing: Style.marginM * scaling NBusyIndicator { @@ -194,18 +193,15 @@ NPanel { // Network list Repeater { model: { - if (!Settings.data.network.wifiEnabled || NetworkService.isLoading) - return [] - + if (!Settings.data.network.wifiEnabled || NetworkService.isLoading) return [] + // Sort networks: connected first, then by signal strength const nets = Object.values(NetworkService.networks) return nets.sort((a, b) => { - if (a.connected && !b.connected) - return -1 - if (!a.connected && b.connected) - return 1 - return b.signal - a.signal - }) + if (a.connected && !b.connected) return -1 + if (!a.connected && b.connected) return 1 + return b.signal - a.signal + }) } Item { @@ -217,8 +213,7 @@ NPanel { width: parent.width implicitHeight: networkContent.implicitHeight + (Style.marginM * scaling * 2) radius: Style.radiusM * scaling - color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, - 0.05) : Color.mSurface + color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface border.width: Math.max(1, Style.borderS * scaling) border.color: modelData.connected ? Color.mPrimary : Color.mOutline clip: true @@ -259,8 +254,7 @@ NPanel { NText { text: { - const security = modelData.security - && modelData.security !== "--" ? modelData.security : "Open" + const security = modelData.security && modelData.security !== "--" ? modelData.security : "Open" const signal = `${modelData.signal}%` return `${signal} • ${security}` } @@ -282,7 +276,7 @@ NPanel { Layout.alignment: Qt.AlignRight | Qt.AlignVCenter spacing: Style.marginS * scaling - // Status badges + // Connected badge Rectangle { visible: modelData.connected color: Color.mPrimary @@ -299,6 +293,7 @@ NPanel { } } + // Saved badge - clickable Rectangle { visible: modelData.cached && !modelData.connected color: Color.mSurfaceVariant @@ -308,6 +303,18 @@ NPanel { border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Qt.darker(Color.mSurfaceVariant, 1.1) + onExited: parent.color = Color.mSurfaceVariant + onClicked: { + expandedNetwork = expandedNetwork === modelData.ssid ? "" : modelData.ssid + showPasswordPrompt = false + } + } + NText { id: savedLabel anchors.centerIn: parent @@ -317,17 +324,6 @@ NPanel { } } - NIconButton { - visible: modelData.existing || modelData.cached - icon: "more_vert" - tooltipText: "Options" - sizeRatio: 0.7 - onClicked: { - expandedNetwork = expandedNetwork === modelData.ssid ? "" : modelData.ssid - showPasswordPrompt = false - } - } - // Action buttons RowLayout { spacing: Style.marginXS * scaling @@ -337,8 +333,7 @@ NPanel { visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt) outlined: !hovered fontSize: Style.fontSizeXS * scaling - text: modelData.existing ? "Connect" : (NetworkService.isSecured( - modelData.security) ? "Password" : "Connect") + text: modelData.existing ? "Connect" : (NetworkService.isSecured(modelData.security) ? "Password" : "Connect") icon: "wifi" onClicked: { if (modelData.existing || !NetworkService.isSecured(modelData.security)) { @@ -389,8 +384,12 @@ NPanel { TextInput { id: passwordInputField - anchors.fill: parent - anchors.margins: Style.marginM * scaling + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginM * scaling + anchors.rightMargin: Style.marginM * scaling + height: parent.height - (Style.marginS * scaling * 2) text: passwordInput font.pointSize: Style.fontSizeS * scaling color: Color.mOnSurface @@ -423,6 +422,7 @@ NPanel { icon: "check" fontSize: Style.fontSizeXS * scaling enabled: passwordInput.length > 0 + outlined: !enabled onClicked: { if (passwordInput) { NetworkService.submitPassword(passwordPromptSsid, passwordInput) @@ -435,7 +435,7 @@ NPanel { NIconButton { icon: "close" tooltipText: "Cancel" - sizeRatio: 0.7 + sizeRatio: 0.9 onClicked: { showPasswordPrompt = false expandedNetwork = "" @@ -445,31 +445,27 @@ NPanel { } } - // Options menu (forget network) - Rectangle { - visible: expandedNetwork === modelData.ssid && !showPasswordPrompt && (modelData.existing - || modelData.cached) + // Forget network option - appears when saved badge is clicked + RowLayout { + visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid && !showPasswordPrompt Layout.fillWidth: true - implicitHeight: visible ? 40 * scaling : 0 - color: Color.mSurfaceVariant - radius: Style.radiusS * scaling + Layout.topMargin: Style.marginXS * scaling + spacing: Style.marginS * scaling - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginM * scaling + Item { Layout.fillWidth: true } - NButton { - Layout.fillWidth: true - text: "Forget Network" - icon: "delete" - fontSize: Style.fontSizeXS * scaling - backgroundColor: Color.mError - outlined: !hovered - onClicked: { - NetworkService.forgetNetwork(modelData.ssid) - expandedNetwork = "" - } + NButton { + id: forgetButton + text: "Forget Network" + icon: "delete_outline" + fontSize: Style.fontSizeXXS * scaling + backgroundColor: Color.mError + textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary + outlined: !forgetButton.hovered + Layout.preferredHeight: 28 * scaling + onClicked: { + NetworkService.forgetNetwork(modelData.ssid) + expandedNetwork = "" } } } @@ -482,8 +478,7 @@ NPanel { ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys( - NetworkService.networks).length === 0 + visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys(NetworkService.networks).length === 0 spacing: Style.marginM * scaling NIcon { @@ -511,4 +506,4 @@ NPanel { } } } -} +} \ No newline at end of file diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index a201e27..ec94319 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -18,7 +18,7 @@ Singleton { property bool ethernet: false property int retryCount: 0 property int maxRetries: 3 - + // File path for persistent storage property string cacheFile: Settings.cacheDir + "network.json" @@ -38,7 +38,7 @@ Singleton { autoConnectTimer.start() } } - onLoadFailed: function (error) { + onLoadFailed: function(error) { Logger.log("Network", "No existing cache found, creating new one") // Initialize with empty data adapter.knownNetworks = ({}) @@ -63,7 +63,7 @@ Singleton { Component.onCompleted: { Logger.log("Network", "Service started") - + if (Settings.data.network.wifiEnabled) { refreshNetworks() } @@ -71,23 +71,15 @@ Singleton { // Signal strength icon mapping function signalIcon(signal) { - const levels = [{ - "threshold": 80, - "icon": "network_wifi" - }, { - "threshold": 60, - "icon": "network_wifi_3_bar" - }, { - "threshold": 40, - "icon": "network_wifi_2_bar" - }, { - "threshold": 20, - "icon": "network_wifi_1_bar" - }] - + const levels = [ + { threshold: 80, icon: "network_wifi" }, + { threshold: 60, icon: "network_wifi_3_bar" }, + { threshold: 40, icon: "network_wifi_2_bar" }, + { threshold: 20, icon: "network_wifi_1_bar" } + ] + for (const level of levels) { - if (signal >= level.threshold) - return level.icon + if (signal >= level.threshold) return level.icon } return "signal_wifi_0_bar" } @@ -98,9 +90,8 @@ Singleton { // Enhanced refresh with retry logic function refreshNetworks() { - if (isLoading) - return - + if (isLoading) return + isLoading = true retryCount = 0 adapter.lastRefresh = Date.now() @@ -146,20 +137,20 @@ Singleton { // Forget network function function forgetNetwork(ssid) { Logger.log("Network", `Forgetting network: ${ssid}`) - + // Remove from cache let known = adapter.knownNetworks delete known[ssid] adapter.knownNetworks = known - + // Clear last connected if it's this network if (adapter.lastConnected === ssid) { adapter.lastConnected = "" } - + // Save changes saveTimer.restart() - + // Remove NetworkManager profile forgetProcess.ssid = ssid forgetProcess.running = true @@ -170,14 +161,14 @@ Singleton { property string ssid: "" running: false command: ["nmcli", "connection", "delete", "id", ssid] - + stdout: StdioCollector { onStreamFinished: { Logger.log("Network", `Successfully forgot network: ${forgetProcess.ssid}`) refreshNetworks() } } - + stderr: StdioCollector { onStreamFinished: { if (text.includes("no such connection profile")) { @@ -206,7 +197,7 @@ Singleton { break } } - + wifiRadioProcess.action = "off" wifiRadioProcess.running = true } @@ -218,7 +209,7 @@ Singleton { property string action: "on" running: false command: ["nmcli", "radio", "wifi", action] - + onRunningChanged: { if (!running) { if (action === "on") { @@ -229,7 +220,7 @@ Singleton { } } } - + stderr: StdioCollector { onStreamFinished: { if (text.trim()) { @@ -268,26 +259,26 @@ Singleton { connectStatus = "" connectStatusSsid = ssid connectError = "" - + // Check if profile exists if (networks[ssid]?.existing) { connectToExisting(ssid) return } - + // Check cache for known network const known = adapter.knownNetworks[ssid] if (known?.profileName) { connectToExisting(known.profileName) return } - + // New connection - need password for secured networks if (isSecured(security)) { // Password will be provided through submitPassword return } - + // Open network - connect directly createAndConnect(ssid, "", security) } @@ -305,7 +296,7 @@ Singleton { function createAndConnect(ssid, password, security) { connectingSsid = ssid - + connectProcess.ssid = ssid connectProcess.password = password connectProcess.isSecured = isSecured(security) @@ -324,7 +315,7 @@ Singleton { property string password: "" property bool isSecured: false running: false - + command: { const cmd = ["nmcli", "device", "wifi", "connect", ssid] if (isSecured && password) { @@ -332,13 +323,13 @@ Singleton { } return cmd } - + stdout: StdioCollector { onStreamFinished: { handleConnectionSuccess(connectProcess.ssid) } } - + stderr: StdioCollector { onStreamFinished: { handleConnectionError(connectProcess.ssid, text) @@ -351,13 +342,13 @@ Singleton { property string profileName: "" running: false command: ["nmcli", "connection", "up", "id", profileName] - + stdout: StdioCollector { onStreamFinished: { handleConnectionSuccess(upConnectionProcess.profileName) } } - + stderr: StdioCollector { onStreamFinished: { handleConnectionError(upConnectionProcess.profileName, text) @@ -370,7 +361,7 @@ Singleton { property string ssid: "" running: false command: ["nmcli", "connection", "down", "id", ssid] - + onRunningChanged: { if (!running) { connectingSsid = "" @@ -380,7 +371,7 @@ Singleton { refreshNetworks() } } - + stderr: StdioCollector { onStreamFinished: { if (text.trim()) { @@ -396,18 +387,18 @@ Singleton { connectStatus = "success" connectStatusSsid = ssid connectError = "" - + // Update cache let known = adapter.knownNetworks known[ssid] = { - "profileName": ssid, - "lastConnected": Date.now(), - "autoConnect": true + profileName: ssid, + lastConnected: Date.now(), + autoConnect: true } adapter.knownNetworks = known adapter.lastConnected = ssid saveTimer.restart() - + Logger.log("Network", `Successfully connected to ${ssid}`) refreshNetworks() } @@ -417,7 +408,7 @@ Singleton { connectStatus = "error" connectStatusSsid = ssid connectError = parseError(error) - + Logger.warn("Network", `Failed to connect to ${ssid}: ${error}`) } @@ -444,144 +435,138 @@ Singleton { id: existingNetworkProcess running: false command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] - + stdout: StdioCollector { onStreamFinished: { const profiles = {} const lines = text.split("\n").filter(l => l.trim()) - + for (const line of lines) { - const [ = line.split(":") - if (name && type === "802-11-wireless") { - profiles[name] = { - "ssid": name, - "type": type - } - } + const [name, type] = line.split(":") + if (name && type === "802-11-wireless") { + profiles[name] = { ssid: name, type: type } + } } - - scanProcess.existingProfiles = profiles - scanProcess.running = true + + scanProcess.existingProfiles = profiles + scanProcess.running = true + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.warn("Network", "Error listing connections:", text) + retryRefresh() } + } + } + } + + Process { + id: scanProcess + property var existingProfiles: ({}) + running: false + command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] + + stdout: StdioCollector { + onStreamFinished: { + const networksMap = {} + const lines = text.split("\n").filter(l => l.trim()) + + for (const line of lines) { + const parts = line.split(":") + if (parts.length < 4) continue + + const [ssid, security, signalStr, inUse] = parts + if (!ssid) continue + + const signal = parseInt(signalStr) || 0 + const connected = inUse === "*" + + // Update last connected if we find the connected network + if (connected && adapter.lastConnected !== ssid) { + adapter.lastConnected = ssid + saveTimer.restart() + } + + // Merge with existing or create new + if (!networksMap[ssid] || signal > networksMap[ssid].signal) { + networksMap[ssid] = { + ssid: ssid, + security: security || "--", + signal: signal, + connected: connected, + existing: ssid in scanProcess.existingProfiles, + cached: ssid in adapter.knownNetworks + } + } } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", "Error listing connections:", text) - retryRefresh() - } - } - } + + root.networks = networksMap + root.isLoading = false + scanProcess.existingProfiles = {} + + Logger.log("Network", `Found ${Object.keys(networksMap).length} networks`) + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.warn("Network", "Error scanning networks:", text) + retryRefresh() } + } + } + } - Process { - id: scanProcess - property var existingProfiles: ({}) - running: false - command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] + Process { + id: checkEthernet + running: false + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + + stdout: StdioCollector { + onStreamFinished: { + root.ethernet = text.split("\n").some(line => { + const parts = line.split(":") + return parts[1] === "ethernet" && parts[2] === "connected" + }) + } + } + } - stdout: StdioCollector { - onStreamFinished: { - const networksMap = {} - const lines = text.split("\n").filter(l => l.trim()) + // Auto-refresh timer + Timer { + interval: 30000 // 30 seconds + running: Settings.data.network.wifiEnabled && !isLoading + repeat: true + onTriggered: { + // Only refresh if we should + const now = Date.now() + const timeSinceLastRefresh = now - adapter.lastRefresh + + // Refresh if: connected, or it's been more than 30 seconds + if (hasActiveConnection || timeSinceLastRefresh > 30000) { + refreshNetworks() + } + } + } - for (const line of lines) { - const parts = line.split(":") - if (parts.length < 4) - continue + property bool hasActiveConnection: { + return Object.values(networks).some(net => net.connected) + } - const [ = parts - if (!ssid) - continue + // Menu state management + function onMenuOpened() { + if (Settings.data.network.wifiEnabled) { + refreshNetworks() + } + } - const signal = parseInt(signalStr) || 0 - const connected = inUse === "*" - - // Update last connected if we find the connected network - if (connected && adapter.lastConnected !== ssid) { - adapter.lastConnected = ssid - saveTimer.restart() - } - - // Merge with existing or create new - if (!networksMap[ssid] || signal > networksMap[ssid].signal) { - networksMap[ssid] = { - "ssid": ssid, - "security": security || "--", - "signal": signal, - "connected": connected, - "existing": ssid in scanProcess.existingProfiles, - "cached": ssid in adapter.knownNetworks - } - } - } - - root.networks = networksMap - root.isLoading = false - scanProcess.existingProfiles = {} - - Logger.log("Network", `Found ${Object.keys(networksMap).length} networks`) - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", "Error scanning networks:", text) - retryRefresh() - } - } - } - } - - Process { - id: checkEthernet - running: false - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] - - stdout: StdioCollector { - onStreamFinished: { - root.ethernet = text.split("\n").some(line => { - const parts = line.split(":") - return parts[1] === "ethernet" - && parts[2] === "connected" - }) - } - } - } - - // Auto-refresh timer - Timer { - interval: 30000 // 30 seconds - running: Settings.data.network.wifiEnabled && !isLoading - repeat: true - onTriggered: { - // Only refresh if we should - const now = Date.now() - const timeSinceLastRefresh = now - adapter.lastRefresh - - // Refresh if: connected, or it's been more than 30 seconds - if (hasActiveConnection || timeSinceLastRefresh > 30000) { - refreshNetworks() - } - } - } - - property bool hasActiveConnection: { - return Object.values(networks).some(net => net.connected) - } - - // Menu state management - function onMenuOpened() { - if (Settings.data.network.wifiEnabled) { - refreshNetworks() - } - } - - function onMenuClosed() { - // Clean up temporary states - connectStatus = "" - connectError = "" - } - } + function onMenuClosed() { + // Clean up temporary states + connectStatus = "" + connectError = "" + } +} \ No newline at end of file