Merge pull request #2109 from turannul/pr/networking-refactor-pt1

Network refactor pt1
This commit is contained in:
Lemmy
2026-03-13 22:12:30 -04:00
committed by GitHub
5 changed files with 1468 additions and 981 deletions
+11 -3
View File
@@ -969,7 +969,8 @@
"hide-unnamed-devices-description": "Hide devices that appear only as Bluetooth addresses.",
"hide-unnamed-devices-label": "Hide unnamed devices",
"pin-instructions": "Please enter the PIN code displayed on your device.",
"title": "Connections"
"title": "Connections",
"wifi-header-text": "Use Wi-Fi to see available networks and connect to the internet."
},
"control-center": {
"cards-desc": "Customize which controls appear in the control center and in what order.",
@@ -1849,7 +1850,8 @@
},
"toast": {
"airplane-mode": {
"title": "Airplane Mode"
"title": "Airplane Mode",
"description": "Disable all wireless communications."
},
"battery": {
"critical": "Critical battery",
@@ -2151,6 +2153,7 @@
"internet-limited": "No internet",
"internet-status": "Internet status",
"ipv4": "IPv4",
"ipv6": "IPv6",
"known-networks": "Known networks",
"link-speed": "Link speed",
"no-ethernet-devices": "No Ethernet devices detected",
@@ -2158,7 +2161,12 @@
"saved": "Saved",
"scan-again": "Scan again",
"searching": "Searching for networks...",
"title": "WiFi"
"title": "WiFi",
"add-hidden-network": "Add hidden network",
"network-name-ssid": "Network name (SSID)",
"security-open": "Open",
"security-wpa": "WPA/WPA2/WPA3 Personal",
"security-wep": "WEP"
},
"signal": {
"excellent": "Excellent",
+12 -106
View File
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "../Settings/Tabs/Connections" as WifiPrefs
import qs.Commons
import qs.Modules.MainScreen
import qs.Modules.Panels.Settings
@@ -15,9 +16,6 @@ SmartPanel {
preferredWidth: Math.round(440 * Style.uiScaleRatio)
preferredHeight: Math.round(500 * Style.uiScaleRatio)
property string passwordSsid: ""
property string expandedSsid: ""
// Info panel collapsed by default, view mode persisted in settings
// Ethernet details UI state (mirrors WiFi info behavior)
property bool ethernetInfoExpanded: false
@@ -30,15 +28,13 @@ SmartPanel {
onPanelViewModeChanged: {
// Persist last view (only after restored the initial value)
if (panelViewPersistEnabled) {
Settings.data.ui.networkPanelView = panelViewMode;
Settings.data.network.networkPanelView = panelViewMode;
}
// Reset transient states to avoid layout artifacts
passwordSsid = "";
expandedSsid = "";
if (panelViewMode === "wifi") {
ethernetInfoExpanded = false;
if (Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0)
if (Settings.data.network.wifiEnabled && !NetworkService.scanning) {
NetworkService.scan();
}
} else {
if (NetworkService.ethernetConnected) {
NetworkService.refreshActiveEthernetDetails();
@@ -48,23 +44,6 @@ SmartPanel {
}
}
// Computed network lists
readonly property var knownNetworks: {
if (!Settings.data.network.wifiEnabled)
return [];
var nets = Object.values(NetworkService.networks);
var known = nets.filter(n => n.connected || n.existing || n.cached);
// Sort: connected first, then by signal strength
known.sort((a, b) => {
if (a.connected !== b.connected)
return b.connected - a.connected;
return b.signal - a.signal;
});
return known;
}
onOpened: {
NetworkService.scan();
// Preload active WiFi details so Info shows instantly
@@ -72,35 +51,23 @@ SmartPanel {
// Also fetch Ethernet details if connected
NetworkService.refreshActiveEthernetDetails();
// Restore last view if valid, otherwise choose what's available (prefer WiFi when both exist)
if (Settings.data.ui.networkPanelView) {
const last = Settings.data.ui.networkPanelView;
if (Settings.data.network.networkPanelView) {
const last = Settings.data.network.networkPanelView;
if (last === "ethernet" && NetworkService.hasEthernet()) {
panelViewMode = "ethernet";
} else {
panelViewMode = "wifi";
}
} else {
if (!Settings.data.network.wifiEnabled && NetworkService.hasEthernet())
if (!Settings.data.network.wifiEnabled && NetworkService.hasEthernet()) {
panelViewMode = "ethernet";
else
} else {
panelViewMode = "wifi";
}
}
panelViewPersistEnabled = true;
}
readonly property var availableNetworks: {
if (!Settings.data.network.wifiEnabled)
return [];
var nets = Object.values(NetworkService.networks);
var available = nets.filter(n => !n.connected && !n.existing && !n.cached);
// Sort by signal strength
available.sort((a, b) => b.signal - a.signal);
return available;
}
panelContent: Rectangle {
color: "transparent"
@@ -153,19 +120,6 @@ SmartPanel {
Layout.fillWidth: true
}
NIconButton {
icon: "refresh"
tooltipText: I18n.tr("common.refresh")
baseSize: Style.baseWidgetSize * 0.8
enabled: panelViewMode === "wifi" ? (Settings.data.network.wifiEnabled && !NetworkService.scanning) : true
onClicked: {
if (panelViewMode === "wifi")
NetworkService.scan();
else
NetworkService.refreshEthernet();
}
}
NToggle {
id: wifiSwitch
visible: panelViewMode === "wifi"
@@ -392,13 +346,6 @@ SmartPanel {
Layout.alignment: Qt.AlignHCenter
}
NButton {
text: I18n.tr("wifi.panel.scan-again")
icon: "refresh"
Layout.alignment: Qt.AlignHCenter
onClicked: NetworkService.scan()
}
Item {
Layout.fillHeight: true
}
@@ -412,48 +359,8 @@ SmartPanel {
width: parent.width
spacing: Style.marginM
WiFiNetworksList {
label: I18n.tr("wifi.panel.known-networks")
model: root.knownNetworks
passwordSsid: root.passwordSsid
expandedSsid: root.expandedSsid
onPasswordRequested: ssid => {
root.passwordSsid = ssid;
root.expandedSsid = "";
}
onPasswordSubmitted: (ssid, password) => {
NetworkService.connect(ssid, password);
root.passwordSsid = "";
}
onPasswordCancelled: root.passwordSsid = ""
onForgetRequested: ssid => root.expandedSsid = root.expandedSsid === ssid ? "" : ssid
onForgetConfirmed: ssid => {
NetworkService.forget(ssid);
root.expandedSsid = "";
}
onForgetCancelled: root.expandedSsid = ""
}
WiFiNetworksList {
label: I18n.tr("wifi.panel.available-networks")
model: root.availableNetworks
passwordSsid: root.passwordSsid
expandedSsid: root.expandedSsid
onPasswordRequested: ssid => {
root.passwordSsid = ssid;
root.expandedSsid = "";
}
onPasswordSubmitted: (ssid, password) => {
NetworkService.connect(ssid, password);
root.passwordSsid = "";
}
onPasswordCancelled: root.passwordSsid = ""
onForgetRequested: ssid => root.expandedSsid = root.expandedSsid === ssid ? "" : ssid
onForgetConfirmed: ssid => {
NetworkService.forget(ssid);
root.expandedSsid = "";
}
onForgetCancelled: root.expandedSsid = ""
WifiPrefs.WifiSubTab {
showOnlyLists: true
}
}
@@ -526,8 +433,7 @@ SmartPanel {
radius: Style.radiusM
border.width: Style.borderS
border.color: modelData.connected ? Color.mPrimary : Color.mOutline
color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface
color: modelData.connected ? Qt.alpha(Color.mPrimary, Math.min(1.15 - Color.panelBackgroundOpacity, 0.75)) : Color.mSurface
ColumnLayout {
id: ethItemColumn
width: parent.width - Style.margin2M
-710
View File
@@ -1,710 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
NBox {
id: root
property string label: ""
property var model: []
// While a password prompt is open, we freeze the displayed model to avoid
// frequent scan updates recreating items and clearing the TextInput.
property var cachedModel: []
readonly property var displayModel: (passwordSsid && passwordSsid.length > 0) ? cachedModel : model
property string passwordSsid: ""
property string expandedSsid: ""
// Currently expanded info panel for a connected SSID
property string infoSsid: ""
// Local layout toggle for details: true = grid (2 cols), false = rows (1 col)
// Persisted under Settings.data.network.wifiDetailsViewMode
property bool detailsGrid: (Settings.data.network.wifiDetailsViewMode === "grid")
signal passwordRequested(string ssid)
signal passwordSubmitted(string ssid, string password)
signal passwordCancelled
signal forgetRequested(string ssid)
signal forgetConfirmed(string ssid)
signal forgetCancelled
onPasswordSsidChanged: {
if (passwordSsid && passwordSsid.length > 0) {
// Freeze current list ordering/content while entering password
try {
// Deep copy to decouple from live updates
cachedModel = JSON.parse(JSON.stringify(model));
} catch (e) {
// Fallback to shallow copy
cachedModel = model.slice ? model.slice() : model;
}
} else {
// Clear freeze when password box is closed
cachedModel = [];
}
}
Layout.fillWidth: true
Layout.preferredHeight: column.implicitHeight + Style.margin2M
visible: root.model.length > 0
ColumnLayout {
id: column
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
visible: root.model.length > 0
Layout.leftMargin: Style.marginS
spacing: Style.marginS
NLabel {
label: root.label
Layout.fillWidth: true
}
}
Repeater {
model: root.displayModel
NBox {
id: networkItem
Layout.fillWidth: true
Layout.leftMargin: Style.marginXS
Layout.rightMargin: Style.marginXS
implicitHeight: netColumn.implicitHeight + Style.margin2M
opacity: (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0
color: modelData.connected ? Qt.alpha(Color.mPrimary, Math.min(1.15 - Color.panelBackgroundOpacity, 0.75)) : Color.mSurface
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
}
}
ColumnLayout {
id: netColumn
width: parent.width - Style.margin2M
x: Style.marginM
y: Style.marginM
spacing: Style.marginS
// Main row
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS
NIcon {
icon: NetworkService.signalIcon(modelData.signal, modelData.connected)
pointSize: Style.fontSizeXXL
color: modelData.connected ? (NetworkService.internetConnectivity ? Color.mPrimary : Color.mError) : Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, NetworkService.getSignalStrengthLabel(modelData.signal) + " (" + modelData.signal + "%)")
onExited: TooltipService.hide()
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
NText {
text: modelData.ssid
pointSize: Style.fontSizeM
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
color: Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginXS
NText {
text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
NIcon {
icon: "lock-off"
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
visible: !NetworkService.isSecured(modelData.security)
}
// Spacing
Item {
Layout.preferredWidth: Style.marginXXS
}
// Status badges
Rectangle {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
color: NetworkService.internetConnectivity ? Color.mPrimary : Color.mError
radius: height * 0.5
width: Math.round(connectedText.implicitWidth + Style.margin2S)
height: Math.round(connectedText.implicitHeight + Style.margin2XXS)
NText {
id: connectedText
anchors.centerIn: parent
text: {
switch (NetworkService.networkConnectivity) {
case "full":
return I18n.tr("common.connected");
case "limited":
return I18n.tr("wifi.panel.internet-limited");
case "portal": // Where Captive Portal is detected (User intervention needed)
return I18n.tr("wifi.panel.action-required");
// I assume unknown is for connecting/disconnecting state where connectivity hasn't been determined yet (Shouldn't be visible for long enough to matter)
// and none is for no connectivity at all.
// None and Unknown will return direct output of NetworkService.networkConnectivity
default:
return NetworkService.networkConnectivity;
}
}
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.disconnectingFrom === modelData.ssid
color: Color.mError
radius: height * 0.5
width: Math.round(disconnectingText.implicitWidth + Style.margin2S)
height: Math.round(disconnectingText.implicitHeight + Style.margin2XXS)
NText {
id: disconnectingText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.disconnecting")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: NetworkService.forgettingNetwork === modelData.ssid
color: Color.mError
radius: height * 0.5
width: Math.round(forgettingText.implicitWidth + Style.margin2S)
height: Math.round(forgettingText.implicitHeight + Style.margin2XXS)
NText {
id: forgettingText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.forgetting")
pointSize: Style.fontSizeXXS
color: Color.mOnPrimary
}
}
Rectangle {
visible: modelData.cached && !modelData.connected && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
color: "transparent"
border.color: Color.mOutline
border.width: Style.borderS
radius: height * 0.5
width: savedText.implicitWidth + Style.margin2S
height: savedText.implicitHeight + (Style.margin2XXS)
NText {
id: savedText
anchors.centerIn: parent
text: I18n.tr("wifi.panel.saved")
pointSize: Style.fontSizeXXS
color: Color.mOnSurfaceVariant
}
}
}
}
// Action area
RowLayout {
spacing: Style.marginS
NBusyIndicator {
visible: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid
running: visible
color: Color.mPrimary
size: Style.baseWidgetSize * 0.5
}
// Info toggle for connected network
NIconButton {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
icon: "info"
tooltipText: I18n.tr("common.info")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (root.infoSsid === modelData.ssid) {
root.infoSsid = "";
} else {
root.infoSsid = modelData.ssid;
NetworkService.refreshActiveWifiDetails();
}
}
}
NIconButton {
visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
icon: "trash"
tooltipText: I18n.tr("tooltips.forget-network")
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.forgetRequested(modelData.ssid)
}
NButton {
visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && root.passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
text: {
if (modelData.existing || modelData.cached)
return I18n.tr("common.connect");
if (!NetworkService.isSecured(modelData.security))
return I18n.tr("common.connect");
return I18n.tr("common.password");
}
outlined: !hovered
fontSize: Style.fontSizeS
enabled: !NetworkService.connecting
onClicked: {
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
NetworkService.connect(modelData.ssid);
} else {
root.passwordRequested(modelData.ssid);
}
}
}
NButton {
visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid
text: I18n.tr("common.disconnect")
outlined: !hovered
fontSize: Style.fontSizeS
backgroundColor: Color.mError
onClicked: NetworkService.disconnect(modelData.ssid)
}
}
}
// Connection info details (compact grid)
Rectangle {
visible: root.infoSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
color: Color.mSurfaceVariant
radius: Style.radiusS
border.width: Style.borderS
border.color: Color.mOutline
implicitHeight: infoGrid.implicitHeight + Style.margin2S
clip: true
onVisibleChanged: {
if (visible && infoGrid && infoGrid.forceLayout) {
Qt.callLater(function () {
infoGrid.forceLayout();
});
}
}
// Grid/List toggle moved here to the top-right corner of the info box
NIconButton {
id: detailsToggle
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS
// Use Tabler layout icons; "grid" alone doesn't exist in our font
icon: root.detailsGrid ? "layout-list" : "layout-grid"
tooltipText: root.detailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.detailsGrid = !root.detailsGrid;
if (Settings.data && Settings.data.ui) {
Settings.data.network.wifiDetailsViewMode = root.detailsGrid ? "grid" : "list";
}
}
z: 1
}
GridLayout {
id: infoGrid
anchors.fill: parent
anchors.margins: Style.marginS
// Layout toggle: grid (2 columns) or rows (1 column)
columns: root.detailsGrid ? 2 : 1
columnSpacing: Style.marginM
rowSpacing: Style.marginXS
// Ensure proper relayout when switching grid/list while open
onColumnsChanged: {
if (infoGrid.forceLayout) {
Qt.callLater(function () {
infoGrid.forceLayout();
});
}
}
// Icons only; values have labels as tooltips on hover
// --- Item 1: Interface ---
// Grid: Row 0, Col 0 | List: Row 0
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.row: 0
Layout.column: 0
spacing: Style.marginXS
NIcon {
icon: "network"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
// Tooltip on hover when using icons-only mode
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.interface"))
onExited: TooltipService.hide()
}
}
NText {
text: NetworkService.activeWifiIf || "-"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.alignment: Qt.AlignVCenter
wrapMode: root.detailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
elide: root.detailsGrid ? Text.ElideRight : Text.ElideNone
maximumLineCount: root.detailsGrid ? 1 : 6
clip: true
// Click-to-copy WiFi interface name
MouseArea {
anchors.fill: parent
enabled: (NetworkService.activeWifiIf && NetworkService.activeWifiIf.length > 0)
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address"))
onExited: TooltipService.hide()
onClicked: {
const value = NetworkService.activeWifiIf || "";
if (value.length > 0) {
Quickshell.execDetached(["wl-copy", value]);
ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.bluetooth.address-copied"), "wifi");
}
}
}
}
}
// --- Item 2: Frequency (Band) ---
// Grid: Row 1, Col 0 | List: Row 1
RowLayout {
Layout.fillWidth: true
Layout.row: 1
Layout.column: 0
spacing: Style.marginXS
NIcon {
icon: "router"
pointSize: Style.fontSizeXS
color: NetworkService.internetConnectivity ? Color.mOnSurface : Color.mError
Layout.alignment: Qt.AlignVCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("common.frequency"))
onExited: TooltipService.hide()
}
}
NText {
text: NetworkService.activeWifiDetails.band || "-"
pointSize: Style.fontSizeXS
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
wrapMode: root.detailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
elide: root.detailsGrid ? Text.ElideRight : Text.ElideNone
maximumLineCount: root.detailsGrid ? 1 : 6
clip: true
}
}
// --- Item 3: Link Speed ---
// Grid: Row 2, Col 0 | List: Row 2
RowLayout {
Layout.fillWidth: true
Layout.row: 2
Layout.column: 0
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.activeWifiDetails.rateShort && NetworkService.activeWifiDetails.rateShort.length > 0) ? NetworkService.activeWifiDetails.rateShort : ((NetworkService.activeWifiDetails.rate && NetworkService.activeWifiDetails.rate.length > 0) ? NetworkService.activeWifiDetails.rate : "-")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
wrapMode: root.detailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
elide: root.detailsGrid ? Text.ElideRight : Text.ElideNone
maximumLineCount: root.detailsGrid ? 1 : 6
clip: true
}
}
// --- Item 4: Gateway ---
// Grid: Row 2, Col 1 | List: Row 5 (Last)
RowLayout {
Layout.fillWidth: true
Layout.row: root.detailsGrid ? 2 : 5
Layout.column: root.detailsGrid ? 1 : 0
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, I18n.tr("common.gateway"))
onExited: TooltipService.hide()
}
}
NText {
text: NetworkService.activeWifiDetails.gateway4 || "-"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
wrapMode: root.detailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
elide: root.detailsGrid ? Text.ElideRight : Text.ElideNone
maximumLineCount: root.detailsGrid ? 1 : 6
clip: true
}
}
// --- Item 5: IPv4 ---
// Grid: Row 0, Col 1 | List: Row 3
RowLayout {
Layout.fillWidth: true
Layout.row: root.detailsGrid ? 0 : 3
Layout.column: root.detailsGrid ? 1 : 0
spacing: Style.marginXS
NIcon {
// IPv4 address icon ("device-lan" doesn't exist in our font)
icon: "network"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.ipv4"))
onExited: TooltipService.hide()
}
}
NText {
text: NetworkService.activeWifiDetails.ipv4 || "-"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
wrapMode: root.detailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
elide: root.detailsGrid ? Text.ElideRight : Text.ElideNone
maximumLineCount: root.detailsGrid ? 1 : 6
clip: true
// Click-to-copy WiFi IPv4 address
MouseArea {
anchors.fill: parent
enabled: (NetworkService.activeWifiDetails.ipv4 && NetworkService.activeWifiDetails.ipv4.length > 0)
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address"))
onExited: TooltipService.hide()
onClicked: {
const value = NetworkService.activeWifiDetails.ipv4 || "";
if (value.length > 0) {
Quickshell.execDetached(["wl-copy", value]);
ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.bluetooth.address-copied"), "wifi");
}
}
}
}
}
// --- Item 6: DNS ---
// Grid: Row 1, Col 1 | List: Row 4
RowLayout {
Layout.fillWidth: true
Layout.row: root.detailsGrid ? 1 : 4
Layout.column: root.detailsGrid ? 1 : 0
spacing: Style.marginXS
// DNS: allow wrapping when selected
NIcon {
icon: "world"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.dns"))
onExited: TooltipService.hide()
}
}
NText {
text: NetworkService.activeWifiDetails.dns || "-"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
wrapMode: root.detailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
elide: root.detailsGrid ? Text.ElideRight : Text.ElideNone
maximumLineCount: root.detailsGrid ? 1 : 6
clip: true
}
}
}
}
// Password input
Rectangle {
visible: root.passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: passwordRow.implicitHeight + Style.margin2S
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
radius: Style.iRadiusXS
RowLayout {
id: passwordRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginM
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Style.iRadiusXS
color: Color.mSurface
border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Style.borderS
TextInput {
id: pwdInput
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginS
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS
color: Color.mOnSurface
echoMode: TextInput.Password
selectByMouse: true
focus: visible
passwordCharacter: "●"
onVisibleChanged: if (visible) {
// Keep any text already typed; only focus
forceActiveFocus();
}
onAccepted: {
if (text && !NetworkService.connecting) {
root.passwordSubmitted(modelData.ssid, text);
}
}
NText {
visible: parent.text.length === 0
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("wifi.panel.enter-password")
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeS
}
}
}
NButton {
text: I18n.tr("common.connect")
fontSize: Style.fontSizeS
enabled: pwdInput.text.length > 0 && !NetworkService.connecting
outlined: true
onClicked: root.passwordSubmitted(modelData.ssid, pwdInput.text)
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.passwordCancelled()
}
}
}
// Forget network
Rectangle {
visible: root.expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
Layout.fillWidth: true
height: forgetRow.implicitHeight + Style.margin2S
color: Color.mSurfaceVariant
radius: Style.radiusS
border.width: Style.borderS
border.color: Color.mOutline
RowLayout {
id: forgetRow
anchors.fill: parent
anchors.margins: Style.marginS
spacing: Style.marginM
RowLayout {
NIcon {
icon: "trash"
pointSize: Style.fontSizeL
color: Color.mError
}
NText {
text: I18n.tr("wifi.panel.forget-network")
pointSize: Style.fontSizeS
color: Color.mError
Layout.fillWidth: true
}
}
NButton {
id: forgetButton
text: I18n.tr("wifi.panel.forget")
fontSize: Style.fontSizeXXS
backgroundColor: Color.mError
outlined: forgetButton.hovered ? false : true
onClicked: root.forgetConfirmed(modelData.ssid)
}
NIconButton {
icon: "close"
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.forgetCancelled()
}
}
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+355 -138
View File
@@ -39,6 +39,42 @@ Singleton {
property bool ignoreScanResults: false
property bool scanPending: false
// Supported Wi-Fi security types
property var supportedSecurityTypes: [
{
key: "open",
name: I18n.tr("wifi.panel.security-open")
},
{
key: "wep",
name: "WEP"
},
{
key: "wpa-psk",
name: "WPA"
},
{
key: "wpa2-psk",
name: "WPA2/WPA3"
},
{
key: "sae",
name: "WPA3"
},
{
key: "wpa-eap",
name: "WPA Enterprise"
},
{
key: "wpa2-eap",
name: "WPA2 Enterprise"
},
{
key: "wpa3-eap",
name: "WPA3 Enterprise"
}
]
// Active WiFi connection details (for info panel)
property var activeWifiDetails: ({})
property string activeWifiIf: ""
@@ -136,33 +172,8 @@ Singleton {
// Function to detect host's networking capabilities eg has WiFi/Ethernet.
function detectNetworkCapabilities() {
if (ProgramCheckerService.nmcliAvailable) {
Logger.d("Network", "Starting network capability detection...");
capabilityDetectProcess.running = true;
}
}
// Process to detect host's networking capabilities
Process {
id: capabilityDetectProcess
running: false
command: ["nmcli", "-t", "-f", "TYPE", "device"]
stdout: StdioCollector {
onStreamFinished: {
var lines = text.trim().split("\n");
var wifi = false;
var eth = false;
for (var i = 0; i < lines.length; i++) {
var type = lines[i].trim();
if (type === "wifi") {
wifi = true;
} else if (type === "ethernet") {
eth = true;
}
}
root._wifiAvailable = wifi;
root._ethernetAvailable = eth;
Logger.d("Network", "Detected capabilities - WiFi:", wifi, "Ethernet:", eth);
}
Logger.d("Network", "Refreshing network status and capabilities...");
ethernetStateProcess.running = true;
}
}
@@ -182,8 +193,9 @@ Singleton {
}
// Use cached details if they are fresh
if (activeWifiIf && activeWifiDetails && (now - activeWifiDetailsTimestamp) < activeWifiDetailsTtlMs)
if (activeWifiIf && activeWifiDetails && (now - activeWifiDetailsTimestamp) < activeWifiDetailsTtlMs) {
return;
}
detailsLoading = true;
wifiDeviceListProcess.running = true;
@@ -224,15 +236,15 @@ Singleton {
return;
}
// If we have fresh details for the same iface, skip
if (activeEthernetIf && activeEthernetDetails && (now - activeEthernetDetailsTimestamp) < activeEthernetDetailsTtlMs)
if (activeEthernetIf && activeEthernetDetails && (now - activeEthernetDetailsTimestamp) < activeEthernetDetailsTtlMs) {
return;
}
ethernetDetailsLoading = true;
ethernetDeviceListProcess.running = true;
}
// Internet connectivity check timer
// Runs every 15s if nmcli is available
Timer {
id: connectivityCheckTimer
interval: 15000
@@ -293,7 +305,7 @@ Singleton {
refreshActiveEthernetDetails();
}
function connect(ssid, password = "") {
function connect(ssid, password = "", isHidden = false) {
if (!ProgramCheckerService.nmcliAvailable || connecting) {
return;
}
@@ -306,15 +318,31 @@ Singleton {
connectProcess.mode = "saved";
connectProcess.ssid = ssid;
connectProcess.password = "";
connectProcess.isHidden = false;
} else {
connectProcess.mode = "new";
connectProcess.ssid = ssid;
connectProcess.password = password;
connectProcess.isHidden = isHidden;
}
connectProcess.running = true;
}
function connectManual(ssid, password, securityKey) {
if (!ProgramCheckerService.nmcliAvailable || connecting) {
return;
}
connecting = true;
connectingTo = ssid;
lastError = "";
manualConnectProcess.ssid = ssid;
manualConnectProcess.password = password;
manualConnectProcess.securityKey = securityKey;
manualConnectProcess.running = true;
}
function disconnect(ssid) {
if (!ProgramCheckerService.nmcliAvailable) {
return;
@@ -416,6 +444,55 @@ Singleton {
}
}
function parseIpDetails(text) {
const details = {
ipv4: "",
gateway4: "",
ipv6: "",
gateway6: "",
dns4: [],
dns6: [],
dns: "",
connectionName: ""
};
const lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) {
continue;
}
const idx = line.indexOf(":");
if (idx === -1) {
continue;
}
const key = line.substring(0, idx);
const val = line.substring(idx + 1);
if (key === "GENERAL.CONNECTION") {
details.connectionName = val;
} else if (key.indexOf("IP4.ADDRESS") === 0) {
details.ipv4 = val.split("/")[0];
} else if (key === "IP4.GATEWAY") {
details.gateway4 = val;
} else if (key.indexOf("IP6.ADDRESS") === 0) {
details.ipv6 = val.split("/")[0];
} else if (key === "IP6.GATEWAY") {
details.gateway6 = val;
} else if (key.indexOf("IP4.DNS") === 0) {
if (val && details.dns4.indexOf(val) === -1) {
details.dns4.push(val);
}
} else if (key.indexOf("IP6.DNS") === 0) {
if (val && details.dns6.indexOf(val) === -1) {
details.dns6.push(val);
}
}
}
details.dns4 = details.dns4.join(", ");
details.dns6 = details.dns6.join(", ");
details.dns = [].concat(details.dns4 ? [details.dns4] : [], details.dns6 ? [details.dns6] : []).join(", ");
return details;
}
// Processes
Process {
id: ethernetStateProcess
@@ -425,30 +502,45 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
var connected = false;
var wifiAvailable = false;
var ethernetAvailable = false;
var devIf = "";
var lines = text.split("\n");
var lines = text.trim().split("\n");
var ethList = [];
for (var i = 0; i < lines.length; i++) {
var parts = lines[i].split(":");
if (parts.length >= 3 && parts[1] === "ethernet") {
if (parts.length >= 3) {
var ifname = parts[0];
var type = parts[1];
var state = parts[2];
var isConn = state === "connected";
ethList.push({
ifname: ifname,
state: state,
connected: isConn
});
if (isConn && !connected) {
connected = true;
devIf = ifname;
if (type === "wifi") {
wifiAvailable = true;
} else if (type === "ethernet" && state !== "unmanaged") {
ethernetAvailable = true;
var isConn = state === "connected";
ethList.push({
ifname: ifname,
state: state,
connected: isConn
});
if (isConn && !connected) {
connected = true;
devIf = ifname;
}
}
}
}
// Update capabilities
root._wifiAvailable = wifiAvailable;
root._ethernetAvailable = ethernetAvailable;
// Sort interfaces: connected first, then by name
ethList.sort(function (a, b) {
if (a.connected !== b.connected)
if (a.connected !== b.connected) {
return a.connected ? -1 : 1;
}
return a.ifname.localeCompare(b.ifname);
});
root.ethernetInterfaces = ethList;
@@ -497,6 +589,9 @@ Singleton {
const dev = parts[0];
const type = parts[1];
const state = parts[2];
if (state === "unmanaged") {
continue;
}
if (type === "ethernet" && state === "connected") {
ifname = dev;
}
@@ -510,8 +605,9 @@ Singleton {
}
}
ethList.sort(function (a, b) {
if (a.connected !== b.connected)
if (a.connected !== b.connected) {
return a.connected ? -1 : 1;
}
return a.ifname.localeCompare(b.ifname);
});
root.ethernetInterfaces = ethList;
@@ -551,42 +647,20 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
const details = root.activeEthernetDetails || ({});
let connName = "";
let ipv4 = "";
let gw4 = "";
let dnsServers = [];
const lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) {
continue;
}
const idx = line.indexOf(":");
if (idx === -1) {
continue;
}
const key = line.substring(0, idx);
const val = line.substring(idx + 1);
if (key === "GENERAL.CONNECTION") {
connName = val;
} else if (key.indexOf("IP4.ADDRESS") === 0) {
ipv4 = val.split("/")[0];
} else if (key === "IP4.GATEWAY") {
gw4 = val;
} else if (key.indexOf("IP4.DNS") === 0) {
if (val && dnsServers.indexOf(val) === -1) {
dnsServers.push(val);
}
}
}
const parsed = root.parseIpDetails(text);
details.ifname = ethernetDeviceShowProcess.ifname;
details.connectionName = connName;
details.connectionName = parsed.connectionName;
// No speed from nmcli: keep empty so ethtool fallback below fills it
details.speed = details.speed && details.speed.length > 0 ? details.speed : "";
details.ipv4 = ipv4;
details.gateway4 = gw4;
details.dnsServers = dnsServers;
details.dns = dnsServers.join(", ");
details.ipv4 = parsed.ipv4;
details.gateway4 = parsed.gateway4;
details.ipv6 = parsed.ipv6;
details.gateway6 = parsed.gateway6;
details.dns4 = parsed.dns4;
details.dns6 = parsed.dns6;
details.dns = parsed.dns;
root.activeEthernetDetails = details;
// If speed missing, try sysfs first, then fallback to ethtool
if (!details.speed || details.speed.length === 0) {
@@ -696,6 +770,9 @@ Singleton {
const dev = parts[0];
const type = parts[1];
const state = parts[2];
if (state === "unmanaged") {
continue;
}
if (type === "wifi" && state === "connected") {
ifname = dev;
break;
@@ -727,45 +804,25 @@ Singleton {
}
}
// Fetch IPv4 and gateway for the interface
// Fetch IP info for the interface
Process {
id: wifiDeviceShowProcess
property string ifname: ""
running: false
command: ["nmcli", "-t", "-f", "IP4.ADDRESS,IP4.GATEWAY,IP4.DNS", "device", "show", ifname]
command: ["nmcli", "-t", "-f", "IP4.ADDRESS,IP4.GATEWAY,IP4.DNS,IP6.ADDRESS,IP6.GATEWAY,IP6.DNS", "device", "show", ifname]
stdout: StdioCollector {
onStreamFinished: {
const details = root.activeWifiDetails || ({});
let ipv4 = "";
let gw4 = "";
let dnsServers = [];
const lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) {
continue;
}
const idx = line.indexOf(":");
if (idx === -1) {
continue;
}
const key = line.substring(0, idx);
const val = line.substring(idx + 1);
if (key.indexOf("IP4.ADDRESS") === 0) {
ipv4 = val.split("/")[0];
} else if (key === "IP4.GATEWAY") {
gw4 = val;
} else if (key.indexOf("IP4.DNS") === 0) {
if (val && dnsServers.indexOf(val) === -1) {
dnsServers.push(val);
}
}
}
details.ipv4 = ipv4;
details.gateway4 = gw4;
details.dnsServers = dnsServers;
details.dns = dnsServers.join(", ");
const parsed = root.parseIpDetails(text);
details.ipv4 = parsed.ipv4;
details.gateway4 = parsed.gateway4;
details.ipv6 = parsed.ipv6;
details.gateway6 = parsed.gateway6;
details.dns4 = parsed.dns4;
details.dns6 = parsed.dns6;
details.dns = parsed.dns;
root.activeWifiDetails = details;
// Try to get link rate (best effort)
@@ -784,18 +841,20 @@ Singleton {
}
}
// Optional: query WiFi bitrate via iw if available
// Optional: query WiFi bitrate and link info via iw if available
Process {
id: wifiIwLinkProcess
property string ifname: ""
running: false
command: ["sh", "-c", "iw dev '" + ifname + "' link 2>/dev/null || true"]
command: ["sh", "-c", "iw dev '" + ifname + "' link 2>/dev/null; iw dev '" + ifname + "' info 2>/dev/null || true"]
stdout: StdioCollector {
onStreamFinished: {
const details = root.activeWifiDetails || ({});
let rate = "";
let freq = "";
let iwChannel = "";
let iwWidth = "";
const lines = text.split("\n");
for (var k = 0; k < lines.length; k++) {
var line2 = lines[k].trim();
@@ -804,12 +863,23 @@ Singleton {
rate = line2.substring(11).trim();
} else if (low.indexOf("freq:") === 0) {
freq = line2.substring(5).trim();
} else if (low.indexOf("channel") === 0) {
// Parse "channel 9 (2452 MHz), width: 20 MHz, center1: 2452 MHz"
var chanMatch = line2.match(/channel\s+(\d+)/i);
if (chanMatch) {
iwChannel = chanMatch[1];
}
var widthMatchInfo = line2.match(/width:\s+(\d+)\s+MHz/i);
if (widthMatchInfo) {
iwWidth = widthMatchInfo[1] + " MHz";
}
}
}
// Determine band from frequency
// Determine band and channel from frequency
// https://en.wikipedia.org/wiki/List_of_WLAN_channels
let band = "";
let channel = iwChannel;
if (freq) {
const f = +freq;
if (f) {
@@ -817,14 +887,27 @@ Singleton {
// https://en.wikipedia.org/wiki/List_of_WLAN_channels#6_GHz_(802.11ax_and_802.11be)
case (f >= 5925 && f < 7125):
band = "6 GHz";
if (!channel) {
channel = Math.round((f - 5940) / 5).toString();
}
break;
// https://en.wikipedia.org/wiki/List_of_WLAN_channels#5_GHz_(802.11a/h/n/ac/ax/be)
case (f >= 5150 && f < 5925):
band = "5 GHz";
if (!channel) {
channel = Math.round((f - 5000) / 5).toString();
}
break;
// https://en.wikipedia.org/wiki/List_of_WLAN_channels#2.4_GHz_(802.11b/g/n/ax/be)
case (f >= 2400 && f < 2500):
band = "2.4 GHz";
if (!channel) {
if (f === 2484) {
channel = "14";
} else {
channel = Math.round((f - 2407) / 5).toString();
}
}
break;
default:
band = `${f} MHz`;
@@ -834,7 +917,16 @@ Singleton {
// Shorten verbose bitrate strings like: "360.0 MBit/s VHT-MCS 8 40MHz short GI"
let rateShort = "";
let width = iwWidth;
if (rate) {
// Extract width from bitrate if not already found in info (fallback)
if (!width) {
var widthMatchBitrate = rate.match(/(\d+)MHz/i);
if (widthMatchBitrate) {
width = widthMatchBitrate[1] + " MHz";
}
}
var parts = rate.trim().split(" ");
// compact consecutive spaces
var compact = [];
@@ -866,9 +958,20 @@ Singleton {
rateShort = compact.slice(0, 2).join(" ");
}
}
// Enhance band string with channel and width: "Band / Ch Channel (Width)"
let enhancedBand = band;
if (channel && width) {
enhancedBand = `${band} / Ch ${channel} (${width})`;
} else if (channel) {
enhancedBand = `${band} / Ch ${channel}`;
}
details.rate = rate;
details.rateShort = rateShort;
details.band = band;
details.band = enhancedBand;
details.channel = channel;
details.width = width;
root.activeWifiDetails = details;
root.activeWifiDetailsTimestamp = Date.now();
root.detailsLoading = false;
@@ -891,6 +994,9 @@ Singleton {
id: wifiStateProcess
running: false
command: ["nmcli", "radio", "wifi"]
environment: ({
"LC_ALL": "C"
})
stdout: StdioCollector {
onStreamFinished: {
@@ -1210,6 +1316,7 @@ Singleton {
property string mode: "new"
property string ssid: ""
property string password: ""
property bool isHidden: false
running: false
command: {
@@ -1217,9 +1324,15 @@ Singleton {
return ["nmcli", "connection", "up", "id", ssid];
} else {
var cmd = ["nmcli", "device", "wifi", "connect", ssid];
if (isHidden) {
cmd.push("hidden", "yes");
}
if (password) {
cmd.push("password", password);
}
if (root.activeWifiIf) {
cmd.push("ifname", root.activeWifiIf);
}
return cmd;
}
}
@@ -1271,10 +1384,10 @@ Singleton {
stderr: StdioCollector {
onStreamFinished: {
root.connecting = false;
root.connectingTo = "";
if (text.trim()) {
root.connecting = false;
root.connectingTo = "";
// Parse common errors
if (text.indexOf("Secrets were required") !== -1 || text.indexOf("no secrets provided") !== -1) {
root.lastError = I18n.tr("toast.wifi.incorrect-password");
@@ -1296,6 +1409,96 @@ Singleton {
}
}
Process {
id: manualConnectProcess
property string ssid: ""
property string password: ""
property string securityKey: ""
running: false
command: {
var script = `
SSID="$1"
PWD="$2"
SEC="$3"
UUID=$(nmcli -t -f UUID,TYPE,NAME connection show | grep ":802-11-wireless:$SSID$" | head -n1 | cut -d: -f1)
if [ -n "$UUID" ]; then
nmcli connection delete uuid "$UUID" 2>/dev/null || true
fi
case "$SEC" in
wpa-psk|wpa2-psk)
nmcli connection add type wifi con-name "$SSID" ssid "$SSID" -- wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$PWD" 802-11-wireless.hidden yes
;;
sae)
nmcli connection add type wifi con-name "$SSID" ssid "$SSID" -- wifi-sec.key-mgmt sae wifi-sec.psk "$PWD" 802-11-wireless.hidden yes
;;
wep)
nmcli connection add type wifi con-name "$SSID" ssid "$SSID" -- wifi-sec.key-mgmt none wifi-sec.wep-key0 "$PWD" 802-11-wireless.hidden yes
;;
*-eap)
# Enterprise not fully supported in Stage 1
echo "Enterprise networks not supported yet"
exit 1
;;
*)
nmcli connection add type wifi con-name "$SSID" ssid "$SSID" -- 802-11-wireless.hidden yes
;;
esac
nmcli connection up id "$SSID"
`;
return ["sh", "-c", script, "--", ssid, password, securityKey];
}
environment: ({
"LC_ALL": "C"
})
stdout: StdioCollector {
onStreamFinished: {
const output = text.trim();
if (output.indexOf("successfully activated") === -1) {
return;
}
// Success - update cache
let known = cacheAdapter.knownNetworks;
known[manualConnectProcess.ssid] = {
"profileName": manualConnectProcess.ssid,
"lastConnected": Date.now()
};
cacheAdapter.knownNetworks = known;
cacheAdapter.lastConnected = manualConnectProcess.ssid;
saveCache();
root.updateNetworkStatus(manualConnectProcess.ssid, true);
root.refreshActiveWifiDetails();
root.connecting = false;
root.connectingTo = "";
Logger.i("Network", "Manually connected to hidden network: '" + manualConnectProcess.ssid + "'");
ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.wifi.connected", {
"ssid": manualConnectProcess.ssid
}), "wifi");
delayedScanTimer.interval = 5000;
delayedScanTimer.restart();
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.connecting = false;
root.connectingTo = "";
root.lastError = I18n.tr("toast.wifi.connection-failed");
Logger.w("Network", "Manual connect error: " + text);
ToastService.showWarning(I18n.tr("common.wifi"), root.lastError);
}
}
}
}
Process {
id: disconnectProcess
property string ssid: ""
@@ -1336,32 +1539,46 @@ Singleton {
id: forgetProcess
property string ssid: ""
running: false
environment: ({
"LC_ALL": "C"
})
// Try multiple common profile name patterns
command: {
var script = "";
script += "ssid=\"$1\"\n";
script += "deleted=false\n\n";
script += "# Try exact SSID match first\n";
script += "if nmcli connection delete id \"$ssid\" 2>/dev/null; then\n";
script += " echo \"Deleted profile: $ssid\"\n";
script += " deleted=true\n";
script += "fi\n\n";
script += "# Try \"Auto $ssid\" pattern\n";
script += "if nmcli connection delete id \"Auto $ssid\" 2>/dev/null; then\n";
script += " echo \"Deleted profile: Auto $ssid\"\n";
script += " deleted=true\n";
script += "fi\n\n";
script += "# Try \"$ssid 1\", \"$ssid 2\", etc. patterns\n";
script += "for i in 1 2 3; do\n";
script += " if nmcli connection delete id \"$ssid $i\" 2>/dev/null; then\n";
script += " echo \"Deleted profile: $ssid $i\"\n";
script += " deleted=true\n";
script += " fi\n";
script += "done\n\n";
script += "if [ \"$deleted\" = \"false\" ]; then\n";
script += " echo \"No profiles found for SSID: $ssid\"\n";
script += "fi\n";
var script = `
ssid="$1"
deleted=false
# Try to find a wifi connection with this SSID and delete it
UUID=$(nmcli -t -f UUID,TYPE,NAME connection show | grep ":802-11-wireless:$ssid$" | head -n1 | cut -d: -f1)
if [ -n "$UUID" ]; then
if nmcli connection delete uuid "$UUID" 2>/dev/null; then
echo "Deleted profile: $ssid ($UUID)"
deleted=true
fi
fi
# Fallback: try common patterns if UUID lookup failed
if [ "$deleted" = "false" ]; then
# 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
fi
if [ "$deleted" = "false" ]; then
echo "No profiles found for SSID: $ssid"
fi
`;
return ["sh", "-c", script, "--", ssid];
}