nosignal-shell/modules/controlcenter/network/NetworkingPane.qml

567 lines
24 KiB
QML

pragma ComponentBehavior: Bound
import ".."
import "../ethernet"
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
RowLayout {
id: contentLayout
anchors.fill: parent
spacing: 0
Item {
Layout.preferredWidth: Math.floor(parent.width * 0.4)
Layout.minimumWidth: 420
Layout.fillHeight: true
// Left pane - networking list with collapsible sections
StyledFlickable {
id: leftFlickable
anchors.fill: parent
flickableDirection: Flickable.VerticalFlick
contentHeight: leftContent.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: leftFlickable
}
ColumnLayout {
id: leftContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Appearance.padding.large + Appearance.padding.normal
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
spacing: Appearance.spacing.normal
// Settings header above the collapsible sections
RowLayout {
Layout.fillWidth: true
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"
onClicked: {
Nmcli.toggleWifi(null);
}
}
ToggleButton {
toggled: Nmcli.scanning
icon: "wifi_find"
accent: "Secondary"
onClicked: {
Nmcli.rescanWifi();
}
}
ToggleButton {
toggled: !root.session.ethernet.active && !root.session.network.active
icon: "settings"
accent: "Primary"
onClicked: {
if (root.session.ethernet.active || root.session.network.active) {
root.session.ethernet.active = null;
root.session.network.active = null;
} else {
// Toggle to show settings - prefer ethernet if available, otherwise wireless
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
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
StyledText {
text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length)
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
}
StyledText {
Layout.fillWidth: true
text: qsTr("All available ethernet devices")
color: Colours.palette.m3outline
}
Repeater {
id: ethernetRepeater
Layout.fillWidth: true
model: Nmcli.ethernetDevices
delegate: StyledRect {
required property var modelData
Layout.fillWidth: true
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
StateLayer {
function onClicked(): void {
root.session.network.active = null;
root.session.ethernet.active = modelData;
}
}
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.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
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
}
}
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 1
text: modelData.interface || qsTr("Unknown")
}
StyledText {
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
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0)
StateLayer {
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
text: modelData.connected ? "link_off" : "link"
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
}
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
}
}
}
}
CollapsibleSection {
id: wirelessListSection
Layout.fillWidth: true
title: qsTr("Wireless")
expanded: true
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
StyledText {
text: qsTr("Networks (%1)").arg(Nmcli.networks.length)
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
StyledText {
visible: Nmcli.scanning
text: qsTr("Scanning...")
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.small
}
}
StyledText {
Layout.fillWidth: true
text: qsTr("All available WiFi networks")
color: Colours.palette.m3outline
}
Repeater {
id: wirelessRepeater
Layout.fillWidth: true
model: ScriptModel {
values: [...Nmcli.networks].sort((a, b) => {
// Put active/connected network first
if (a.active !== b.active)
return b.active - a.active;
// Then sort by signal strength
return b.strength - a.strength;
})
}
delegate: StyledRect {
required property var modelData
Layout.fillWidth: true
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (modelData && root.session.network.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
StateLayer {
function onClicked(): void {
if (!modelData) {
return;
}
root.session.ethernet.active = null;
root.session.network.active = modelData;
// Check if we need to refresh saved connections when selecting a network
if (modelData.ssid) {
checkSavedProfileForNetwork(modelData.ssid);
}
}
}
RowLayout {
id: wirelessRowLayout
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: wirelessIcon.implicitHeight + Appearance.padding.normal * 2
radius: Appearance.rounding.normal
color: (modelData && modelData.active) ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
MaterialIcon {
id: wirelessIcon
anchors.centerIn: parent
text: Icons.getNetworkIcon(modelData && modelData.strength !== undefined ? modelData.strength : 0)
font.pointSize: Appearance.font.size.large
fill: (modelData && modelData.active) ? 1 : 0
color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 1
text: (modelData && modelData.ssid) ? modelData.ssid : qsTr("Unknown")
}
StyledText {
text: (modelData && modelData.active) ? qsTr("Connected") : ((modelData && modelData.isSecure) ? qsTr("Secured") : qsTr("Open"))
color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
font.weight: (modelData && modelData.active) ? 500 : 400
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: Qt.alpha(Colours.palette.m3primaryContainer, (modelData && modelData.active) ? 1 : 0)
StateLayer {
function onClicked(): void {
if (modelData && modelData.active) {
Nmcli.disconnectFromNetwork();
} else if (modelData) {
handleWirelessConnect(modelData);
}
}
}
MaterialIcon {
id: wirelessConnectIcon
anchors.centerIn: parent
text: (modelData && modelData.active) ? "link_off" : "link"
color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
}
implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2
}
}
}
}
}
}
InnerBorder {
leftThickness: 0
rightThickness: Appearance.padding.normal / 2
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ClippingRectangle {
anchors.fill: parent
anchors.margins: Appearance.padding.normal
anchors.leftMargin: 0
anchors.rightMargin: Appearance.padding.normal / 2
radius: rightBorder.innerRadius
color: "transparent"
// Right pane - networking details/settings
Loader {
id: loader
property var ethernetPane: root.session.ethernet.active
property var wirelessPane: root.session.network.active
property var pane: ethernetPane || wirelessPane
property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "")
anchors.fill: parent
anchors.margins: Appearance.padding.large * 2
opacity: 1
scale: 1
transformOrigin: Item.Center
asynchronous: true
sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings
Behavior on paneId {
SequentialAnimation {
ParallelAnimation {
Anim {
target: loader
property: "opacity"
to: 0
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
Anim {
target: loader
property: "scale"
to: 0.8
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
}
PropertyAction {}
ParallelAnimation {
Anim {
target: loader
property: "opacity"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
target: loader
property: "scale"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
}
onPaneChanged: {
paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "");
}
}
}
InnerBorder {
id: rightBorder
leftThickness: Appearance.padding.normal / 2
}
Component {
id: settings
StyledFlickable {
flickableDirection: Flickable.VerticalFlick
contentHeight: settingsInner.height
NetworkSettings {
id: settingsInner
anchors.left: parent.left
anchors.right: parent.right
session: root.session
}
}
}
Component {
id: ethernetDetails
EthernetDetails {
session: root.session
}
}
Component {
id: wirelessDetails
WirelessDetails {
session: root.session
}
}
}
}
WirelessPasswordDialog {
anchors.fill: parent
session: root.session
z: 1000
}
component Anim: NumberAnimation {
target: loader
duration: Appearance.anim.durations.normal / 2
easing.type: Easing.BezierSpline
}
function checkSavedProfileForNetwork(ssid: string): void {
if (ssid && ssid.length > 0) {
Nmcli.loadSavedConnections(() => {});
}
}
function handleWirelessConnect(network): void {
if (Nmcli.active && Nmcli.active.ssid !== network.ssid) {
Nmcli.disconnectFromNetwork();
Qt.callLater(() => {
connectToWirelessNetwork(network);
});
} else {
connectToWirelessNetwork(network);
}
}
function connectToWirelessNetwork(network): void {
if (network.isSecure) {
const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid);
if (hasSavedProfile) {
Nmcli.connectToNetwork(network.ssid, "", network.bssid, null);
} else {
Nmcli.connectToNetworkWithPasswordCheck(
network.ssid,
network.isSecure,
(result) => {
if (result.needsPassword) {
if (Nmcli.pendingConnection) {
Nmcli.connectionCheckTimer.stop();
Nmcli.immediateCheckTimer.stop();
Nmcli.immediateCheckTimer.checkCount = 0;
Nmcli.pendingConnection = null;
}
root.session.network.showPasswordDialog = true;
root.session.network.pendingNetwork = network;
}
},
network.bssid
);
}
} else {
Nmcli.connectToNetwork(network.ssid, "", network.bssid, null);
}
}
}