diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2026-01-03 17:53:06 +1100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-03 17:53:06 +1100 |
| commit | bdcd13222fc6edc77c779a396900ab909e7d5439 (patch) | |
| tree | f9457f3c91c05ec852f974f239d06aca52a3918e /modules/controlcenter/network | |
| parent | [CI] chore: update flake (diff) | |
| parent | Merge branch 'caelestia-dots:main' into main (diff) | |
| download | caelestia-shell-bdcd13222fc6edc77c779a396900ab909e7d5439.tar.gz caelestia-shell-bdcd13222fc6edc77c779a396900ab909e7d5439.tar.bz2 caelestia-shell-bdcd13222fc6edc77c779a396900ab909e7d5439.zip | |
Merge pull request #906 from atdma/main
controlcenter: many setting panes and minor features
Diffstat (limited to 'modules/controlcenter/network')
| -rw-r--r-- | modules/controlcenter/network/EthernetDetails.qml | 118 | ||||
| -rw-r--r-- | modules/controlcenter/network/EthernetList.qml | 177 | ||||
| -rw-r--r-- | modules/controlcenter/network/EthernetPane.qml | 50 | ||||
| -rw-r--r-- | modules/controlcenter/network/EthernetSettings.qml | 76 | ||||
| -rw-r--r-- | modules/controlcenter/network/NetworkSettings.qml | 98 | ||||
| -rw-r--r-- | modules/controlcenter/network/NetworkingPane.qml | 305 | ||||
| -rw-r--r-- | modules/controlcenter/network/WirelessDetails.qml | 212 | ||||
| -rw-r--r-- | modules/controlcenter/network/WirelessList.qml | 226 | ||||
| -rw-r--r-- | modules/controlcenter/network/WirelessPane.qml | 57 | ||||
| -rw-r--r-- | modules/controlcenter/network/WirelessPasswordDialog.qml | 512 | ||||
| -rw-r--r-- | modules/controlcenter/network/WirelessSettings.qml | 73 |
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 |