Network/WiFi: improve UI with more immediate feedback on operations.

+ proper deletion of profiles when forgetting a network
This commit is contained in:
LemmyCook 2025-09-06 13:03:22 -04:00
parent 5bc8f410e7
commit fc1ee9fb2f
2 changed files with 210 additions and 23 deletions

View file

@ -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

View file

@ -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 <SSID>" pattern
if nmcli connection delete id "Auto $ssid" 2>/dev/null; then
echo "Deleted profile: Auto $ssid"
deleted=true
fi
# Try "<SSID> 1", "<SSID> 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()
}
}