mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
quickshell and hyprland additions
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user