quickshell and hyprland additions

This commit is contained in:
2026-03-15 13:56:00 +02:00
parent c9c27d1554
commit 1ad06b82a6
509 changed files with 68371 additions and 19 deletions

View File

@@ -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
}
}
}
}
]
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -0,0 +1,171 @@
pragma ComponentBehavior: Bound
import ".."
import "../components"
import qs.components
import qs.components.controls
import qs.components.containers
import qs.components.effects
import qs.services
import qs.config
import QtQuick
import QtQuick.Controls
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("VPN")
description: qsTr("VPN provider settings")
visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0
}
SectionContainer {
visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0
ToggleRow {
label: qsTr("VPN enabled")
checked: Config.utilities.vpn.enabled
toggle.onToggled: {
Config.utilities.vpn.enabled = checked;
Config.save();
}
}
PropertyRow {
showTopMargin: true
label: qsTr("Providers")
value: qsTr("%1").arg(Config.utilities.vpn.provider.length)
}
TextButton {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
text: qsTr("⚙ Manage VPN Providers")
inactiveColour: Colours.palette.m3secondaryContainer
inactiveOnColour: Colours.palette.m3onSecondaryContainer
onClicked: {
vpnSettingsDialog.open();
}
}
}
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")
}
}
Popup {
id: vpnSettingsDialog
parent: Overlay.overlay
anchors.centerIn: parent
width: Math.min(600, parent.width - Appearance.padding.large * 2)
height: Math.min(700, parent.height - Appearance.padding.large * 2)
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: StyledRect {
color: Colours.palette.m3surface
radius: Appearance.rounding.large
}
StyledFlickable {
anchors.fill: parent
anchors.margins: Appearance.padding.large * 1.5
flickableDirection: Flickable.VerticalFlick
contentHeight: vpnSettingsContent.height
clip: true
VpnSettings {
id: vpnSettingsContent
anchors.left: parent.left
anchors.right: parent.right
session: root.session
}
}
}
}

View File

@@ -0,0 +1,373 @@
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: vpnListSection
Layout.fillWidth: true
title: qsTr("VPN")
expanded: true
Loader {
Layout.fillWidth: true
sourceComponent: Component {
VpnList {
session: root.session
showHeader: false
}
}
}
}
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 vpnPane: root.session && root.session.vpn ? root.session.vpn.active : null
property var ethernetPane: root.session && root.session.ethernet ? root.session.ethernet.active : null
property var wirelessPane: root.session && root.session.network ? root.session.network.active : null
property var pane: vpnPane || ethernetPane || wirelessPane
property string paneId: vpnPane ? ("vpn:" + (vpnPane.name || "")) : (ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings"))
property Component targetComponent: settingsComponent
property Component nextComponent: settingsComponent
function getComponentForPane() {
if (vpnPane)
return vpnDetailsComponent;
if (ethernetPane)
return ethernetDetailsComponent;
if (wirelessPane)
return wirelessDetailsComponent;
return settingsComponent;
}
Component.onCompleted: {
targetComponent = getComponentForPane();
nextComponent = targetComponent;
}
Connections {
target: root.session && root.session.vpn ? root.session.vpn : null
enabled: target !== null
function onActiveChanged() {
// Clear others when VPN is selected
if (root.session && root.session.vpn && root.session.vpn.active) {
if (root.session.ethernet && root.session.ethernet.active)
root.session.ethernet.active = null;
if (root.session.network && root.session.network.active)
root.session.network.active = null;
}
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
}
}
Connections {
target: root.session && root.session.ethernet ? root.session.ethernet : null
enabled: target !== null
function onActiveChanged() {
// Clear others when ethernet is selected
if (root.session && root.session.ethernet && root.session.ethernet.active) {
if (root.session.vpn && root.session.vpn.active)
root.session.vpn.active = null;
if (root.session.network && root.session.network.active)
root.session.network.active = null;
}
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
}
}
Connections {
target: root.session && root.session.network ? root.session.network : null
enabled: target !== null
function onActiveChanged() {
// Clear others when wireless is selected
if (root.session && root.session.network && root.session.network.active) {
if (root.session.vpn && root.session.vpn.active)
root.session.vpn.active = null;
if (root.session.ethernet && root.session.ethernet.active)
root.session.ethernet.active = null;
}
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
}
}
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
}
}
}
Component {
id: vpnDetailsComponent
StyledFlickable {
id: vpnFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: vpnDetailsInner.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: vpnFlickable
}
VpnDetails {
id: vpnDetailsInner
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
session: root.session
}
}
}
WirelessPasswordDialog {
anchors.fill: parent
session: root.session
z: 1000
}
}

View File

@@ -0,0 +1,396 @@
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 qs.utils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
DeviceDetails {
id: root
required property Session session
readonly property var vpnProvider: root.session.vpn.active
readonly property bool providerEnabled: {
if (!vpnProvider || vpnProvider.index === undefined)
return false;
const provider = Config.utilities.vpn.provider[vpnProvider.index];
return provider && typeof provider === "object" && provider.enabled === true;
}
device: vpnProvider
headerComponent: Component {
ConnectionHeader {
icon: "vpn_key"
title: root.vpnProvider?.displayName ?? qsTr("Unknown")
}
}
sections: [
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader {
title: qsTr("Connection status")
description: qsTr("VPN connection settings")
}
SectionContainer {
ToggleRow {
label: qsTr("Enable this provider")
checked: root.providerEnabled
toggle.onToggled: {
if (!root.vpnProvider)
return;
const providers = [];
const index = root.vpnProvider.index;
// Copy providers and update enabled state
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
const p = Config.utilities.vpn.provider[i];
if (typeof p === "object") {
const newProvider = {
name: p.name,
displayName: p.displayName,
interface: p.interface
};
if (checked) {
// Enable this one, disable others
newProvider.enabled = (i === index);
} else {
// Just disable this one
newProvider.enabled = (i === index) ? false : (p.enabled !== false);
}
providers.push(newProvider);
} else {
providers.push(p);
}
}
Config.utilities.vpn.provider = providers;
Config.save();
}
}
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
spacing: Appearance.spacing.normal
TextButton {
Layout.fillWidth: true
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
visible: root.providerEnabled
enabled: !VPN.connecting
inactiveColour: Colours.palette.m3primaryContainer
inactiveOnColour: Colours.palette.m3onPrimaryContainer
text: VPN.connected ? qsTr("Disconnect") : qsTr("Connect")
onClicked: {
VPN.toggle();
}
}
TextButton {
Layout.fillWidth: true
text: qsTr("Edit Provider")
inactiveColour: Colours.palette.m3secondaryContainer
inactiveOnColour: Colours.palette.m3onSecondaryContainer
onClicked: {
editVpnDialog.editIndex = root.vpnProvider.index;
editVpnDialog.providerName = root.vpnProvider.name;
editVpnDialog.displayName = root.vpnProvider.displayName;
editVpnDialog.interfaceName = root.vpnProvider.interface;
editVpnDialog.open();
}
}
TextButton {
Layout.fillWidth: true
text: qsTr("Delete Provider")
inactiveColour: Colours.palette.m3errorContainer
inactiveOnColour: Colours.palette.m3onErrorContainer
onClicked: {
const providers = [];
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
if (i !== root.vpnProvider.index) {
providers.push(Config.utilities.vpn.provider[i]);
}
}
Config.utilities.vpn.provider = providers;
Config.save();
root.session.vpn.active = null;
}
}
}
}
}
},
Component {
ColumnLayout {
spacing: Appearance.spacing.normal
SectionHeader {
title: qsTr("Provider details")
description: qsTr("VPN provider information")
}
SectionContainer {
contentSpacing: Appearance.spacing.small / 2
PropertyRow {
label: qsTr("Provider")
value: root.vpnProvider?.name ?? qsTr("Unknown")
}
PropertyRow {
showTopMargin: true
label: qsTr("Display name")
value: root.vpnProvider?.displayName ?? qsTr("Unknown")
}
PropertyRow {
showTopMargin: true
label: qsTr("Interface")
value: root.vpnProvider?.interface || qsTr("N/A")
}
PropertyRow {
showTopMargin: true
label: qsTr("Status")
value: {
if (!root.providerEnabled)
return qsTr("Disabled");
if (VPN.connecting)
return qsTr("Connecting...");
if (VPN.connected)
return qsTr("Connected");
return qsTr("Enabled (Not connected)");
}
}
PropertyRow {
showTopMargin: true
label: qsTr("Enabled")
value: root.providerEnabled ? qsTr("Yes") : qsTr("No")
}
}
}
}
]
// Edit VPN Dialog
Popup {
id: editVpnDialog
property int editIndex: -1
property string providerName: ""
property string displayName: ""
property string interfaceName: ""
parent: Overlay.overlay
anchors.centerIn: parent
width: Math.min(400, parent.width - Appearance.padding.large * 2)
padding: Appearance.padding.large * 1.5
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
opacity: 0
scale: 0.7
enter: Transition {
Anim {
property: "opacity"
from: 0
to: 1
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
Anim {
property: "scale"
from: 0.7
to: 1
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
exit: Transition {
Anim {
property: "opacity"
from: 1
to: 0
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
Anim {
property: "scale"
from: 1
to: 0.7
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
function closeWithAnimation(): void {
close();
}
Overlay.modal: Rectangle {
color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity)
}
background: StyledRect {
color: Colours.palette.m3surfaceContainerHigh
radius: Appearance.rounding.large
Elevation {
anchors.fill: parent
radius: parent.radius
level: 3
z: -1
}
}
contentItem: ColumnLayout {
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Edit VPN Provider")
font.pointSize: Appearance.font.size.large
font.weight: 500
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller / 2
StyledText {
text: qsTr("Display Name")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
StyledRect {
Layout.fillWidth: true
implicitHeight: 40
color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color {
CAnim {}
}
Behavior on border.color {
CAnim {}
}
StyledTextField {
id: displayNameField
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignLeft
text: editVpnDialog.displayName
onTextChanged: editVpnDialog.displayName = text
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller / 2
StyledText {
text: qsTr("Interface (e.g., wg0, torguard)")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
StyledRect {
Layout.fillWidth: true
implicitHeight: 40
color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color {
CAnim {}
}
Behavior on border.color {
CAnim {}
}
StyledTextField {
id: interfaceNameField
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignLeft
text: editVpnDialog.interfaceName
onTextChanged: editVpnDialog.interfaceName = text
}
}
}
RowLayout {
Layout.topMargin: Appearance.spacing.normal
Layout.fillWidth: true
spacing: Appearance.spacing.normal
TextButton {
Layout.fillWidth: true
text: qsTr("Cancel")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: editVpnDialog.closeWithAnimation()
}
TextButton {
Layout.fillWidth: true
text: qsTr("Save")
enabled: editVpnDialog.interfaceName.length > 0
inactiveColour: Colours.palette.m3primaryContainer
inactiveOnColour: Colours.palette.m3onPrimaryContainer
onClicked: {
const providers = [];
const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex];
const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true;
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
if (i === editVpnDialog.editIndex) {
providers.push({
name: editVpnDialog.providerName,
displayName: editVpnDialog.displayName || editVpnDialog.interfaceName,
interface: editVpnDialog.interfaceName,
enabled: wasEnabled
});
} else {
providers.push(Config.utilities.vpn.provider[i]);
}
}
Config.utilities.vpn.provider = providers;
Config.save();
editVpnDialog.closeWithAnimation();
}
}
}
}
}
}

View File

@@ -0,0 +1,686 @@
pragma ComponentBehavior: Bound
import ".."
import qs.components
import qs.components.controls
import qs.components.effects
import qs.services
import qs.config
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ColumnLayout {
id: root
required property Session session
property bool showHeader: true
property int pendingSwitchIndex: -1
spacing: Appearance.spacing.normal
Connections {
target: VPN
function onConnectedChanged() {
if (!VPN.connected && root.pendingSwitchIndex >= 0) {
const targetIndex = root.pendingSwitchIndex;
root.pendingSwitchIndex = -1;
const providers = [];
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
const p = Config.utilities.vpn.provider[i];
if (typeof p === "object") {
const newProvider = {
name: p.name,
displayName: p.displayName,
interface: p.interface,
enabled: (i === targetIndex)
};
providers.push(newProvider);
} else {
providers.push(p);
}
}
Config.utilities.vpn.provider = providers;
Config.save();
Qt.callLater(function () {
VPN.toggle();
});
}
}
}
TextButton {
Layout.fillWidth: true
text: qsTr("+ Add VPN Provider")
inactiveColour: Colours.palette.m3primaryContainer
inactiveOnColour: Colours.palette.m3onPrimaryContainer
onClicked: {
vpnDialog.showProviderSelection();
}
}
ListView {
id: listView
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
spacing: Appearance.spacing.smaller
model: ScriptModel {
values: Config.utilities.vpn.provider.map((provider, index) => {
const isObject = typeof provider === "object";
const name = isObject ? (provider.name || "custom") : String(provider);
const displayName = isObject ? (provider.displayName || name) : name;
const iface = isObject ? (provider.interface || "") : "";
const enabled = isObject ? (provider.enabled === true) : false;
return {
index: index,
name: name,
displayName: displayName,
interface: iface,
provider: provider,
enabled: enabled
};
})
}
delegate: Component {
StyledRect {
required property var modelData
required property int index
width: ListView.view ? ListView.view.width : undefined
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
StateLayer {
function onClicked(): void {
if (root.session && root.session.vpn) {
root.session.vpn.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.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
MaterialIcon {
id: icon
anchors.centerIn: parent
text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off"
font.pointSize: Appearance.font.size.large
fill: modelData.enabled && VPN.connected ? 1 : 0
color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
maximumLineCount: 1
text: modelData.displayName || qsTr("Unknown")
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller
StyledText {
Layout.fillWidth: true
text: {
if (modelData.enabled && VPN.connected)
return qsTr("Connected");
if (modelData.enabled && VPN.connecting)
return qsTr("Connecting...");
if (modelData.enabled)
return qsTr("Enabled");
return qsTr("Disabled");
}
color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
font.weight: modelData.enabled && VPN.connected ? 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, VPN.connected && modelData.enabled ? 1 : 0)
StateLayer {
enabled: !VPN.connecting
function onClicked(): void {
const clickedIndex = modelData.index;
if (modelData.enabled) {
VPN.toggle();
} else {
if (VPN.connected) {
root.pendingSwitchIndex = clickedIndex;
VPN.toggle();
} else {
const providers = [];
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
const p = Config.utilities.vpn.provider[i];
if (typeof p === "object") {
const newProvider = {
name: p.name,
displayName: p.displayName,
interface: p.interface,
enabled: (i === clickedIndex)
};
providers.push(newProvider);
} else {
providers.push(p);
}
}
Config.utilities.vpn.provider = providers;
Config.save();
Qt.callLater(function () {
VPN.toggle();
});
}
}
}
}
MaterialIcon {
id: connectIcon
anchors.centerIn: parent
text: VPN.connected && modelData.enabled ? "link_off" : "link"
color: VPN.connected && modelData.enabled ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
}
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: "transparent"
StateLayer {
function onClicked(): void {
const providers = [];
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
if (i !== modelData.index) {
providers.push(Config.utilities.vpn.provider[i]);
}
}
Config.utilities.vpn.provider = providers;
Config.save();
}
}
MaterialIcon {
id: deleteIcon
anchors.centerIn: parent
text: "delete"
color: Colours.palette.m3onSurface
}
}
}
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
}
}
}
Popup {
id: vpnDialog
property string currentState: "selection"
property int editIndex: -1
property string providerName: ""
property string displayName: ""
property string interfaceName: ""
parent: Overlay.overlay
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2)
padding: Appearance.padding.large * 1.5
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
opacity: 0
scale: 0.7
enter: Transition {
ParallelAnimation {
Anim {
property: "opacity"
from: 0
to: 1
duration: Appearance.anim.durations.normal
easing.bezierCurve: Appearance.anim.curves.emphasized
}
Anim {
property: "scale"
from: 0.7
to: 1
duration: Appearance.anim.durations.normal
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
exit: Transition {
ParallelAnimation {
Anim {
property: "opacity"
from: 1
to: 0
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.emphasized
}
Anim {
property: "scale"
from: 1
to: 0.7
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
function showProviderSelection(): void {
currentState = "selection";
open();
}
function closeWithAnimation(): void {
close();
}
function showAddForm(providerType: string, defaultDisplayName: string): void {
editIndex = -1;
providerName = providerType;
displayName = defaultDisplayName;
interfaceName = "";
if (currentState === "selection") {
transitionToForm.start();
} else {
currentState = "form";
isClosing = false;
open();
}
}
function showEditForm(index: int): void {
const provider = Config.utilities.vpn.provider[index];
const isObject = typeof provider === "object";
editIndex = index;
providerName = isObject ? (provider.name || "custom") : String(provider);
displayName = isObject ? (provider.displayName || providerName) : providerName;
interfaceName = isObject ? (provider.interface || "") : "";
currentState = "form";
open();
}
Overlay.modal: Rectangle {
color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity)
}
onClosed: {
currentState = "selection";
}
SequentialAnimation {
id: transitionToForm
ParallelAnimation {
Anim {
target: selectionContent
property: "opacity"
to: 0
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
ScriptAction {
script: {
vpnDialog.currentState = "form";
}
}
ParallelAnimation {
Anim {
target: formContent
property: "opacity"
to: 1
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
background: StyledRect {
color: Colours.palette.m3surfaceContainerHigh
radius: Appearance.rounding.large
Elevation {
anchors.fill: parent
radius: parent.radius
level: 3
z: -1
}
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.normal
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
contentItem: Item {
implicitHeight: vpnDialog.currentState === "selection" ? selectionContent.implicitHeight : formContent.implicitHeight
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.normal
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
ColumnLayout {
id: selectionContent
anchors.fill: parent
spacing: Appearance.spacing.normal
visible: vpnDialog.currentState === "selection"
opacity: vpnDialog.currentState === "selection" ? 1 : 0
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
StyledText {
text: qsTr("Add VPN Provider")
font.pointSize: Appearance.font.size.large
font.weight: 500
}
StyledText {
Layout.fillWidth: true
text: qsTr("Choose a provider to add")
wrapMode: Text.WordWrap
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
TextButton {
Layout.topMargin: Appearance.spacing.normal
Layout.fillWidth: true
text: qsTr("NetBird")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: {
const providers = [];
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
providers.push(Config.utilities.vpn.provider[i]);
}
providers.push({
name: "netbird",
displayName: "NetBird",
interface: "wt0"
});
Config.utilities.vpn.provider = providers;
Config.save();
vpnDialog.closeWithAnimation();
}
}
TextButton {
Layout.fillWidth: true
text: qsTr("Tailscale")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: {
const providers = [];
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
providers.push(Config.utilities.vpn.provider[i]);
}
providers.push({
name: "tailscale",
displayName: "Tailscale",
interface: "tailscale0"
});
Config.utilities.vpn.provider = providers;
Config.save();
vpnDialog.closeWithAnimation();
}
}
TextButton {
Layout.fillWidth: true
text: qsTr("Cloudflare WARP")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: {
const providers = [];
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
providers.push(Config.utilities.vpn.provider[i]);
}
providers.push({
name: "warp",
displayName: "Cloudflare WARP",
interface: "CloudflareWARP"
});
Config.utilities.vpn.provider = providers;
Config.save();
vpnDialog.closeWithAnimation();
}
}
TextButton {
Layout.fillWidth: true
text: qsTr("WireGuard (Custom)")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: {
vpnDialog.showAddForm("wireguard", "WireGuard");
}
}
TextButton {
Layout.topMargin: Appearance.spacing.normal
Layout.fillWidth: true
text: qsTr("Cancel")
inactiveColour: Colours.palette.m3secondaryContainer
inactiveOnColour: Colours.palette.m3onSecondaryContainer
onClicked: vpnDialog.closeWithAnimation()
}
}
ColumnLayout {
id: formContent
anchors.fill: parent
spacing: Appearance.spacing.normal
visible: vpnDialog.currentState === "form"
opacity: vpnDialog.currentState === "form" ? 1 : 0
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
StyledText {
text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName)
font.pointSize: Appearance.font.size.large
font.weight: 500
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller / 2
StyledText {
text: qsTr("Display Name")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
StyledRect {
Layout.fillWidth: true
implicitHeight: 40
color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color {
CAnim {}
}
Behavior on border.color {
CAnim {}
}
StyledTextField {
id: displayNameField
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignLeft
text: vpnDialog.displayName
onTextChanged: vpnDialog.displayName = text
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller / 2
StyledText {
text: qsTr("Interface (e.g., wg0, torguard)")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
StyledRect {
Layout.fillWidth: true
implicitHeight: 40
color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color {
CAnim {}
}
Behavior on border.color {
CAnim {}
}
StyledTextField {
id: interfaceNameField
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignLeft
text: vpnDialog.interfaceName
onTextChanged: vpnDialog.interfaceName = text
}
}
}
RowLayout {
Layout.topMargin: Appearance.spacing.normal
Layout.fillWidth: true
spacing: Appearance.spacing.normal
TextButton {
Layout.fillWidth: true
text: qsTr("Cancel")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: vpnDialog.closeWithAnimation()
}
TextButton {
Layout.fillWidth: true
text: qsTr("Save")
enabled: vpnDialog.interfaceName.length > 0
inactiveColour: Colours.palette.m3primaryContainer
inactiveOnColour: Colours.palette.m3onPrimaryContainer
onClicked: {
const providers = [];
const newProvider = {
name: vpnDialog.providerName,
displayName: vpnDialog.displayName || vpnDialog.interfaceName,
interface: vpnDialog.interfaceName
};
if (vpnDialog.editIndex >= 0) {
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
if (i === vpnDialog.editIndex) {
providers.push(newProvider);
} else {
providers.push(Config.utilities.vpn.provider[i]);
}
}
} else {
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
providers.push(Config.utilities.vpn.provider[i]);
}
providers.push(newProvider);
}
Config.utilities.vpn.provider = providers;
Config.save();
vpnDialog.closeWithAnimation();
}
}
}
}
}
}
}

View File

@@ -0,0 +1,232 @@
pragma ComponentBehavior: Bound
import ".."
import "../components"
import qs.components
import qs.components.controls
import qs.components.containers
import qs.components.effects
import qs.services
import qs.config
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ColumnLayout {
id: root
required property Session session
spacing: Appearance.spacing.normal
SettingsHeader {
icon: "vpn_key"
title: qsTr("VPN Settings")
}
SectionHeader {
Layout.topMargin: Appearance.spacing.large
title: qsTr("General")
description: qsTr("VPN configuration")
}
SectionContainer {
ToggleRow {
label: qsTr("VPN enabled")
checked: Config.utilities.vpn.enabled
toggle.onToggled: {
Config.utilities.vpn.enabled = checked;
Config.save();
}
}
}
SectionHeader {
Layout.topMargin: Appearance.spacing.large
title: qsTr("Providers")
description: qsTr("Manage VPN providers")
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ListView {
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
spacing: Appearance.spacing.smaller
model: ScriptModel {
values: Config.utilities.vpn.provider.map((provider, index) => {
const isObject = typeof provider === "object";
const name = isObject ? (provider.name || "custom") : String(provider);
const displayName = isObject ? (provider.displayName || name) : name;
const iface = isObject ? (provider.interface || "") : "";
return {
index: index,
name: name,
displayName: displayName,
interface: iface,
provider: provider,
isActive: index === 0
};
})
}
delegate: Component {
StyledRect {
required property var modelData
required property int index
width: ListView.view ? ListView.view.width : undefined
color: Colours.tPalette.m3surfaceContainerHigh
radius: Appearance.rounding.normal
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
MaterialIcon {
text: modelData.isActive ? "vpn_key" : "vpn_key_off"
font.pointSize: Appearance.font.size.large
color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
text: modelData.displayName
font.weight: modelData.isActive ? 500 : 400
}
StyledText {
text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface"))
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3outline
}
}
IconButton {
icon: modelData.isActive ? "arrow_downward" : "arrow_upward"
visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1
onClicked: {
if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) {
// Move down
const providers = [...Config.utilities.vpn.provider];
const temp = providers[index];
providers[index] = providers[index + 1];
providers[index + 1] = temp;
Config.utilities.vpn.provider = providers;
Config.save();
} else if (!modelData.isActive) {
// Make active (move to top)
const providers = [...Config.utilities.vpn.provider];
const provider = providers.splice(index, 1)[0];
providers.unshift(provider);
Config.utilities.vpn.provider = providers;
Config.save();
}
}
}
IconButton {
icon: "delete"
onClicked: {
const providers = [...Config.utilities.vpn.provider];
providers.splice(index, 1);
Config.utilities.vpn.provider = providers;
Config.save();
}
}
}
implicitHeight: 60
}
}
}
TextButton {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
text: qsTr("+ Add Provider")
inactiveColour: Colours.palette.m3primaryContainer
inactiveOnColour: Colours.palette.m3onPrimaryContainer
onClicked: {
addProviderDialog.open();
}
}
}
SectionHeader {
Layout.topMargin: Appearance.spacing.large
title: qsTr("Quick Add")
description: qsTr("Add common VPN providers")
}
SectionContainer {
contentSpacing: Appearance.spacing.smaller
TextButton {
Layout.fillWidth: true
text: qsTr("+ Add NetBird")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: {
const providers = [...Config.utilities.vpn.provider];
providers.push({
name: "netbird",
displayName: "NetBird",
interface: "wt0"
});
Config.utilities.vpn.provider = providers;
Config.save();
}
}
TextButton {
Layout.fillWidth: true
text: qsTr("+ Add Tailscale")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: {
const providers = [...Config.utilities.vpn.provider];
providers.push({
name: "tailscale",
displayName: "Tailscale",
interface: "tailscale0"
});
Config.utilities.vpn.provider = providers;
Config.save();
}
}
TextButton {
Layout.fillWidth: true
text: qsTr("+ Add Cloudflare WARP")
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
inactiveOnColour: Colours.palette.m3onSurface
onClicked: {
const providers = [...Config.utilities.vpn.provider];
providers.push({
name: "warp",
displayName: "Cloudflare WARP",
interface: "CloudflareWARP"
});
Config.utilities.vpn.provider = providers;
Config.save();
}
}
}
}

View File

@@ -0,0 +1,211 @@
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
}
}
}
}
]
}

View File

@@ -0,0 +1,228 @@
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(() => {});
}
}
}

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,511 @@
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();
}
}

View File

@@ -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")
}
}
}