mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge pull request #2109 from turannul/pr/networking-refactor-pt1
Network refactor pt1
This commit is contained in:
@@ -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": "Wi‑Fi"
|
||||
"title": "Wi‑Fi",
|
||||
"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",
|
||||
|
||||
@@ -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 Wi‑Fi 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 Wi‑Fi 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 Wi‑Fi 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
|
||||
|
||||
@@ -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 Wi‑Fi 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 Wi‑Fi 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
@@ -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 Wi‑Fi 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 Wi‑Fi bitrate via iw if available
|
||||
// Optional: query Wi‑Fi 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];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user