diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 4b8b904..c6c2427 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -215,11 +215,23 @@ NPanel { Layout.fillWidth: true implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2) radius: Style.radiusM * scaling + + // Add opacity for operations in progress + opacity: (NetworkService.disconnectingFrom === modelData.ssid + || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0 + 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 + // Smooth opacity animation + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + } + } + ColumnLayout { id: netColumn width: parent.width - (Style.marginM * scaling * 2) @@ -276,8 +288,9 @@ NPanel { Layout.preferredWidth: Style.marginXXS * scaling } + // Update the status badges area (around line 237) Rectangle { - visible: modelData.connected + visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid color: Color.mPrimary radius: height * 0.5 width: connectedText.implicitWidth + (Style.marginS * scaling * 2) @@ -292,8 +305,42 @@ NPanel { } } + Rectangle { + visible: NetworkService.disconnectingFrom === modelData.ssid + color: Color.mError + radius: height * 0.5 + width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2) + height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2) + + NText { + id: disconnectingText + anchors.centerIn: parent + text: "Disconnecting..." + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnPrimary + } + } + + Rectangle { + visible: NetworkService.forgettingNetwork === modelData.ssid + color: Color.mError + radius: height * 0.5 + width: forgettingText.implicitWidth + (Style.marginS * scaling * 2) + height: forgettingText.implicitHeight + (Style.marginXXS * scaling * 2) + + NText { + id: forgettingText + anchors.centerIn: parent + text: "Forgetting..." + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnPrimary + } + } + Rectangle { visible: modelData.cached && !modelData.connected + && NetworkService.forgettingNetwork !== modelData.ssid + && NetworkService.disconnectingFrom !== modelData.ssid color: Color.transparent border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) @@ -318,6 +365,8 @@ NPanel { NBusyIndicator { visible: NetworkService.connectingTo === modelData.ssid + || NetworkService.disconnectingFrom === modelData.ssid + || NetworkService.forgettingNetwork === modelData.ssid running: visible color: Color.mPrimary size: Style.baseWidgetSize * 0.5 * scaling @@ -326,6 +375,8 @@ NPanel { NIconButton { visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid + && NetworkService.disconnectingFrom !== modelData.ssid icon: "delete" tooltipText: "Forget network" sizeRatio: 0.7 @@ -335,6 +386,8 @@ NPanel { NButton { visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid + && NetworkService.disconnectingFrom !== modelData.ssid text: { if (modelData.existing || modelData.cached) return "Connect" @@ -356,7 +409,7 @@ NPanel { } NButton { - visible: modelData.connected + visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid text: "Disconnect" outlined: !hovered fontSize: Style.fontSizeXS * scaling @@ -368,7 +421,8 @@ NPanel { // Password input Rectangle { - visible: passwordSsid === modelData.ssid + visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid Layout.fillWidth: true height: passwordRow.implicitHeight + Style.marginS * scaling * 2 color: Color.mSurfaceVariant @@ -449,7 +503,8 @@ NPanel { // Forget network Rectangle { - visible: expandedSsid === modelData.ssid + visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid Layout.fillWidth: true height: forgetRow.implicitHeight + Style.marginS * 2 * scaling color: Color.mSurfaceVariant diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 91fbca9..f46e12a 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -15,6 +15,8 @@ Singleton { property string connectingTo: "" property string lastError: "" property bool ethernetConnected: false + property string disconnectingFrom: "" + property string forgettingNetwork: "" // Persistent cache property string cacheFile: Settings.cacheDir + "network.json" @@ -123,11 +125,14 @@ Singleton { } function disconnect(ssid) { + disconnectingFrom = ssid disconnectProcess.ssid = ssid disconnectProcess.running = true } function forget(ssid) { + forgettingNetwork = ssid + // Remove from cache let known = cacheAdapter.knownNetworks delete known[ssid] @@ -144,6 +149,40 @@ Singleton { forgetProcess.running = true } + // Helper function to immediately update network status + function updateNetworkStatus(ssid, connected) { + let nets = networks + + // Update all networks connected status + for (let key in nets) { + if (nets[key].connected && key !== ssid) { + nets[key].connected = false + } + } + + // Update the target network if it exists + if (nets[ssid]) { + nets[ssid].connected = connected + nets[ssid].existing = true + nets[ssid].cached = true + } else if (connected) { + // Create a temporary entry if network doesn't exist yet + nets[ssid] = { + "ssid": ssid, + "security": "--", + "signal": 100, + "connected"// Default to good signal until real scan + : true, + "existing": true, + "cached": true + } + } + + // Trigger property change notification + networks = ({}) + networks = nets + } + // Helper functions function signalIcon(signal) { if (signal >= 80) @@ -170,9 +209,9 @@ Singleton { stdout: StdioCollector { onStreamFinished: { const connected = text.split("\n").some(line => { - const parts = line.split(":") - return parts[1] === "ethernet" && parts[2] === "connected" - }) + const parts = line.split(":") + return parts[1] === "ethernet" && parts[2] === "connected" + }) if (root.ethernetConnected !== connected) { root.ethernetConnected = connected Logger.log("Network", "Ethernet connected:", root.ethernetConnected) @@ -189,7 +228,7 @@ Singleton { stdout: StdioCollector { onStreamFinished: { const enabled = text.trim() === "enabled" - Logger.log("Network", "Wifi enabled:", enabled) + Logger.log("Network", "Wi-Fi enabled:", enabled) if (Settings.data.network.wifiEnabled !== enabled) { Settings.data.network.wifiEnabled = enabled } @@ -229,11 +268,11 @@ Singleton { id: scanProcess running: false command: ["sh", "-c", ` - # Get existing profiles - profiles=$(nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless' | cut -d: -f1) + # Get list of saved connection profiles (just the names) + profiles=$(nmcli -t -f NAME connection show | tr '\n' '|') # Get WiFi networks - nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list | while read line; do + nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do ssid=$(echo "$line" | cut -d: -f1) security=$(echo "$line" | cut -d: -f2) signal=$(echo "$line" | cut -d: -f3) @@ -244,8 +283,10 @@ Singleton { continue fi + # Check if SSID matches any profile name (simple check) + # This covers most cases where profile name equals or contains the SSID existing=false - if echo "$profiles" | grep -q "^$ssid$"; then + if echo "$profiles" | grep -qF "$ssid|"; then existing=true fi @@ -288,12 +329,24 @@ Singleton { } } - if (JSON.stringify(root.networks) !== JSON.stringify(nets)) { - Logger.log("Network", "Discovered", Object.keys(nets).length, "Wi-Fi networks") + // For logging purpose only + const oldSSIDs = Object.keys(root.networks) + const newSSIDs = Object.keys(nets) + const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid)) + const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid)) + if (newNetworks.length > 0 || lostNetworks.length > 0) { + if (newNetworks.length > 0) { + Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", ")) + } + if (lostNetworks.length > 0) { + Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", ")) + } + Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(nets).length) } + + // Assign the results root.networks = nets root.scanning = false - } } @@ -343,11 +396,14 @@ Singleton { cacheAdapter.lastConnected = connectProcess.ssid saveCache() + // Immediately update the UI before scanning + root.updateNetworkStatus(connectProcess.ssid, true) + root.connecting = false root.connectingTo = "" - Logger.log("Network", "Connected to " + connectProcess.ssid) + Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`) - // Rescan to update status + // Still do a scan to get accurate signal and security info delayedScanTimer.interval = 1000 delayedScanTimer.restart() } @@ -383,8 +439,27 @@ Singleton { running: false command: ["nmcli", "connection", "down", "id", ssid] - onRunningChanged: { - if (!running) { + stdout: StdioCollector { + onStreamFinished: { + Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`) + + // Immediately update UI on successful disconnect + root.updateNetworkStatus(disconnectProcess.ssid, false) + root.disconnectingFrom = "" + + // Do a scan to refresh the list + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.disconnectingFrom = "" + if (text.trim()) { + Logger.warn("Network", "Disconnect error: " + text) + } + // Still trigger a scan even on error delayedScanTimer.interval = 1000 delayedScanTimer.restart() } @@ -395,11 +470,68 @@ Singleton { id: forgetProcess property string ssid: "" running: false - command: ["nmcli", "connection", "delete", "id", ssid] - onRunningChanged: { - if (!running) { - delayedScanTimer.interval = 1000 + // Try multiple common profile name patterns + command: ["sh", "-c", ` + ssid="$1" + deleted=false + + # Try exact SSID match first + if nmcli connection delete id "$ssid" 2>/dev/null; then + echo "Deleted profile: $ssid" + deleted=true + fi + + # Try "Auto " pattern + if nmcli connection delete id "Auto $ssid" 2>/dev/null; then + echo "Deleted profile: Auto $ssid" + deleted=true + fi + + # Try " 1", " 2", etc. patterns + for i in 1 2 3; do + if nmcli connection delete id "$ssid $i" 2>/dev/null; then + echo "Deleted profile: $ssid $i" + deleted=true + fi + done + + if [ "$deleted" = "false" ]; then + echo "No profiles found for SSID: $ssid" + fi + `, "--", ssid] + + stdout: StdioCollector { + onStreamFinished: { + Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`) + Logger.log("Network", text.trim().replace(/[\r\n]/g, " ")) + + // Update both cached and existing status immediately + let nets = root.networks + if (nets[forgetProcess.ssid]) { + nets[forgetProcess.ssid].cached = false + nets[forgetProcess.ssid].existing = false + // Trigger property change + root.networks = ({}) + root.networks = nets + } + + root.forgettingNetwork = "" + + // Quick scan to verify the profile is gone + delayedScanTimer.interval = 500 + delayedScanTimer.restart() + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.forgettingNetwork = "" + if (text.trim() && !text.includes("No profiles found")) { + Logger.warn("Network", "Forget error: " + text) + } + // Still Trigger a scan even on error + delayedScanTimer.interval = 500 delayedScanTimer.restart() } }