summaryrefslogtreecommitdiff
path: root/modules/controlcenter/network
diff options
context:
space:
mode:
Diffstat (limited to 'modules/controlcenter/network')
-rw-r--r--modules/controlcenter/network/EthernetDetails.qml118
-rw-r--r--modules/controlcenter/network/EthernetList.qml177
-rw-r--r--modules/controlcenter/network/EthernetPane.qml50
-rw-r--r--modules/controlcenter/network/EthernetSettings.qml76
-rw-r--r--modules/controlcenter/network/NetworkSettings.qml98
-rw-r--r--modules/controlcenter/network/NetworkingPane.qml305
-rw-r--r--modules/controlcenter/network/WirelessDetails.qml212
-rw-r--r--modules/controlcenter/network/WirelessList.qml226
-rw-r--r--modules/controlcenter/network/WirelessPane.qml57
-rw-r--r--modules/controlcenter/network/WirelessPasswordDialog.qml512
-rw-r--r--modules/controlcenter/network/WirelessSettings.qml73
11 files changed, 1904 insertions, 0 deletions
diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml
new file mode 100644
index 0000000..1cd6c0a
--- /dev/null
+++ b/modules/controlcenter/network/EthernetDetails.qml
@@ -0,0 +1,118 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.components.containers
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+DeviceDetails {
+ id: root
+
+ required property Session session
+ readonly property var ethernetDevice: root.session.ethernet.active
+
+ device: ethernetDevice
+
+ Component.onCompleted: {
+ if (ethernetDevice && ethernetDevice.interface) {
+ Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
+ }
+ }
+
+ onEthernetDeviceChanged: {
+ if (ethernetDevice && ethernetDevice.interface) {
+ Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
+ } else {
+ Nmcli.ethernetDeviceDetails = null;
+ }
+ }
+
+ headerComponent: Component {
+ ConnectionHeader {
+ icon: "cable"
+ title: root.ethernetDevice?.interface ?? qsTr("Unknown")
+ }
+ }
+
+ sections: [
+ Component {
+ ColumnLayout {
+ spacing: Appearance.spacing.normal
+
+ SectionHeader {
+ title: qsTr("Connection status")
+ description: qsTr("Connection settings for this device")
+ }
+
+ SectionContainer {
+ ToggleRow {
+ label: qsTr("Connected")
+ checked: root.ethernetDevice?.connected ?? false
+ toggle.onToggled: {
+ if (checked) {
+ Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {});
+ } else {
+ if (root.ethernetDevice?.connection) {
+ Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {});
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ Component {
+ ColumnLayout {
+ spacing: Appearance.spacing.normal
+
+ SectionHeader {
+ title: qsTr("Device properties")
+ description: qsTr("Additional information")
+ }
+
+ SectionContainer {
+ contentSpacing: Appearance.spacing.small / 2
+
+ PropertyRow {
+ label: qsTr("Interface")
+ value: root.ethernetDevice?.interface ?? qsTr("Unknown")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("Connection")
+ value: root.ethernetDevice?.connection || qsTr("Not connected")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("State")
+ value: root.ethernetDevice?.state ?? qsTr("Unknown")
+ }
+ }
+ }
+ },
+ Component {
+ ColumnLayout {
+ spacing: Appearance.spacing.normal
+
+ SectionHeader {
+ title: qsTr("Connection information")
+ description: qsTr("Network connection details")
+ }
+
+ SectionContainer {
+ ConnectionInfoSection {
+ deviceDetails: Nmcli.ethernetDeviceDetails
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml
new file mode 100644
index 0000000..4f4dc8a
--- /dev/null
+++ b/modules/controlcenter/network/EthernetList.qml
@@ -0,0 +1,177 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import qs.components
+import qs.components.controls
+import qs.components.containers
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+DeviceList {
+ id: root
+
+ required property Session session
+
+ title: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length)
+ description: qsTr("All available ethernet devices")
+ activeItem: session.ethernet.active
+
+ model: Nmcli.ethernetDevices
+
+ headerComponent: Component {
+ RowLayout {
+ spacing: Appearance.spacing.smaller
+
+ StyledText {
+ text: qsTr("Settings")
+ font.pointSize: Appearance.font.size.large
+ font.weight: 500
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ ToggleButton {
+ toggled: !root.session.ethernet.active
+ icon: "settings"
+ accent: "Primary"
+ iconSize: Appearance.font.size.normal
+ horizontalPadding: Appearance.padding.normal
+ verticalPadding: Appearance.padding.smaller
+
+ onClicked: {
+ if (root.session.ethernet.active)
+ root.session.ethernet.active = null;
+ else {
+ root.session.ethernet.active = root.view.model.get(0)?.modelData ?? null;
+ }
+ }
+ }
+ }
+ }
+
+ delegate: Component {
+ StyledRect {
+ id: ethernetItem
+
+ required property var modelData
+ readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface
+
+ width: ListView.view ? ListView.view.width : undefined
+ implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
+
+ color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0)
+ radius: Appearance.rounding.normal
+
+ StateLayer {
+ id: stateLayer
+
+ function onClicked(): void {
+ root.session.ethernet.active = modelData;
+ }
+ }
+
+ RowLayout {
+ id: rowLayout
+
+ anchors.fill: parent
+ anchors.margins: Appearance.padding.normal
+
+ spacing: Appearance.spacing.normal
+
+ StyledRect {
+ implicitWidth: implicitHeight
+ implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
+
+ radius: Appearance.rounding.normal
+ color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
+
+ StyledRect {
+ anchors.fill: parent
+ radius: parent.radius
+ color: Qt.alpha(modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)
+ }
+
+ MaterialIcon {
+ id: icon
+
+ anchors.centerIn: parent
+ text: "cable"
+ font.pointSize: Appearance.font.size.large
+ fill: modelData.connected ? 1 : 0
+ color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
+
+ Behavior on fill {
+ Anim {}
+ }
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+
+ spacing: 0
+
+ StyledText {
+ Layout.fillWidth: true
+ text: modelData.interface || qsTr("Unknown")
+ elide: Text.ElideRight
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.smaller
+
+ StyledText {
+ Layout.fillWidth: true
+ text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected")
+ color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.small
+ font.weight: modelData.connected ? 500 : 400
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ StyledRect {
+ id: connectBtn
+
+ implicitWidth: implicitHeight
+ implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
+
+ radius: Appearance.rounding.full
+ color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0)
+
+ StateLayer {
+ color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
+
+ function onClicked(): void {
+ if (modelData.connected && modelData.connection) {
+ Nmcli.disconnectEthernet(modelData.connection, () => {});
+ } else {
+ Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {});
+ }
+ }
+ }
+
+ MaterialIcon {
+ id: connectIcon
+
+ anchors.centerIn: parent
+ animate: true
+ text: modelData.connected ? "link_off" : "link"
+ color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
+ }
+ }
+ }
+ }
+ }
+
+ onItemSelected: function(item) {
+ session.ethernet.active = item;
+ }
+}
diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml
new file mode 100644
index 0000000..126535a
--- /dev/null
+++ b/modules/controlcenter/network/EthernetPane.qml
@@ -0,0 +1,50 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import qs.components
+import qs.components.containers
+import qs.config
+import Quickshell.Widgets
+import QtQuick
+
+SplitPaneWithDetails {
+ id: root
+
+ required property Session session
+
+ anchors.fill: parent
+
+ activeItem: session.ethernet.active
+ paneIdGenerator: function(item) {
+ return item ? (item.interface || "") : "";
+ }
+
+ leftContent: Component {
+ EthernetList {
+ session: root.session
+ }
+ }
+
+ rightDetailsComponent: Component {
+ EthernetDetails {
+ session: root.session
+ }
+ }
+
+ rightSettingsComponent: Component {
+ StyledFlickable {
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: settingsInner.height
+ clip: true
+
+ EthernetSettings {
+ id: settingsInner
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ session: root.session
+ }
+ }
+ }
+}
diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml
new file mode 100644
index 0000000..f0f66b4
--- /dev/null
+++ b/modules/controlcenter/network/EthernetSettings.qml
@@ -0,0 +1,76 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ required property Session session
+
+ spacing: Appearance.spacing.normal
+
+ SettingsHeader {
+ icon: "cable"
+ title: qsTr("Ethernet settings")
+ }
+
+ StyledText {
+ Layout.topMargin: Appearance.spacing.large
+ text: qsTr("Ethernet devices")
+ font.pointSize: Appearance.font.size.larger
+ font.weight: 500
+ }
+
+ StyledText {
+ text: qsTr("Available ethernet devices")
+ color: Colours.palette.m3outline
+ }
+
+ StyledRect {
+ Layout.fillWidth: true
+ implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2
+
+ radius: Appearance.rounding.normal
+ color: Colours.tPalette.m3surfaceContainer
+
+ ColumnLayout {
+ id: ethernetInfo
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Appearance.padding.large
+
+ spacing: Appearance.spacing.small / 2
+
+ StyledText {
+ text: qsTr("Total devices")
+ }
+
+ StyledText {
+ text: qsTr("%1").arg(Nmcli.ethernetDevices.length)
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.small
+ }
+
+ StyledText {
+ Layout.topMargin: Appearance.spacing.normal
+ text: qsTr("Connected devices")
+ }
+
+ StyledText {
+ text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.small
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml
new file mode 100644
index 0000000..22e07cb
--- /dev/null
+++ b/modules/controlcenter/network/NetworkSettings.qml
@@ -0,0 +1,98 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ required property Session session
+
+ spacing: Appearance.spacing.normal
+
+ SettingsHeader {
+ icon: "router"
+ title: qsTr("Network Settings")
+ }
+
+ SectionHeader {
+ Layout.topMargin: Appearance.spacing.large
+ title: qsTr("Ethernet")
+ description: qsTr("Ethernet device information")
+ }
+
+ SectionContainer {
+ contentSpacing: Appearance.spacing.small / 2
+
+ PropertyRow {
+ label: qsTr("Total devices")
+ value: qsTr("%1").arg(Nmcli.ethernetDevices.length)
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("Connected devices")
+ value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)
+ }
+ }
+
+ SectionHeader {
+ Layout.topMargin: Appearance.spacing.large
+ title: qsTr("Wireless")
+ description: qsTr("WiFi network settings")
+ }
+
+ SectionContainer {
+ ToggleRow {
+ label: qsTr("WiFi enabled")
+ checked: Nmcli.wifiEnabled
+ toggle.onToggled: {
+ Nmcli.enableWifi(checked);
+ }
+ }
+ }
+
+ SectionHeader {
+ Layout.topMargin: Appearance.spacing.large
+ title: qsTr("Current connection")
+ description: qsTr("Active network connection information")
+ }
+
+ SectionContainer {
+ contentSpacing: Appearance.spacing.small / 2
+
+ PropertyRow {
+ label: qsTr("Network")
+ value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected"))
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ visible: Nmcli.active !== null
+ label: qsTr("Signal strength")
+ value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ visible: Nmcli.active !== null
+ label: qsTr("Security")
+ value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ visible: Nmcli.active !== null
+ label: qsTr("Frequency")
+ value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
+ }
+ }
+}
+
diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml
new file mode 100644
index 0000000..b430cce
--- /dev/null
+++ b/modules/controlcenter/network/NetworkingPane.qml
@@ -0,0 +1,305 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import "."
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.components.containers
+import qs.services
+import qs.config
+import qs.utils
+import Quickshell
+import Quickshell.Widgets
+import QtQuick
+import QtQuick.Layouts
+
+Item {
+ id: root
+
+ required property Session session
+
+ anchors.fill: parent
+
+ SplitPaneLayout {
+ id: splitLayout
+
+ anchors.fill: parent
+
+ leftContent: Component {
+ StyledFlickable {
+ id: leftFlickable
+
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: leftContent.height
+
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: leftFlickable
+ }
+
+ ColumnLayout {
+ id: leftContent
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: Appearance.spacing.normal
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.smaller
+
+ StyledText {
+ text: qsTr("Network")
+ font.pointSize: Appearance.font.size.large
+ font.weight: 500
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ ToggleButton {
+ toggled: Nmcli.wifiEnabled
+ icon: "wifi"
+ accent: "Tertiary"
+ iconSize: Appearance.font.size.normal
+ horizontalPadding: Appearance.padding.normal
+ verticalPadding: Appearance.padding.smaller
+ tooltip: qsTr("Toggle WiFi")
+
+ onClicked: {
+ Nmcli.toggleWifi(null);
+ }
+ }
+
+ ToggleButton {
+ toggled: Nmcli.scanning
+ icon: "wifi_find"
+ accent: "Secondary"
+ iconSize: Appearance.font.size.normal
+ horizontalPadding: Appearance.padding.normal
+ verticalPadding: Appearance.padding.smaller
+ tooltip: qsTr("Scan for networks")
+
+ onClicked: {
+ Nmcli.rescanWifi();
+ }
+ }
+
+ ToggleButton {
+ toggled: !root.session.ethernet.active && !root.session.network.active
+ icon: "settings"
+ accent: "Primary"
+ iconSize: Appearance.font.size.normal
+ horizontalPadding: Appearance.padding.normal
+ verticalPadding: Appearance.padding.smaller
+ tooltip: qsTr("Network settings")
+
+ onClicked: {
+ if (root.session.ethernet.active || root.session.network.active) {
+ root.session.ethernet.active = null;
+ root.session.network.active = null;
+ } else {
+ if (Nmcli.ethernetDevices.length > 0) {
+ root.session.ethernet.active = Nmcli.ethernetDevices[0];
+ } else if (Nmcli.networks.length > 0) {
+ root.session.network.active = Nmcli.networks[0];
+ }
+ }
+ }
+ }
+ }
+
+ CollapsibleSection {
+ id: ethernetListSection
+
+ Layout.fillWidth: true
+ title: qsTr("Ethernet")
+ expanded: true
+
+ Loader {
+ Layout.fillWidth: true
+ sourceComponent: Component {
+ EthernetList {
+ session: root.session
+ showHeader: false
+ }
+ }
+ }
+ }
+
+ CollapsibleSection {
+ id: wirelessListSection
+
+ Layout.fillWidth: true
+ title: qsTr("Wireless")
+ expanded: true
+
+ Loader {
+ Layout.fillWidth: true
+ sourceComponent: Component {
+ WirelessList {
+ session: root.session
+ showHeader: false
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ rightContent: Component {
+ Item {
+ id: rightPaneItem
+
+ property var ethernetPane: root.session.ethernet.active
+ property var wirelessPane: root.session.network.active
+ property var pane: ethernetPane || wirelessPane
+ property string paneId: ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings")
+ property Component targetComponent: settingsComponent
+ property Component nextComponent: settingsComponent
+
+ function getComponentForPane() {
+ if (ethernetPane) return ethernetDetailsComponent;
+ if (wirelessPane) return wirelessDetailsComponent;
+ return settingsComponent;
+ }
+
+ Component.onCompleted: {
+ targetComponent = getComponentForPane();
+ nextComponent = targetComponent;
+ }
+
+ Connections {
+ target: root.session.ethernet
+ function onActiveChanged() {
+ // Clear wireless when ethernet is selected
+ if (root.session.ethernet.active && root.session.network.active) {
+ root.session.network.active = null;
+ return; // Let the network.onActiveChanged handle the update
+ }
+ rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
+ // paneId will automatically update via property binding
+ }
+ }
+
+ Connections {
+ target: root.session.network
+ function onActiveChanged() {
+ // Clear ethernet when wireless is selected
+ if (root.session.network.active && root.session.ethernet.active) {
+ root.session.ethernet.active = null;
+ return; // Let the ethernet.onActiveChanged handle the update
+ }
+ rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
+ // paneId will automatically update via property binding
+ }
+ }
+
+ Loader {
+ id: rightLoader
+
+ anchors.fill: parent
+
+ opacity: 1
+ scale: 1
+ transformOrigin: Item.Center
+ clip: false
+
+ asynchronous: true
+ sourceComponent: rightPaneItem.targetComponent
+ }
+
+ Behavior on paneId {
+ PaneTransition {
+ target: rightLoader
+ propertyActions: [
+ PropertyAction {
+ target: rightPaneItem
+ property: "targetComponent"
+ value: rightPaneItem.nextComponent
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: settingsComponent
+
+ StyledFlickable {
+ id: settingsFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: settingsInner.height
+
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: settingsFlickable
+ }
+
+ NetworkSettings {
+ id: settingsInner
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ session: root.session
+ }
+ }
+ }
+
+ Component {
+ id: ethernetDetailsComponent
+
+ StyledFlickable {
+ id: ethernetFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: ethernetDetailsInner.height
+
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: ethernetFlickable
+ }
+
+ EthernetDetails {
+ id: ethernetDetailsInner
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ session: root.session
+ }
+ }
+ }
+
+ Component {
+ id: wirelessDetailsComponent
+
+ StyledFlickable {
+ id: wirelessFlickable
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: wirelessDetailsInner.height
+
+ StyledScrollBar.vertical: StyledScrollBar {
+ flickable: wirelessFlickable
+ }
+
+ WirelessDetails {
+ id: wirelessDetailsInner
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ session: root.session
+ }
+ }
+ }
+
+ WirelessPasswordDialog {
+ anchors.fill: parent
+ session: root.session
+ z: 1000
+ }
+}
diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml
new file mode 100644
index 0000000..47d42c2
--- /dev/null
+++ b/modules/controlcenter/network/WirelessDetails.qml
@@ -0,0 +1,212 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import "."
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.components.containers
+import qs.services
+import qs.config
+import qs.utils
+import QtQuick
+import QtQuick.Layouts
+
+DeviceDetails {
+ id: root
+
+ required property Session session
+ readonly property var network: root.session.network.active
+
+ device: network
+
+ Component.onCompleted: {
+ updateDeviceDetails();
+ checkSavedProfile();
+ }
+
+ onNetworkChanged: {
+ connectionUpdateTimer.stop();
+ if (network && network.ssid) {
+ connectionUpdateTimer.start();
+ }
+ updateDeviceDetails();
+ checkSavedProfile();
+ }
+
+ function checkSavedProfile(): void {
+ if (network && network.ssid) {
+ Nmcli.loadSavedConnections(() => {});
+ }
+ }
+
+ Connections {
+ target: Nmcli
+ function onActiveChanged() {
+ updateDeviceDetails();
+ }
+ function onWirelessDeviceDetailsChanged() {
+ if (network && network.ssid) {
+ const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
+ if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) {
+ connectionUpdateTimer.stop();
+ }
+ }
+ }
+ }
+
+ Timer {
+ id: connectionUpdateTimer
+ interval: 500
+ repeat: true
+ running: network && network.ssid
+ onTriggered: {
+ if (network) {
+ const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
+ if (isActive) {
+ if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) {
+ Nmcli.getWirelessDeviceDetails("", () => {
+ });
+ } else {
+ connectionUpdateTimer.stop();
+ }
+ } else {
+ if (Nmcli.wirelessDeviceDetails !== null) {
+ Nmcli.wirelessDeviceDetails = null;
+ }
+ }
+ }
+ }
+ }
+
+ function updateDeviceDetails(): void {
+ if (network && network.ssid) {
+ const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
+ if (isActive) {
+ Nmcli.getWirelessDeviceDetails("");
+ } else {
+ Nmcli.wirelessDeviceDetails = null;
+ }
+ } else {
+ Nmcli.wirelessDeviceDetails = null;
+ }
+ }
+
+ headerComponent: Component {
+ ConnectionHeader {
+ icon: root.network?.isSecure ? "lock" : "wifi"
+ title: root.network?.ssid ?? qsTr("Unknown")
+ }
+ }
+
+ sections: [
+ Component {
+ ColumnLayout {
+ spacing: Appearance.spacing.normal
+
+ SectionHeader {
+ title: qsTr("Connection status")
+ description: qsTr("Connection settings for this network")
+ }
+
+ SectionContainer {
+ ToggleRow {
+ label: qsTr("Connected")
+ checked: root.network?.active ?? false
+ toggle.onToggled: {
+ if (checked) {
+ NetworkConnection.handleConnect(root.network, root.session, null);
+ } else {
+ Nmcli.disconnectFromNetwork();
+ }
+ }
+ }
+
+ TextButton {
+ Layout.fillWidth: true
+ Layout.topMargin: Appearance.spacing.normal
+ Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
+ visible: {
+ if (!root.network || !root.network.ssid) {
+ return false;
+ }
+ return Nmcli.hasSavedProfile(root.network.ssid);
+ }
+ inactiveColour: Colours.palette.m3secondaryContainer
+ inactiveOnColour: Colours.palette.m3onSecondaryContainer
+ text: qsTr("Forget Network")
+
+ onClicked: {
+ if (root.network && root.network.ssid) {
+ if (root.network.active) {
+ Nmcli.disconnectFromNetwork();
+ }
+ Nmcli.forgetNetwork(root.network.ssid);
+ }
+ }
+ }
+ }
+ }
+ },
+ Component {
+ ColumnLayout {
+ spacing: Appearance.spacing.normal
+
+ SectionHeader {
+ title: qsTr("Network properties")
+ description: qsTr("Additional information")
+ }
+
+ SectionContainer {
+ contentSpacing: Appearance.spacing.small / 2
+
+ PropertyRow {
+ label: qsTr("SSID")
+ value: root.network?.ssid ?? qsTr("Unknown")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("BSSID")
+ value: root.network?.bssid ?? qsTr("Unknown")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("Signal strength")
+ value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("Frequency")
+ value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("Security")
+ value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A")
+ }
+ }
+ }
+ },
+ Component {
+ ColumnLayout {
+ spacing: Appearance.spacing.normal
+
+ SectionHeader {
+ title: qsTr("Connection information")
+ description: qsTr("Network connection details")
+ }
+
+ SectionContainer {
+ ConnectionInfoSection {
+ deviceDetails: Nmcli.wirelessDeviceDetails
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml
new file mode 100644
index 0000000..8159291
--- /dev/null
+++ b/modules/controlcenter/network/WirelessList.qml
@@ -0,0 +1,226 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import "."
+import qs.components
+import qs.components.controls
+import qs.components.containers
+import qs.components.effects
+import qs.services
+import qs.config
+import qs.utils
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+
+DeviceList {
+ id: root
+
+ required property Session session
+
+ title: qsTr("Networks (%1)").arg(Nmcli.networks.length)
+ description: qsTr("All available WiFi networks")
+ activeItem: session.network.active
+
+ titleSuffix: Component {
+ StyledText {
+ visible: Nmcli.scanning
+ text: qsTr("Scanning...")
+ color: Colours.palette.m3primary
+ font.pointSize: Appearance.font.size.small
+ }
+ }
+
+ model: ScriptModel {
+ values: [...Nmcli.networks].sort((a, b) => {
+ if (a.active !== b.active)
+ return b.active - a.active;
+ return b.strength - a.strength;
+ })
+ }
+
+ headerComponent: Component {
+ RowLayout {
+ spacing: Appearance.spacing.smaller
+
+ StyledText {
+ text: qsTr("Settings")
+ font.pointSize: Appearance.font.size.large
+ font.weight: 500
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ ToggleButton {
+ toggled: Nmcli.wifiEnabled
+ icon: "wifi"
+ accent: "Tertiary"
+ iconSize: Appearance.font.size.normal
+ horizontalPadding: Appearance.padding.normal
+ verticalPadding: Appearance.padding.smaller
+
+ onClicked: {
+ Nmcli.toggleWifi(null);
+ }
+ }
+
+ ToggleButton {
+ toggled: Nmcli.scanning
+ icon: "wifi_find"
+ accent: "Secondary"
+ iconSize: Appearance.font.size.normal
+ horizontalPadding: Appearance.padding.normal
+ verticalPadding: Appearance.padding.smaller
+
+ onClicked: {
+ Nmcli.rescanWifi();
+ }
+ }
+
+ ToggleButton {
+ toggled: !root.session.network.active
+ icon: "settings"
+ accent: "Primary"
+ iconSize: Appearance.font.size.normal
+ horizontalPadding: Appearance.padding.normal
+ verticalPadding: Appearance.padding.smaller
+
+ onClicked: {
+ if (root.session.network.active)
+ root.session.network.active = null;
+ else {
+ root.session.network.active = root.view.model.get(0)?.modelData ?? null;
+ }
+ }
+ }
+ }
+ }
+
+ delegate: Component {
+ StyledRect {
+ required property var modelData
+
+ width: ListView.view ? ListView.view.width : undefined
+
+ color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
+ radius: Appearance.rounding.normal
+
+ StateLayer {
+ function onClicked(): void {
+ root.session.network.active = modelData;
+ if (modelData && modelData.ssid) {
+ root.checkSavedProfileForNetwork(modelData.ssid);
+ }
+ }
+ }
+
+ RowLayout {
+ id: rowLayout
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Appearance.padding.normal
+
+ spacing: Appearance.spacing.normal
+
+ StyledRect {
+ implicitWidth: implicitHeight
+ implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
+
+ radius: Appearance.rounding.normal
+ color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
+
+ MaterialIcon {
+ id: icon
+
+ anchors.centerIn: parent
+ text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure)
+ font.pointSize: Appearance.font.size.large
+ fill: modelData.active ? 1 : 0
+ color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+
+ spacing: 0
+
+ StyledText {
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ maximumLineCount: 1
+
+ text: modelData.ssid || qsTr("Unknown")
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.smaller
+
+ StyledText {
+ Layout.fillWidth: true
+ text: {
+ if (modelData.active) return qsTr("Connected");
+ if (modelData.isSecure && modelData.security && modelData.security.length > 0) {
+ return modelData.security;
+ }
+ if (modelData.isSecure) return qsTr("Secured");
+ return qsTr("Open");
+ }
+ color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.small
+ font.weight: modelData.active ? 500 : 400
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ StyledRect {
+ implicitWidth: implicitHeight
+ implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
+
+ radius: Appearance.rounding.full
+ color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0)
+
+ StateLayer {
+ function onClicked(): void {
+ if (modelData.active) {
+ Nmcli.disconnectFromNetwork();
+ } else {
+ NetworkConnection.handleConnect(modelData, root.session, null);
+ }
+ }
+ }
+
+ MaterialIcon {
+ id: connectIcon
+
+ anchors.centerIn: parent
+ text: modelData.active ? "link_off" : "link"
+ color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
+ }
+ }
+ }
+
+ implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
+ }
+ }
+
+ onItemSelected: function(item) {
+ session.network.active = item;
+ if (item && item.ssid) {
+ checkSavedProfileForNetwork(item.ssid);
+ }
+ }
+
+ function checkSavedProfileForNetwork(ssid: string): void {
+ if (ssid && ssid.length > 0) {
+ Nmcli.loadSavedConnections(() => {});
+ }
+ }
+}
diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml
new file mode 100644
index 0000000..109d416
--- /dev/null
+++ b/modules/controlcenter/network/WirelessPane.qml
@@ -0,0 +1,57 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import qs.components
+import qs.components.containers
+import qs.config
+import Quickshell.Widgets
+import QtQuick
+
+SplitPaneWithDetails {
+ id: root
+
+ required property Session session
+
+ anchors.fill: parent
+
+ activeItem: session.network.active
+ paneIdGenerator: function(item) {
+ return item ? (item.ssid || item.bssid || "") : "";
+ }
+
+ leftContent: Component {
+ WirelessList {
+ session: root.session
+ }
+ }
+
+ rightDetailsComponent: Component {
+ WirelessDetails {
+ session: root.session
+ }
+ }
+
+ rightSettingsComponent: Component {
+ StyledFlickable {
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: settingsInner.height
+ clip: true
+
+ WirelessSettings {
+ id: settingsInner
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ session: root.session
+ }
+ }
+ }
+
+ overlayComponent: Component {
+ WirelessPasswordDialog {
+ anchors.fill: parent
+ session: root.session
+ }
+ }
+}
diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml
new file mode 100644
index 0000000..7c046af
--- /dev/null
+++ b/modules/controlcenter/network/WirelessPasswordDialog.qml
@@ -0,0 +1,512 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "."
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.components.containers
+import qs.services
+import qs.config
+import qs.utils
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+
+Item {
+ id: root
+
+ required property Session session
+
+ readonly property var network: {
+ if (session.network.pendingNetwork) {
+ return session.network.pendingNetwork;
+ }
+ if (session.network.active) {
+ return session.network.active;
+ }
+ return null;
+ }
+
+ property bool isClosing: false
+ visible: session.network.showPasswordDialog || isClosing
+ enabled: session.network.showPasswordDialog && !isClosing
+ focus: enabled
+
+ Keys.onEscapePressed: {
+ closeDialog();
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: Qt.rgba(0, 0, 0, 0.5)
+ opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: closeDialog()
+ }
+ }
+
+ StyledRect {
+ id: dialog
+
+ anchors.centerIn: parent
+
+ implicitWidth: 400
+ implicitHeight: content.implicitHeight + Appearance.padding.large * 2
+
+ radius: Appearance.rounding.normal
+ color: Colours.tPalette.m3surface
+ opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0
+ scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ ParallelAnimation {
+ running: root.isClosing
+ onFinished: {
+ if (root.isClosing) {
+ root.session.network.showPasswordDialog = false;
+ root.isClosing = false;
+ }
+ }
+
+ Anim {
+ target: dialog
+ property: "opacity"
+ to: 0
+ }
+ Anim {
+ target: dialog
+ property: "scale"
+ to: 0.7
+ }
+ }
+
+ Keys.onEscapePressed: closeDialog()
+
+ ColumnLayout {
+ id: content
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Appearance.padding.large
+
+ spacing: Appearance.spacing.normal
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignHCenter
+ text: "lock"
+ font.pointSize: Appearance.font.size.extraLarge * 2
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Enter password")
+ font.pointSize: Appearance.font.size.large
+ font.weight: 500
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : ""
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.small
+ }
+
+ StyledText {
+ id: statusText
+
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: Appearance.spacing.small
+ visible: connectButton.connecting || connectButton.hasError
+ text: {
+ if (connectButton.hasError) {
+ return qsTr("Connection failed. Please check your password and try again.");
+ }
+ if (connectButton.connecting) {
+ return qsTr("Connecting...");
+ }
+ return "";
+ }
+ color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant
+ font.pointSize: Appearance.font.size.small
+ font.weight: 400
+ wrapMode: Text.WordWrap
+ Layout.maximumWidth: parent.width - Appearance.padding.large * 2
+ }
+
+ Item {
+ id: passwordContainer
+ Layout.topMargin: Appearance.spacing.large
+ Layout.fillWidth: true
+ implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)
+
+ focus: true
+ Keys.onPressed: event => {
+ if (!activeFocus) {
+ forceActiveFocus();
+ }
+
+ if (connectButton.hasError && event.text && event.text.length > 0) {
+ connectButton.hasError = false;
+ }
+
+ if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
+ if (connectButton.enabled) {
+ connectButton.clicked();
+ }
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Backspace) {
+ if (event.modifiers & Qt.ControlModifier) {
+ passwordBuffer = "";
+ } else {
+ passwordBuffer = passwordBuffer.slice(0, -1);
+ }
+ event.accepted = true;
+ } else if (event.text && event.text.length > 0) {
+ passwordBuffer += event.text;
+ event.accepted = true;
+ }
+ }
+
+ property string passwordBuffer: ""
+
+ Connections {
+ target: root.session.network
+ function onShowPasswordDialogChanged(): void {
+ if (root.session.network.showPasswordDialog) {
+ Qt.callLater(() => {
+ passwordContainer.forceActiveFocus();
+ passwordContainer.passwordBuffer = "";
+ connectButton.hasError = false;
+ });
+ }
+ }
+ }
+
+ Connections {
+ target: root
+ function onVisibleChanged(): void {
+ if (root.visible) {
+ Qt.callLater(() => {
+ passwordContainer.forceActiveFocus();
+ });
+ }
+ }
+ }
+
+ StyledRect {
+ anchors.fill: parent
+ radius: Appearance.rounding.normal
+ color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer
+ border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0)
+ border.color: {
+ if (connectButton.hasError) {
+ return Colours.palette.m3error;
+ }
+ if (passwordContainer.activeFocus) {
+ return Colours.palette.m3primary;
+ }
+ return root.visible ? Colours.palette.m3outline : "transparent";
+ }
+
+ Behavior on border.color {
+ CAnim {}
+ }
+
+ Behavior on border.width {
+ CAnim {}
+ }
+
+ Behavior on color {
+ CAnim {}
+ }
+ }
+
+ StateLayer {
+ hoverEnabled: false
+ cursorShape: Qt.IBeamCursor
+
+ function onClicked(): void {
+ passwordContainer.forceActiveFocus();
+ }
+ }
+
+ StyledText {
+ id: placeholder
+ anchors.centerIn: parent
+ text: qsTr("Password")
+ color: Colours.palette.m3outline
+ font.pointSize: Appearance.font.size.normal
+ font.family: Appearance.font.family.mono
+ opacity: passwordContainer.passwordBuffer ? 0 : 1
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ ListView {
+ id: charList
+
+ readonly property int fullWidth: count * (implicitHeight + spacing) - spacing
+
+ anchors.centerIn: parent
+ implicitWidth: fullWidth
+ implicitHeight: Appearance.font.size.normal
+
+ orientation: Qt.Horizontal
+ spacing: Appearance.spacing.small / 2
+ interactive: false
+
+ model: ScriptModel {
+ values: passwordContainer.passwordBuffer.split("")
+ }
+
+ delegate: StyledRect {
+ id: ch
+
+ implicitWidth: implicitHeight
+ implicitHeight: charList.implicitHeight
+
+ color: Colours.palette.m3onSurface
+ radius: Appearance.rounding.small / 2
+
+ opacity: 0
+ scale: 0
+ Component.onCompleted: {
+ opacity = 1;
+ scale = 1;
+ }
+ ListView.onRemove: removeAnim.start()
+
+ SequentialAnimation {
+ id: removeAnim
+
+ PropertyAction {
+ target: ch
+ property: "ListView.delayRemove"
+ value: true
+ }
+ ParallelAnimation {
+ Anim {
+ target: ch
+ property: "opacity"
+ to: 0
+ }
+ Anim {
+ target: ch
+ property: "scale"
+ to: 0.5
+ }
+ }
+ PropertyAction {
+ target: ch
+ property: "ListView.delayRemove"
+ value: false
+ }
+ }
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ Behavior on scale {
+ Anim {
+ duration: Appearance.anim.durations.expressiveFastSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
+ }
+ }
+ }
+
+ Behavior on implicitWidth {
+ Anim {}
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.topMargin: Appearance.spacing.normal
+ Layout.fillWidth: true
+ spacing: Appearance.spacing.normal
+
+ TextButton {
+ id: cancelButton
+
+ Layout.fillWidth: true
+ Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
+ inactiveColour: Colours.palette.m3secondaryContainer
+ inactiveOnColour: Colours.palette.m3onSecondaryContainer
+ text: qsTr("Cancel")
+
+ onClicked: root.closeDialog()
+ }
+
+ TextButton {
+ id: connectButton
+
+ property bool connecting: false
+ property bool hasError: false
+
+ Layout.fillWidth: true
+ Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
+ inactiveColour: Colours.palette.m3primary
+ inactiveOnColour: Colours.palette.m3onPrimary
+ text: qsTr("Connect")
+ enabled: passwordContainer.passwordBuffer.length > 0 && !connecting
+
+ onClicked: {
+ if (!root.network || connecting) {
+ return;
+ }
+
+ const password = passwordContainer.passwordBuffer;
+ if (!password || password.length === 0) {
+ return;
+ }
+
+ hasError = false;
+ connecting = true;
+ enabled = false;
+ text = qsTr("Connecting...");
+
+ NetworkConnection.connectWithPassword(root.network, password, result => {
+ if (result && result.success) {
+ } else if (result && result.needsPassword) {
+ connectionMonitor.stop();
+ connecting = false;
+ hasError = true;
+ enabled = true;
+ text = qsTr("Connect");
+ passwordContainer.passwordBuffer = "";
+ if (root.network && root.network.ssid) {
+ Nmcli.forgetNetwork(root.network.ssid);
+ }
+ } else {
+ connectionMonitor.stop();
+ connecting = false;
+ hasError = true;
+ enabled = true;
+ text = qsTr("Connect");
+ passwordContainer.passwordBuffer = "";
+ if (root.network && root.network.ssid) {
+ Nmcli.forgetNetwork(root.network.ssid);
+ }
+ }
+ });
+
+ connectionMonitor.start();
+ }
+ }
+ }
+ }
+ }
+
+ function checkConnectionStatus(): void {
+ if (!root.visible || !connectButton.connecting) {
+ return;
+ }
+
+ const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
+
+ if (isConnected) {
+ connectionSuccessTimer.start();
+ return;
+ }
+
+ if (Nmcli.pendingConnection === null && connectButton.connecting) {
+ if (connectionMonitor.repeatCount > 10) {
+ connectionMonitor.stop();
+ connectButton.connecting = false;
+ connectButton.hasError = true;
+ connectButton.enabled = true;
+ connectButton.text = qsTr("Connect");
+ passwordContainer.passwordBuffer = "";
+ if (root.network && root.network.ssid) {
+ Nmcli.forgetNetwork(root.network.ssid);
+ }
+ }
+ }
+ }
+
+ Timer {
+ id: connectionMonitor
+ interval: 1000
+ repeat: true
+ triggeredOnStart: false
+ property int repeatCount: 0
+
+ onTriggered: {
+ repeatCount++;
+ checkConnectionStatus();
+ }
+
+ onRunningChanged: {
+ if (!running) {
+ repeatCount = 0;
+ }
+ }
+ }
+
+ Timer {
+ id: connectionSuccessTimer
+ interval: 500
+ onTriggered: {
+ if (root.visible && Nmcli.active && Nmcli.active.ssid) {
+ const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
+ if (stillConnected) {
+ connectionMonitor.stop();
+ connectButton.connecting = false;
+ connectButton.text = qsTr("Connect");
+ closeDialog();
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: Nmcli
+ function onActiveChanged() {
+ if (root.visible) {
+ checkConnectionStatus();
+ }
+ }
+ function onConnectionFailed(ssid: string) {
+ if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) {
+ connectionMonitor.stop();
+ connectButton.connecting = false;
+ connectButton.hasError = true;
+ connectButton.enabled = true;
+ connectButton.text = qsTr("Connect");
+ passwordContainer.passwordBuffer = "";
+ Nmcli.forgetNetwork(ssid);
+ }
+ }
+ }
+
+ function closeDialog(): void {
+ if (isClosing) {
+ return;
+ }
+
+ isClosing = true;
+ passwordContainer.passwordBuffer = "";
+ connectButton.connecting = false;
+ connectButton.hasError = false;
+ connectButton.text = qsTr("Connect");
+ connectionMonitor.stop();
+ }
+}
diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml
new file mode 100644
index 0000000..f87fe39
--- /dev/null
+++ b/modules/controlcenter/network/WirelessSettings.qml
@@ -0,0 +1,73 @@
+pragma ComponentBehavior: Bound
+
+import ".."
+import "../components"
+import qs.components
+import qs.components.controls
+import qs.components.effects
+import qs.services
+import qs.config
+import QtQuick
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+
+ required property Session session
+
+ spacing: Appearance.spacing.normal
+
+ SettingsHeader {
+ icon: "wifi"
+ title: qsTr("Network settings")
+ }
+
+ SectionHeader {
+ Layout.topMargin: Appearance.spacing.large
+ title: qsTr("WiFi status")
+ description: qsTr("General WiFi settings")
+ }
+
+ SectionContainer {
+ ToggleRow {
+ label: qsTr("WiFi enabled")
+ checked: Nmcli.wifiEnabled
+ toggle.onToggled: {
+ Nmcli.enableWifi(checked);
+ }
+ }
+ }
+
+ SectionHeader {
+ Layout.topMargin: Appearance.spacing.large
+ title: qsTr("Network information")
+ description: qsTr("Current network connection")
+ }
+
+ SectionContainer {
+ contentSpacing: Appearance.spacing.small / 2
+
+ PropertyRow {
+ label: qsTr("Connected network")
+ value: Nmcli.active ? Nmcli.active.ssid : qsTr("Not connected")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("Signal strength")
+ value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("Security")
+ value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
+ }
+
+ PropertyRow {
+ showTopMargin: true
+ label: qsTr("Frequency")
+ value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
+ }
+ }
+} \ No newline at end of file