Moved network related files, added network sub pages
C++ / fmt (pull_request) Successful in 5s
C++ / build (pull_request) Successful in 2m10s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 17s
Python / static (pull_request) Successful in 44s
C++ / clang-tidy (pull_request) Successful in 3m37s
Rust / build (pull_request) Successful in 1m3s
Rust / fmt (pull_request) Failing after 15m10s
Python / verify (pull_request) Failing after 16m16s
Rust / clippy (pull_request) Successful in 45s

This commit is contained in:
2026-10-18 16:29:12 +02:00
parent d1a045787e
commit 6391bc85c4
7 changed files with 2631 additions and 1 deletions
+21
View File
@@ -12,6 +12,7 @@ import qs.Modules.Settings.Pages.Apps
import qs.Modules.Settings.Pages.Panels
import qs.Modules.Settings.Pages.Panels.Bar
import qs.Modules.Settings.Pages.Services
import qs.Modules.Settings.Pages.Network
import qs.Services
QtObject {
@@ -50,6 +51,26 @@ QtObject {
NetworkPage {
}
}
Component {
AllNetworks {
}
}
Component {
SavedNetworks {
}
}
Component {
AddNetwork {
}
}
Component {
OldNetwork {
}
}
}
},
Component {
@@ -0,0 +1,256 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Internal
import ZShell.Blobs
import ZShell.Services
import ZShell.Components
import qs.Components
import qs.Modules.Settings.Common
PageBase {
id: root
property bool connecting: false
property bool failed: false
// Security model: index 0 = none, 1 = WPA/WPA2/WPA3 personal.
readonly property bool secured: securitySelect.active !== noneItem
property bool success: false
function submit(): void {
const ssid = ssidField.text.trim();
if (ssid.length === 0) {
ssidField.isError = true;
ssidField.forceActiveFocus();
return;
}
if (root.secured && passwordField.text.length < 8) {
passwordField.isError = true;
passwordField.forceActiveFocus();
return;
}
root.failed = false;
root.connecting = true;
Nmcli.addHiddenNetwork(ssid, root.secured ? passwordField.text : "", root.secured ? "wpa" : "none", hiddenToggle.checked, result => {
root.connecting = false;
if (result && result.success) {
root.success = true;
root.nState.closeSubPage();
} else {
root.failed = true;
if (root.secured)
passwordField.isError = true;
// Clean up the half-created profile so a retry starts fresh.
Nmcli.forgetNetwork(ssid);
}
});
}
isSubPage: true
title: Tr.tr("Add network")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.large
width: root.cappedWidth
Connections {
function onSubPageClosed(): void {
if (root.success)
return;
const ssid = ssidField.text.trim();
if (ssid)
Nmcli.forgetNetwork(ssid);
}
target: root.nState
}
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.padding.extraSmall
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.small
text: Tr.tr("Enter the details below to manually connect to a network.")
wrapMode: Text.WordWrap
}
CustomTextField {
id: ssidField
Layout.fillWidth: true
Layout.topMargin: Tokens.spacing.extraSmall
errorText: Tr.tr("Network name is required")
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
leadingIcon: "wifi"
// TRANSLATORS: SSID is a protocol term, leave it untranslated
placeholderText: Tr.tr("Network name (SSID)")
// TRANSLATORS: sample network name, translate the e.g. but the name is a placeholder
supportingText: Tr.tr("e.g. MyHiddenNetwork")
onAccepted: root.secured ? passwordField.forceActiveFocus() : root.submit()
}
ToggleRow {
id: hiddenToggle
checked: true
first: true
subtext: Tr.tr("Actively probe for a network that doesn't broadcast its name")
text: Tr.tr("Hidden network")
}
SelectRow {
id: securitySelect
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
fallbackIcon: "lock"
// TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated
fallbackText: Tr.tr("WPA/WPA2/WPA3 Personal")
// label: Tr.tr("Security")
last: !root.secured
Behavior on bottomLeftRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on bottomRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
menuItems: [
MenuItem {
icon: "lock"
// TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated
text: Tr.tr("WPA/WPA2/WPA3 Personal")
},
MenuItem {
id: noneItem
icon: "lock_open"
text: Tr.trCtx("None (open)", "wifi security type")
}
]
}
Item {
Layout.bottomMargin: root.secured ? 0 : -parent.spacing
Layout.fillWidth: true
implicitHeight: root.secured ? passwordField.implicitHeight : 0
opacity: root.secured ? 1 : 0
Behavior on Layout.bottomMargin {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on implicitHeight {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
CustomTextField {
id: passwordField
anchors.left: parent.left
anchors.right: parent.right
echoMode: TextInput.Password
enabled: root.secured
errorText: root.failed ? Tr.tr("Connection failed — check the password") : Tr.tr("Password must be at least 8 characters")
leadingIcon: "key"
placeholderText: Tr.tr("Password")
// TRANSLATORS: WPA is a protocol name, leave it untranslated
supportingText: Tr.tr("WPA passwords are at least 8 characters")
onAccepted: root.submit()
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.topMargin: Tokens.spacing.extraSmall - parent.spacing
spacing: Tokens.spacing.small
TextButton {
Layout.fillHeight: true
horizontalPadding: Tokens.padding.extraLarge
isRound: true
text: Tr.trCtx("Cancel", "button")
type: TextButton.Tonal
onClicked: root.nState.closeSubPage()
}
// Connect button — swaps to a loading spinner while connecting,
// matching the Wi-Fi list connect animation.
ButtonBase {
id: connectBtn
implicitHeight: connectMetrics.height + Tokens.padding.medium * 2
implicitWidth: connectMetrics.width + Tokens.padding.extraLarge * 2
// inactiveColour: Colours.palette.m3primary
// inactiveOnColour: Colours.palette.m3onPrimary
isRound: true
shapeMorph: true
// stateLayer.disabled: root.connecting || ssidField.text.trim().length === 0
onClicked: {
if (!root.connecting && ssidField.text.trim().length > 0)
root.submit();
}
TextMetrics {
id: connectMetrics
font: connectBtn.font
text: Tr.tr("Connect")
}
Anim {
id: connectContent
// anchors.centerIn: parent
// inAnimType: Anim.SlowEffects
// outAnimType: Anim.SlowEffects
// sourceComp: root.connecting ? connectLoadingComp : connectTextComp
}
Component {
id: connectLoadingComp
LoadingIndicator {
color: connectBtn.onColour
implicitSize: Math.round(Tokens.font.body.medium.pointSize * 1.4)
}
}
Component {
id: connectTextComp
CustomText {
color: connectBtn.onColour
font: connectBtn.font
text: connectMetrics.text
}
}
}
}
}
}
@@ -0,0 +1,345 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Helpers.Network
import qs.Services
import qs.Modules.Settings.Common
PageBase {
id: root
flickable.bottomMargin: Tokens.padding.extraExtraLarge * 2 // Extra scrolling space at the bottom
isSubPage: true
title: Tr.tr("All networks")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
// spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
Timer {
interval: 300 // GlobalConfig.nexus.networkRescanInterval
repeat: true
running: root.visible && Nmcli.wifiEnabled
triggeredOnStart: true
onTriggered: Nmcli.rescanWifi()
}
ConnectedRect {
Layout.fillWidth: true
first: true
implicitHeight: headerLayout.implicitHeight + Tokens.padding.medium * 2
RowLayout {
id: headerLayout
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1
// spacing: Tokens.spacing.extraSmall / 2
CustomText {
font: Tokens.font.title.small
text: Tr.trCtx("Filters", "network filter heading")
}
Item {
Layout.fillWidth: true
}
FilterButton {
id: savedFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return Nmcli.hasSavedProfile(ap.ssid);
}
bottomLeftRadius: pressed ? pressedRadius : implicitHeight / 2
text: Tr.trCtx("Saved", "network filter")
topLeftRadius: pressed ? pressedRadius : implicitHeight / 2
Behavior on bottomLeftRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on topLeftRadius {
Anim {
type: Anim.DefaultEffects
}
}
}
FilterButton {
id: secureFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return ap.security !== "none";
}
text: Tr.trCtx("Secured", "network filter")
}
FilterButton {
id: highFreqFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return ap.frequency >= 4900 && ap.frequency <= 5900;
}
text: Tr.trCtx("5 GHz", "network filter")
}
FilterButton {
id: lowFreqFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return ap.frequency >= 2400 && ap.frequency <= 2500;
}
bottomRightRadius: pressed ? pressedRadius : implicitHeight / 2
text: Tr.trCtx("2.4 GHz", "network filter")
topRightRadius: pressed ? pressedRadius : implicitHeight / 2
Behavior on bottomRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on topRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}
NetworkList {
id: networkList
function networkFilter(ap: Nmcli.AccessPoint): bool {
return savedFilter.filter(ap) && secureFilter.filter(ap) && highFreqFilter.filter(ap) && lowFreqFilter.filter(ap);
}
enableFilter: true
last: true
// nState: root.nState
}
}
component FilterButton: IconTextButton {
id: filter
property int filterState // 0 = default, 1 = on, 2 = negate
function filter(ap: Nmcli.AccessPoint): bool {
if (filterState === 0)
return true;
if (filterState === 1)
return internalFilter(ap);
return !internalFilter(ap);
}
function internalFilter(ap: Nmcli.AccessPoint): bool {
return true;
}
defaultRadius: filterState > 0 ? implicitHeight / 2 : Tokens.rounding.small
icon: ["check_indeterminate_small", "check", "close"][filterState]
// inactiveColour: [Colours.palette.m3secondaryContainer, Colours.palette.m3secondary, Colours.palette.m3tertiary][filterState]
// inactiveOnColour: [Colours.palette.m3onSecondaryContainer, Colours.palette.m3onSecondary, Colours.palette.m3onTertiary][filterState]
pressedRadius: Tokens.rounding.extraSmall
// spacing: Tokens.spacing.extraSmall
onClicked: filterState = (filterState + 1) % 3
}
component NetworkList: ItemList {
id: netList
property bool enableFilter
property int limit: 0 // 0 = show all
// required property NexusState nState
signal networkSelected(ap: Nmcli.AccessPoint)
function networkFilter(ap: Nmcli.AccessPoint): bool {
return true;
}
extraHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0 // Inline so it isn't affected by anim
list.anchors.top: scanningIndicator.bottom
placeholderIcon: Nmcli.wifiEnabled ? "wifi_find" : "signal_wifi_off"
placeholderText: Nmcli.wifiEnabled ? Tr.tr("No networks found") : Tr.tr("Wi-Fi disabled")
showList: Nmcli.wifiEnabled
delegate: StateLayer {
id: network
property bool currentSelected
required property int index
required property var modelData
property real textOpacity: disabled ? 0.5 : 1
anchors.fill: undefined
anchors.left: netList.list.contentItem.left
anchors.right: netList.list.contentItem.right
bottomLeftRadius: netList?.last && index === netList.list.count - 1 ? Tokens.rounding.extraLarge : radius
bottomRightRadius: netList?.last && index === netList.list.count - 1 ? Tokens.rounding.extraLarge : radius
// disabled: currentSelected || Nmcli.connectingSsid() === modelData.ssid
implicitHeight: networkLayout.implicitHeight + networkLayout.anchors.margins * 2
radius: Tokens.rounding.extraSmall
Behavior on textOpacity {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: {
if (!modelData.active) {
NetworkConnection.handleConnect(modelData);
currentSelected = true;
netList.networkSelected(modelData);
} else {
// Active network: open its detail/settings sub-page.
netList.nState.selectedNetworkSsid = modelData.ssid;
netList.nState.networkDetailsFromSaved = false;
netList.nState.openSubPage(3);
}
}
Connections {
function onActiveChanged(): void {
if (network.modelData.active)
network.currentSelected = false;
}
target: network.modelData
}
Connections {
function onNetworkSelected(ap: Nmcli.AccessPoint): void {
if (ap !== network.modelData)
network.currentSelected = false;
}
target: netList
}
RowLayout {
id: networkLayout
anchors.fill: parent
anchors.leftMargin: Tokens.padding.extraLarge
anchors.margins: Tokens.padding.large
anchors.rightMargin: Tokens.padding.extraLarge
// spacing: Tokens.spacing.extraSmall
MaterialIcon {
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
opacity: network.textOpacity
text: Icons.getNetworkIcon(network.modelData.strength)
}
ColumnLayout {
Layout.fillWidth: true
opacity: network.textOpacity
// spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font: Tokens.font.body.small
text: network.modelData.ssid
}
CustomText {
Layout.fillWidth: true
color: Colours.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.label.small
text: {
const net = network.modelData;
let status = "";
if (net.active)
status = Tr.trCtx("Connected", "network connected");
else if (Nmcli.hasSavedProfile(net.ssid))
status = Tr.trCtx("Saved", "network saved");
if (status)
// TRANSLATORS: %1 = security type, %2 = connection status
return Tr.trCtx("Security: %1 • %2", "network security and status").arg(net.security).arg(status);
return Tr.trCtx("Security: %1", "network security").arg(net.security);
}
}
}
// Anim {
// // sourceComp: Nmcli.connectingSsid() === network.modelData.ssid ? loadingComp : iconComp
//
// Component {
// id: iconComp
//
// MaterialIcon {
// color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
// fontStyle: Tokens.font.icon.medium
// opacity: network.textOpacity
// text: network.modelData.active ? "settings" : "lock"
// }
// }
//
// Component {
// id: loadingComp
//
// LoadingIndicator {
// implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
// }
// }
// }
}
}
// model: ScriptModel {
// values: {
// const connecting = Nmcli.connectingSsid();
// // Lower rank sorts higher in the list
// const rank = n => n.active ? 0 : n.ssid === connecting ? 1 : Nmcli.hasSavedProfile(n.ssid) ? 2 : 3;
// const sorted = [...Nmcli.networks].sort((a, b) => rank(a) - rank(b) || b.strength - a.strength);
// if (netList.limit > 0 && sorted.length > netList.limit)
// sorted.length = netList.limit;
// return netList.enableFilter ? sorted.filter(netList.networkFilter) : sorted;
// }
// }
// StyledProgressBar {
// id: scanningIndicator
//
// anchors.left: parent.left
// anchors.margins: 1
// anchors.right: parent.right
// implicitHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0
// indeterminate: true
//
// Behavior on implicitHeight {
// Anim {
// type: Anim.DefaultEffects
// }
// }
// }
}
}
@@ -0,0 +1,55 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers.Network
import qs.Services
PageBase {
id: root
title: qsTr("Begin page")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
NavRow {
first: true
icon: "dock_to_bottom"
// settingAnchor: "panels-bar"
text: qsTr("Show all networks")
onClicked: root.sState.openSubPage(1)
}
NavRow {
icon: "dock_to_bottom"
text: qsTr("Saved networks")
onClicked: root.sState.openSubPage(2)
}
NavRow {
icon: "dock_to_bottom"
text: qsTr("Add network")
onClicked: root.sState.openSubPage(3)
}
NavRow {
icon: "dock_to_bottom"
last: true
text: qsTr("Old Network Page")
onClicked: root.sState.openSubPage(4)
}
}
}
@@ -13,7 +13,8 @@ import qs.Services
PageBase {
id: root
title: qsTr("Network")
isSubPage: true
title: qsTr("Old network page")
Component.onCompleted: Network.active = true
Component.onDestruction: Network.active = false
@@ -0,0 +1,118 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Internal
import ZShell.Blobs
import ZShell.Services
import ZShell.Components
import qs.Components
import qs.Modules.Settings.Common
PageBase {
id: root
isSubPage: true
title: Tr.tr("Saved networks")
Component.onCompleted: Nmcli.loadSavedConnections(() => {})
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
ItemList {
id: savedList
first: true
last: true
placeholderIcon: "wifi_find"
placeholderText: Tr.tr("No saved networks")
showList: true
delegate: StateLayer {
id: saved
readonly property var ap: Nmcli.findNetwork(modelData)
required property int index
readonly property bool isActive: !!Nmcli.active && Nmcli.active.ssid === modelData
required property var modelData
anchors.fill: undefined
anchors.left: savedList.list.contentItem.left
anchors.right: savedList.list.contentItem.right
bottomLeftRadius: index === savedList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
bottomRightRadius: index === savedList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
implicitHeight: savedLayout.implicitHeight + savedLayout.anchors.margins * 2
radius: Tokens.rounding.extraSmall
topLeftRadius: index === 0 ? Tokens.rounding.extraLarge : radius
topRightRadius: index === 0 ? Tokens.rounding.extraLarge : radius
onClicked: {
root.nState.selectedNetworkSsid = saved.modelData;
root.nState.networkDetailsFromSaved = true;
root.nState.openSubPage(3); // Shared network detail/edit sub-page
}
RowLayout {
id: savedLayout
anchors.fill: parent
anchors.leftMargin: Tokens.padding.extraLarge
anchors.margins: Tokens.padding.large
anchors.rightMargin: Tokens.padding.extraLarge
spacing: Tokens.spacing.medium
MaterialIcon {
color: saved.isActive ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
text: saved.ap ? Icons.getNetworkIcon(saved.ap.strength, !["", "none"].includes(Nmcli.savedSecurityFor(saved.modelData))) : "signal_wifi_off"
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font: Tokens.font.body.small
text: saved.modelData
}
CustomText {
Layout.fillWidth: true
color: saved.isActive ? Colours.palette.m3primary : Colours.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.label.small
text: {
let security;
if (saved.ap)
security = saved.ap.security || Tr.trCtx("Open", "wifi security type");
else
security = Nmcli.securityLabel(Nmcli.savedSecurityFor(saved.modelData)) || Tr.trCtx("Unknown", "unknown wifi security");
if (saved.isActive)
// TRANSLATORS: %1 = security type
return Tr.trCtx("Connected • %1", "network connected with security").arg(security);
return security;
}
}
}
MaterialIcon {
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
text: "chevron_right"
}
}
}
model: ScriptModel {
values: [...Nmcli.savedConnectionSsids].sort((a, b) => a.localeCompare(b))
}
}
}
}
+1834
View File
File diff suppressed because it is too large Load Diff