Network/WiFi: improve UI with more immediate feedback on operations.
+ proper deletion of profiles when forgetting a network
This commit is contained in:
parent
5bc8f410e7
commit
fc1ee9fb2f
2 changed files with 210 additions and 23 deletions
|
|
@ -215,11 +215,23 @@ NPanel {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2)
|
implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2)
|
||||||
radius: Style.radiusM * scaling
|
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,
|
color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b,
|
||||||
0.05) : Color.mSurface
|
0.05) : Color.mSurface
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
|
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
|
||||||
|
|
||||||
|
// Smooth opacity animation
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: netColumn
|
id: netColumn
|
||||||
width: parent.width - (Style.marginM * scaling * 2)
|
width: parent.width - (Style.marginM * scaling * 2)
|
||||||
|
|
@ -276,8 +288,9 @@ NPanel {
|
||||||
Layout.preferredWidth: Style.marginXXS * scaling
|
Layout.preferredWidth: Style.marginXXS * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the status badges area (around line 237)
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: modelData.connected
|
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
radius: height * 0.5
|
radius: height * 0.5
|
||||||
width: connectedText.implicitWidth + (Style.marginS * scaling * 2)
|
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 {
|
Rectangle {
|
||||||
visible: modelData.cached && !modelData.connected
|
visible: modelData.cached && !modelData.connected
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
|
&& NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
border.color: Color.mOutline
|
border.color: Color.mOutline
|
||||||
border.width: Math.max(1, Style.borderS * scaling)
|
border.width: Math.max(1, Style.borderS * scaling)
|
||||||
|
|
@ -318,6 +365,8 @@ NPanel {
|
||||||
|
|
||||||
NBusyIndicator {
|
NBusyIndicator {
|
||||||
visible: NetworkService.connectingTo === modelData.ssid
|
visible: NetworkService.connectingTo === modelData.ssid
|
||||||
|
|| NetworkService.disconnectingFrom === modelData.ssid
|
||||||
|
|| NetworkService.forgettingNetwork === modelData.ssid
|
||||||
running: visible
|
running: visible
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
size: Style.baseWidgetSize * 0.5 * scaling
|
size: Style.baseWidgetSize * 0.5 * scaling
|
||||||
|
|
@ -326,6 +375,8 @@ NPanel {
|
||||||
NIconButton {
|
NIconButton {
|
||||||
visible: (modelData.existing || modelData.cached) && !modelData.connected
|
visible: (modelData.existing || modelData.cached) && !modelData.connected
|
||||||
&& NetworkService.connectingTo !== modelData.ssid
|
&& NetworkService.connectingTo !== modelData.ssid
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
|
&& NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
icon: "delete"
|
icon: "delete"
|
||||||
tooltipText: "Forget network"
|
tooltipText: "Forget network"
|
||||||
sizeRatio: 0.7
|
sizeRatio: 0.7
|
||||||
|
|
@ -335,6 +386,8 @@ NPanel {
|
||||||
NButton {
|
NButton {
|
||||||
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid
|
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid
|
||||||
&& passwordSsid !== modelData.ssid
|
&& passwordSsid !== modelData.ssid
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
|
&& NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
text: {
|
text: {
|
||||||
if (modelData.existing || modelData.cached)
|
if (modelData.existing || modelData.cached)
|
||||||
return "Connect"
|
return "Connect"
|
||||||
|
|
@ -356,7 +409,7 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
NButton {
|
NButton {
|
||||||
visible: modelData.connected
|
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
text: "Disconnect"
|
text: "Disconnect"
|
||||||
outlined: !hovered
|
outlined: !hovered
|
||||||
fontSize: Style.fontSizeXS * scaling
|
fontSize: Style.fontSizeXS * scaling
|
||||||
|
|
@ -368,7 +421,8 @@ NPanel {
|
||||||
|
|
||||||
// Password input
|
// Password input
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: passwordSsid === modelData.ssid
|
visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: passwordRow.implicitHeight + Style.marginS * scaling * 2
|
height: passwordRow.implicitHeight + Style.marginS * scaling * 2
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
@ -449,7 +503,8 @@ NPanel {
|
||||||
|
|
||||||
// Forget network
|
// Forget network
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: expandedSsid === modelData.ssid
|
visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
|
||||||
|
&& NetworkService.forgettingNetwork !== modelData.ssid
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
|
height: forgetRow.implicitHeight + Style.marginS * 2 * scaling
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ Singleton {
|
||||||
property string connectingTo: ""
|
property string connectingTo: ""
|
||||||
property string lastError: ""
|
property string lastError: ""
|
||||||
property bool ethernetConnected: false
|
property bool ethernetConnected: false
|
||||||
|
property string disconnectingFrom: ""
|
||||||
|
property string forgettingNetwork: ""
|
||||||
|
|
||||||
// Persistent cache
|
// Persistent cache
|
||||||
property string cacheFile: Settings.cacheDir + "network.json"
|
property string cacheFile: Settings.cacheDir + "network.json"
|
||||||
|
|
@ -123,11 +125,14 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnect(ssid) {
|
function disconnect(ssid) {
|
||||||
|
disconnectingFrom = ssid
|
||||||
disconnectProcess.ssid = ssid
|
disconnectProcess.ssid = ssid
|
||||||
disconnectProcess.running = true
|
disconnectProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function forget(ssid) {
|
function forget(ssid) {
|
||||||
|
forgettingNetwork = ssid
|
||||||
|
|
||||||
// Remove from cache
|
// Remove from cache
|
||||||
let known = cacheAdapter.knownNetworks
|
let known = cacheAdapter.knownNetworks
|
||||||
delete known[ssid]
|
delete known[ssid]
|
||||||
|
|
@ -144,6 +149,40 @@ Singleton {
|
||||||
forgetProcess.running = true
|
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
|
// Helper functions
|
||||||
function signalIcon(signal) {
|
function signalIcon(signal) {
|
||||||
if (signal >= 80)
|
if (signal >= 80)
|
||||||
|
|
@ -170,9 +209,9 @@ Singleton {
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
const connected = text.split("\n").some(line => {
|
const connected = text.split("\n").some(line => {
|
||||||
const parts = line.split(":")
|
const parts = line.split(":")
|
||||||
return parts[1] === "ethernet" && parts[2] === "connected"
|
return parts[1] === "ethernet" && parts[2] === "connected"
|
||||||
})
|
})
|
||||||
if (root.ethernetConnected !== connected) {
|
if (root.ethernetConnected !== connected) {
|
||||||
root.ethernetConnected = connected
|
root.ethernetConnected = connected
|
||||||
Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
|
Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
|
||||||
|
|
@ -189,7 +228,7 @@ Singleton {
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
const enabled = text.trim() === "enabled"
|
const enabled = text.trim() === "enabled"
|
||||||
Logger.log("Network", "Wifi enabled:", enabled)
|
Logger.log("Network", "Wi-Fi enabled:", enabled)
|
||||||
if (Settings.data.network.wifiEnabled !== enabled) {
|
if (Settings.data.network.wifiEnabled !== enabled) {
|
||||||
Settings.data.network.wifiEnabled = enabled
|
Settings.data.network.wifiEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
@ -229,11 +268,11 @@ Singleton {
|
||||||
id: scanProcess
|
id: scanProcess
|
||||||
running: false
|
running: false
|
||||||
command: ["sh", "-c", `
|
command: ["sh", "-c", `
|
||||||
# Get existing profiles
|
# Get list of saved connection profiles (just the names)
|
||||||
profiles=$(nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless' | cut -d: -f1)
|
profiles=$(nmcli -t -f NAME connection show | tr '\n' '|')
|
||||||
|
|
||||||
# Get WiFi networks
|
# 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)
|
ssid=$(echo "$line" | cut -d: -f1)
|
||||||
security=$(echo "$line" | cut -d: -f2)
|
security=$(echo "$line" | cut -d: -f2)
|
||||||
signal=$(echo "$line" | cut -d: -f3)
|
signal=$(echo "$line" | cut -d: -f3)
|
||||||
|
|
@ -244,8 +283,10 @@ Singleton {
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check if SSID matches any profile name (simple check)
|
||||||
|
# This covers most cases where profile name equals or contains the SSID
|
||||||
existing=false
|
existing=false
|
||||||
if echo "$profiles" | grep -q "^$ssid$"; then
|
if echo "$profiles" | grep -qF "$ssid|"; then
|
||||||
existing=true
|
existing=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -288,12 +329,24 @@ Singleton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (JSON.stringify(root.networks) !== JSON.stringify(nets)) {
|
// For logging purpose only
|
||||||
Logger.log("Network", "Discovered", Object.keys(nets).length, "Wi-Fi networks")
|
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.networks = nets
|
||||||
root.scanning = false
|
root.scanning = false
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,11 +396,14 @@ Singleton {
|
||||||
cacheAdapter.lastConnected = connectProcess.ssid
|
cacheAdapter.lastConnected = connectProcess.ssid
|
||||||
saveCache()
|
saveCache()
|
||||||
|
|
||||||
|
// Immediately update the UI before scanning
|
||||||
|
root.updateNetworkStatus(connectProcess.ssid, true)
|
||||||
|
|
||||||
root.connecting = false
|
root.connecting = false
|
||||||
root.connectingTo = ""
|
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.interval = 1000
|
||||||
delayedScanTimer.restart()
|
delayedScanTimer.restart()
|
||||||
}
|
}
|
||||||
|
|
@ -383,8 +439,27 @@ Singleton {
|
||||||
running: false
|
running: false
|
||||||
command: ["nmcli", "connection", "down", "id", ssid]
|
command: ["nmcli", "connection", "down", "id", ssid]
|
||||||
|
|
||||||
onRunningChanged: {
|
stdout: StdioCollector {
|
||||||
if (!running) {
|
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.interval = 1000
|
||||||
delayedScanTimer.restart()
|
delayedScanTimer.restart()
|
||||||
}
|
}
|
||||||
|
|
@ -395,11 +470,68 @@ Singleton {
|
||||||
id: forgetProcess
|
id: forgetProcess
|
||||||
property string ssid: ""
|
property string ssid: ""
|
||||||
running: false
|
running: false
|
||||||
command: ["nmcli", "connection", "delete", "id", ssid]
|
|
||||||
|
|
||||||
onRunningChanged: {
|
// Try multiple common profile name patterns
|
||||||
if (!running) {
|
command: ["sh", "-c", `
|
||||||
delayedScanTimer.interval = 1000
|
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()
|
delayedScanTimer.restart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue