WIP: Network #129

Draft
Inorishio wants to merge 38 commits from module/wifi into main
27 changed files with 4005 additions and 39 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ CustomRect {
property color activeOnColor property color activeOnColor
property bool checked property bool checked
property real checkedRadius: Tokens.rounding.medium property real checkedRadius: Tokens.rounding.medium
property real defaultRadius: Tokens.rounding.large property real defaultRadius: Math.min(width, height) / 2
property color disabledColor: Qt.alpha(Colors.palette.m3onSurface, 0.1) property color disabledColor: Qt.alpha(Colors.palette.m3onSurface, 0.1)
property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38) property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38)
property bool fillWidth property bool fillWidth
+2
View File
@@ -34,6 +34,7 @@ BusyIndicator {
anchors.fill: parent anchors.fill: parent
bgColor: root.bgColor bgColor: root.bgColor
fgColor: root.fgColor fgColor: root.fgColor
hasEndIndicator: false
padding: root.padding padding: root.padding
rotation: manager.rotation rotation: manager.rotation
startAngle: manager.startFraction * 360 startAngle: manager.startFraction * 360
@@ -53,6 +54,7 @@ BusyIndicator {
Anim { Anim {
duration: manager.completeEndDuration * Tokens.anim.durations.scale duration: manager.completeEndDuration * Tokens.anim.durations.scale
properties: "opacity,internalStrokeWidth" properties: "opacity,internalStrokeWidth"
type: Anim.DefaultEffects
} }
} }
+1
View File
@@ -150,6 +150,7 @@ TextFieldBase {
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.larger
text: root.leadingIcon text: root.leadingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build() fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
} }
+1
View File
@@ -11,6 +11,7 @@ import ZShell.Blobs
import qs.Daemons import qs.Daemons
import qs.Components import qs.Components
import qs.Modules.Bar import qs.Modules.Bar
import qs.Modules.Bar.Popouts.Network as N
import ZShell.Config import ZShell.Config
import qs.Helpers import qs.Helpers
import qs.Drawers import qs.Drawers
-10
View File
@@ -77,16 +77,6 @@ Singleton {
"99": "thunderstorm" "99": "thunderstorm"
}) })
function getAppCategoryIcon(name: string, fallback: string): string {
const categories = DesktopEntries.heuristicLookup(name)?.categories;
if (categories)
for (const [key, value] of Object.entries(categoryIcons))
if (categories.includes(key))
return value;
return fallback;
}
function getAppIcon(name: string, fallback: string): string { function getAppIcon(name: string, fallback: string): string {
const icon = DesktopEntries.heuristicLookup(name)?.icon; const icon = DesktopEntries.heuristicLookup(name)?.icon;
if (fallback !== "undefined") if (fallback !== "undefined")
+122
View File
@@ -0,0 +1,122 @@
pragma Singleton
import Quickshell
import Quickshell.Networking
import QtQuick
Singleton {
id: root
property bool active: false
readonly property list<Network> connectedNetworks: networks.filter(n => n.connected)
property Network connectingNetwork
readonly property list<NetworkDevice> devices: Networking.devices.values
property Network failedNetwork
readonly property list<Network> knownNetworks: networks.filter(n => n.known && !n.connected)
readonly property list<Network> networks: {
const list = [];
for (const d of wifiDevices) {
for (const n of d.networks.values) {
if (!list.includes(n))
list.push(n);
}
}
return list;
}
property bool scanning: false
readonly property list<Network> unknownNetworks: networks.filter(n => !n.known)
readonly property list<WifiDevice> wifiDevices: devices.filter(d => wifiDevice(d))
readonly property bool wifiEnabled: Networking.wifiEnabled
signal networkConnected
function forgetNetwork(target): void {
if (!target) {
return;
}
target.forget();
}
function isSecure(security): bool {
return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae);
}
function requestConnect(network: Network, secret = undefined): void {
connectingNetwork = network;
if (secret === undefined || secret === "") {
network.connect();
} else {
(network as WifiNetwork).connectWithPsk(secret);
}
}
function setScan(value: bool): void {
for (const d of wifiDevices) {
d.scannerEnabled = value;
}
}
function setWifi(value: bool): void {
Networking.wifiEnabled = value;
}
function wifiDevice(dev): bool {
return dev.type === DeviceType.Wifi;
}
onActiveChanged: {
for (const d of wifiDevices)
d.scannerEnabled = active;
}
Timer {
id: flushFailed
interval: 5000
running: root.failedNetwork
onTriggered: {
root.failedNetwork = null;
}
}
Timer {
id: checkConnection
interval: 500
repeat: true
running: root.connectingNetwork
onTriggered: {
var completedNetwork;
switch (root.connectingNetwork.state) {
case ConnectionState.Connecting:
break;
case ConnectionState.Connected:
completedNetwork = root.connectingNetwork;
root.networkConnected();
break;
case ConnectionState.Disconnected:
root.failedNetwork = root.connectingNetwork;
completedNetwork = root.connectingNetwork;
break;
}
if (completedNetwork) {
root.connectingNetwork = null;
}
}
}
Timer {
interval: 10000
running: checkConnection.running
onTriggered: {
root.failedNetwork = root.connectingNetwork;
root.connectingNetwork = null;
}
}
}
+31
View File
@@ -0,0 +1,31 @@
import QtQuick
import ZShell.Config
import qs.Services
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers
CustomText {
required property var modelData
bottomPadding: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.smaller
text: {
if (modelData.type === "known") {
const count = Network.knownNetworks.filter(n => !n.connected).length;
return qsTr("%1 known network%2").arg(count).arg(count === 1 ? "" : "s");
}
if (modelData.type === "unknown") {
const count = Network.unknownNetworks.filter(n => !n.connected).length;
return qsTr("%1 network%2 available").arg(count).arg(count === 1 ? "" : "s");
}
return qsTr("object unknown");
}
topPadding: Tokens.spacing.small
}
+118
View File
@@ -0,0 +1,118 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers
import qs.Helpers.Network
import qs.Services
ConnectedRect {
id: networkItem
readonly property int connectedCount: Network.connectedNetworks.length
readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Tokens.padding.largeIncreased
readonly property bool inConnectedGroup: index < connectedCount
// sectioning first / last item in list reminder
// if index === network.connectedNetworks.length - 1
// if index === network.connectedNetworks.length - 1 + network.knownNetworks.length - 2
// if index === network.connectedNetworks.length - 1 + network.knownNetworks.length
// list.model.values.length - 1
required property int index
readonly property bool isSecure: Network.isSecure(modelData.security)
required property ListView list
required property var modelData
property int verticalPadding: Tokens.padding.smaller
Layout.fillWidth: true
first: !inConnectedGroup && index === connectedCount + 1
implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
last: inConnectedGroup ? index === connectedCount - 1 : index === (list?.count ?? -1)
radius: Tokens.rounding.small
RowLayout {
id: rowLayout
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0
anchors.fill: parent
anchors.leftMargin: networkItem.horizontalPadding
anchors.rightMargin: networkItem.horizontalPadding
Column {
id: column
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.extraSmall
Layout.rightMargin: Tokens.spacing.extraSmall
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface
elide: Text.ElideRight
text: networkItem.modelData.name ? networkItem.modelData.name : qsTr("Unknown Network")
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: networkItem.modelData.connected ? qsTr("Connected: %1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") : qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%")
}
}
Item {
Layout.alignment: Qt.AlignRight
implicitHeight: icon.height
implicitWidth: icon.width
MaterialIcon {
id: icon
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large
opacity: networkItem.connecting ? 0 : 1
text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure)
Behavior on opacity {
Anim {
}
}
}
Loader {
active: opacity > 0
anchors.fill: parent
opacity: networkItem.connecting ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
running: true
}
}
}
}
StateLayer {
color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: {
if (networkItem.isSecure && !networkItem.known) {
PopupManager.requestOpen(Qt.resolvedUrl("../../Modules/Bar/Popouts/Network/PSKOverlay.qml"), {
network: networkItem.modelData
});
} else {
Network.requestConnect(networkItem.modelData);
}
}
}
}
+74
View File
@@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Services
import qs.Components
import qs.Modules.Settings.Common
CustomSwitch {
id: toggleItem
readonly property alias bg: bg
property alias first: bg.first
property alias last: bg.last
property string subtext
Layout.fillWidth: true
cLayer: 2
horizontalPadding: Tokens.padding.largeIncreased
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
indicator.anchors.right: right
indicator.anchors.rightMargin: toggleItem.horizontalPadding
indicator.anchors.verticalCenter: verticalCenter
verticalPadding: Tokens.padding.normal
background: ConnectedRect {
id: bg
StateLayer {
id: stateLayer
manualPressOverride: toggleItem.pressed
}
}
contentItem: Item {
anchors.left: parent.left
anchors.leftMargin: toggleItem.horizontalPadding
anchors.right: toggleItem.indicator.left
anchors.rightMargin: Tokens.spacing.normal
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
ColumnLayout {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: label
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Tokens.font.size.smaller
text: toggleItem.text
}
CustomText {
id: subtext
Layout.fillWidth: true
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: toggleItem.subtext
visible: toggleItem.subtext
}
}
}
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
}
-9
View File
@@ -206,15 +206,6 @@ GridLayout {
} }
} }
DelegateChoice {
roleValue: "network"
delegate: EntryWrapper {
NetworkWidget {
}
}
}
DelegateChoice { DelegateChoice {
roleValue: "media" roleValue: "media"
+22
View File
@@ -105,6 +105,28 @@ WidgetBase {
} }
} }
DelegateChoice {
roleValue: "bluetooth"
delegate: EntryWrapper {
name: "bluetooth"
BluetoothWidget {
}
}
}
DelegateChoice {
roleValue: "network"
delegate: EntryWrapper {
name: "network"
NetworkWidget {
}
}
}
DelegateChoice { DelegateChoice {
roleValue: "power" roleValue: "power"
@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Services
import qs.Components
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
animate: true
color: Colors.palette.m3onSurface // Network.connected ? root.textColor : Colors.palette.m3error
fill: 1
font.pointSize: Tokens.font.size.larger
text: "bluetooth"
}
@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Services
import qs.Components
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
animate: true
color: Colors.palette.m3onSurface // Network.connected ? root.textColor : Colors.palette.m3error
fill: 1
font.pointSize: Tokens.font.size.larger
text: "android_wifi_4_bar"
}
+385
View File
@@ -0,0 +1,385 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQml.Models
import Quickshell
import Quickshell.Bluetooth
import ZShell.Components
import ZShell.Config
import qs.Services
import qs.Components
import qs.Helpers
CustomClippingRect {
id: root
readonly property list<var> btConnected: Bluetooth.devices.values.filter(d => d.connected)
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
implicitWidth: 500
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.larger
spacing: Tokens.spacing.extraSmall / 2
CustomText {
Layout.rightMargin: Tokens.padding.extraSmall
Layout.topMargin: Tokens.padding.normal
font.pointSize: Tokens.font.size.large
text: qsTr("Bluetooth")
}
Toggle {
checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type
first: true
subtext: qsTr("Toggle bluetooth device state")
text: checked ? qsTr("Enabled") : qsTr("Disabled")
onToggled: {
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
if (adapter)
adapter.enabled = checked;
}
}
Toggle {
checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type
last: root.btConnected.length === 0
subtext: qsTr("Enable scanning for new devices")
text: qsTr("Discover")
onToggled: {
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
if (adapter)
adapter.discovering = checked;
}
}
ListView {
id: rows
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
section.property: "connected"
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
}
delegate: DelegateChooser {
role: "isDivider"
DelegateChoice {
roleValue: true
delegate: StatusDivider {
}
}
DelegateChoice {
delegate: BluetoothItem {
list: rows
width: rows.width
}
}
}
displaced: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
model: ScriptModel {
objectProp: "address"
values: {
const byName = (a, b) => a.name.localeCompare(b.name);
return [...[...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5),
{
isDivider: true
},
...[...Bluetooth.devices.values].filter(d => !d.connected).sort(byName).slice(0, 5)];
}
}
move: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
}
section.delegate: Rectangle {
required property string section
color: "transparent"
height: childrenRect.height
width: ListView.view.width
CustomText {
text: parent.section
}
}
}
}
component BluetoothItem: ConnectedRect {
id: btItem
readonly property int connectedCount: root.btConnected.length
readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Tokens.padding.largeIncreased
readonly property bool inConnectedGroup: index < connectedCount
required property int index
required property ListView list
readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type
required property BluetoothDevice modelData
property int verticalPadding: Tokens.padding.smaller
first: !inConnectedGroup && index === connectedCount + 1
implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1
radius: Tokens.rounding.small
Timer {
interval: 50
running: btItem.ListView.delayRemove
onTriggered: btItem.ListView.delayRemove = false
}
Connections {
function onConnectedChanged(): void {
if (!btItem.modelData.connected)
btItem.ListView.delayRemove = true;
}
function onPairedChanged(): void {
if (btItem.modelData.paired)
btItem.modelData.connect();
}
target: btItem.modelData
}
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.leftMargin: btItem.horizontalPadding
anchors.rightMargin: btItem.horizontalPadding
Column {
id: column
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.extraSmall
Layout.rightMargin: Tokens.spacing.extraSmall
elide: Text.ElideRight
text: btItem.modelData.name
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: btItem.modelData.connected ? (btItem.modelData.batteryAvailable ? qsTr("%1 battery left").arg(Math.round(btItem.modelData.battery * 100) + "%") : qsTr("Battery status unknown")) : btItem.modelData.address
}
}
Item {
Layout.alignment: Qt.AlignRight
implicitHeight: icon.height
implicitWidth: icon.width
MaterialIcon {
id: icon
font.pointSize: Tokens.font.size.large
opacity: btItem.connecting ? 0 : 1
text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_unknown"
visible: btItem.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type
Behavior on opacity {
Anim {
}
}
}
Loader {
active: opacity > 0
anchors.fill: parent
opacity: btItem.connecting ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
running: true
}
}
}
ButtonRow {
spacing: Tokens.spacing.small
IconTextButton {
font.pointSize: Tokens.font.size.small
icon: btItem.modelData.connected ? "link_off" : btItem.modelData.paired ? "link" : "add_link"
text: btItem.modelData.connected ? qsTr("Disconnect") : btItem.modelData.paired ? qsTr("Connect") : qsTr("Pair")
onClicked: {
if (btItem.modelData.connected)
btItem.modelData.disconnect();
else if (btItem.modelData.paired)
btItem.modelData.connect();
else if (!btItem.modelData.paired)
btItem.modelData.pair();
else if (btItem.modelData.pairing)
btItem.modelData.cancelPair();
}
}
}
}
}
component ConnectedRect: CustomRect {
id: bg
property bool first
property bool last
bottomLeftRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
bottomRightRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
color: Colors.tPalette.m3surfaceContainer
topLeftRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
topRightRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
Behavior on bottomLeftRadius {
Anim {
}
}
Behavior on bottomRightRadius {
Anim {
}
}
Behavior on topLeftRadius {
Anim {
}
}
Behavior on topRightRadius {
Anim {
}
}
}
component StatusDivider: CustomText {
bottomPadding: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.smaller
text: {
const devices = Bluetooth.devices.values; // qmllint disable unresolved-type
const connectedCount = devices.filter(d => d.connected).length;
let available = qsTr("%1 device%2 available").arg(devices.length - connectedCount).arg(devices.length === 1 ? "" : "s");
return available;
}
topPadding: Tokens.spacing.small
}
component Toggle: CustomSwitch {
id: toggleItem
readonly property alias bg: bg
property alias first: bg.first
property alias last: bg.last
property string subtext
Layout.fillWidth: true
cLayer: 2
horizontalPadding: Tokens.padding.largeIncreased
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
indicator.anchors.right: right
indicator.anchors.rightMargin: toggleItem.horizontalPadding
indicator.anchors.verticalCenter: verticalCenter
verticalPadding: Tokens.padding.normal
background: ConnectedRect {
id: bg
StateLayer {
id: stateLayer
manualPressOverride: toggleItem.pressed
}
}
contentItem: Item {
anchors.left: parent.left
anchors.leftMargin: toggleItem.horizontalPadding
anchors.right: toggleItem.indicator.left
anchors.rightMargin: Tokens.spacing.normal
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
Column {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: label
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
font: {
const f = Qt.font(label.font);
f.pointSize = Tokens.font.size.smaller;
return f;
}
text: toggleItem.text
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font: {
const f = Qt.font(subtext.font);
f.pointSize = Tokens.font.size.small;
return f;
}
text: toggleItem.subtext
visible: toggleItem.subtext
}
}
}
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
}
}
+7
View File
@@ -82,6 +82,13 @@ Item {
} }
} }
Popout {
name: "bluetooth"
sourceComponent: BluetoothPopout {
}
}
Popout { Popout {
name: "updates" name: "updates"
@@ -0,0 +1,71 @@
import QtQuick
import ZShell.Config
import qs.Services
import qs.Helpers.Network
import qs.Components
CustomRect {
id: root
readonly property bool closing: PopupManager.closed
readonly property url currentPopup: PopupManager.currentPopup
property bool shouldBeVisible: loader.status === Loader.Ready
color: Qt.alpha(Colors.palette.m3shadow, 0.3)
opacity: shouldBeVisible && !closing ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
onVisibleChanged: {
if (!visible) {
PopupManager.requestClose();
PopupManager.currentPopup = "";
}
}
CustomMouseArea {
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: {
const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x;
const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y;
if (insideItemHeight && insideItemWidth)
return;
PopupManager.requestClose();
}
}
Loader {
id: loader
anchors.centerIn: parent
Connections {
function onCurrentPopupChanged(): void {
if (PopupManager.currentPopup) {
loader.setSource(PopupManager.currentPopup, PopupManager.currentPopupProps);
} else {
loader.source = null;
}
}
target: PopupManager
}
}
Connections {
function onNetworkConnected(): void {
PopupManager.requestClose();
}
target: Network
}
}
+102
View File
@@ -0,0 +1,102 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Components
import ZShell.Config
import qs.Services
import qs.Components
import qs.Helpers.Network
CustomClippingRect {
id: root
required property var network
color: Colors.palette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
radius: Tokens.rounding.large
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.extraLarge
spacing: Tokens.spacing.medium
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
font.pointSize: Tokens.font.size.extraLarge * 2
text: "lock"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3onSurfaceVariant
text: qsTr("Connect to %1").arg(root.network.name)
}
CustomTextField {
id: passwordField
Layout.fillWidth: true
Layout.preferredWidth: 400
echoMode: CustomTextField.Password
errorText: qsTr("Failed to connect with given password")
isError: Network.failedNetwork
leadingIcon: "lock"
passwordMaskDelay: 5000
placeholderText: qsTr("Input password")
}
ButtonRow {
id: buttonRow
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
IconTextButton {
fillWidth: true
font.pointSize: Tokens.font.size.normal
icon: "close"
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
isRound: true
isToggle: false
shapeMorph: true
text: "Cancel"
onClicked: PopupManager.requestClose()
}
IconTextButton {
fillWidth: true
font.pointSize: Tokens.font.size.normal
icon: Network.connectingNetwork ? "" : "check"
inactiveColor: Network.connectingNetwork ? Colors.palette.m3primaryContainer : Network.failedNetwork ? Colors.palette.m3error : Colors.palette.m3primary
inactiveOnColor: Network.connectingNetwork ? Colors.palette.m3onPrimaryContainer : Network.failedNetwork ? Colors.palette.m3onError : Colors.palette.m3onPrimary
isRound: true
isToggle: false
shapeMorph: true
text: Network.connectingNetwork ? "" : "Apply"
onClicked: Network.requestConnect(root.network, passwordField.text)
Loader {
active: opacity > 0
anchors.centerIn: parent
opacity: Network.connectingNetwork ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
implicitSize: buttonRow.height - Tokens.padding.normal
running: true
}
}
}
}
}
}
@@ -0,0 +1,22 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: root
property bool closed: true
property url currentPopup: ""
property var currentPopupProps: ({})
function requestClose(): void {
closed = true;
}
function requestOpen(source: url, properties = {}): void {
currentPopupProps = properties;
currentPopup = source;
closed = false;
}
}
+122 -16
View File
@@ -1,36 +1,142 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Networking
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components import Quickshell
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Modules import qs.Services
import qs.Helpers as Helpers import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers.Network
Item { CustomClippingRect {
id: root id: root
required property var wrapper required property var wrapper
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: networkPopContent.height + networkPopContent.anchors.margins * 2
implicitWidth: 500 + 8 * 2
radius: (20 - Tokens.padding.small) * Tokens.rounding.scale
Component.onCompleted: Network.active = true
Component.onDestruction: Network.active = false
ColumnLayout { ColumnLayout {
id: layout id: networkPopContent
spacing: 8 anchors.left: parent.left
anchors.margins: Tokens.padding.large
anchors.right: parent.right
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
Repeater { CustomText {
model: Helpers.Network.devices Layout.preferredHeight: visible ? implicitHeight : 0
Layout.rightMargin: Tokens.padding.extraSmall
font.pointSize: Tokens.font.size.large
text: qsTr("Wifi")
}
CustomRadioButton { Toggle {
id: network Layout.preferredHeight: visible ? implicitHeight : 0
checked: Network.wifiEnabled
first: true
last: Network.connectedNetworks.length === 0
subtext: qsTr("Toggle WiFi device state")
text: checked ? qsTr("Enabled") : qsTr("Disabled")
required property NetworkDevice modelData onToggled: Network.setWifi(checked)
}
checked: Helpers.Network.activeDevice?.name === modelData.name ListView {
text: modelData.description id: rows
visible: modelData.name !== "lo"
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
section.property: "connected"
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
}
delegate: DelegateChooser {
role: "isDivider"
DelegateChoice {
roleValue: true
delegate: NetworkDivider {
}
}
DelegateChoice {
delegate: NetworkItem {
list: rows
width: rows.width
}
}
}
displaced: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
model: ScriptModel {
values: {
const byStrengthName = (a, b) => b.signalStrength - a.signalStrength || a.name.localeCompare(b.name);
const cNetworks = [...Network.connectedNetworks];
const kNetworks = [...Network.knownNetworks];
const uNetworks = [...Network.unknownNetworks];
return [...cNetworks.sort(byStrengthName), ...(kNetworks.length > 0 ? [
{
isDivider: true,
type: "known"
},
...kNetworks.sort(byStrengthName)] : []), ...(uNetworks.length > 0 ? [
{
isDivider: true,
type: "unknown"
},
...uNetworks.sort(byStrengthName)] : [])];
}
}
move: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
}
section.delegate: Rectangle {
required property string section
color: "transparent"
height: childrenRect.height
width: ListView.view.width
CustomText {
text: parent.section
}
} }
} }
} }
NetworkOverlay {
anchors.fill: parent
}
} }
+23 -2
View File
@@ -2,8 +2,8 @@ pragma Singleton
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Modules.Settings.Common import qs.Modules.Settings.Common
import qs.Modules.Settings.Pages import qs.Modules.Settings.Pages
import qs.Modules.Settings.Pages.Wallpaper import qs.Modules.Settings.Pages.Wallpaper
@@ -13,6 +13,7 @@ import qs.Modules.Settings.Pages.Panels
import qs.Modules.Settings.Pages.Panels.Bar import qs.Modules.Settings.Pages.Panels.Bar
import qs.Modules.Settings.Pages.Panels.Sidebar import qs.Modules.Settings.Pages.Panels.Sidebar
import qs.Modules.Settings.Pages.Services import qs.Modules.Settings.Pages.Services
import qs.Modules.Settings.Pages.Network
import qs.Services import qs.Services
QtObject { QtObject {
@@ -47,7 +48,27 @@ QtObject {
// Connectivity // Connectivity
Component { Component {
PlaceholderComp {} StackPage {
Component {
NetworkPage {}
}
Component {
AllNetworks {}
}
Component {
SavedNetworks {}
}
Component {
AddNetwork {}
}
Component {
OldNetwork {}
}
}
}, },
Component { Component {
PlaceholderComp {} PlaceholderComp {}
@@ -0,0 +1,256 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Internal
import ZShell.Blobs
import ZShell.Services
import ZShell.Components
import qs.Components
import qs.Modules.Settings.Common
PageBase {
id: root
property bool connecting: false
property bool failed: false
// Security model: index 0 = none, 1 = WPA/WPA2/WPA3 personal.
readonly property bool secured: securitySelect.active !== noneItem
property bool success: false
function submit(): void {
const ssid = ssidField.text.trim();
if (ssid.length === 0) {
ssidField.isError = true;
ssidField.forceActiveFocus();
return;
}
if (root.secured && passwordField.text.length < 8) {
passwordField.isError = true;
passwordField.forceActiveFocus();
return;
}
root.failed = false;
root.connecting = true;
Nmcli.addHiddenNetwork(ssid, root.secured ? passwordField.text : "", root.secured ? "wpa" : "none", hiddenToggle.checked, result => {
root.connecting = false;
if (result && result.success) {
root.success = true;
root.nState.closeSubPage();
} else {
root.failed = true;
if (root.secured)
passwordField.isError = true;
// Clean up the half-created profile so a retry starts fresh.
Nmcli.forgetNetwork(ssid);
}
});
}
isSubPage: true
title: Tr.tr("Add network")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.large
width: root.cappedWidth
Connections {
function onSubPageClosed(): void {
if (root.success)
return;
const ssid = ssidField.text.trim();
if (ssid)
Nmcli.forgetNetwork(ssid);
}
target: root.nState
}
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.padding.extraSmall
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.small
text: Tr.tr("Enter the details below to manually connect to a network.")
wrapMode: Text.WordWrap
}
CustomTextField {
id: ssidField
Layout.fillWidth: true
Layout.topMargin: Tokens.spacing.extraSmall
errorText: Tr.tr("Network name is required")
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
leadingIcon: "wifi"
// TRANSLATORS: SSID is a protocol term, leave it untranslated
placeholderText: Tr.tr("Network name (SSID)")
// TRANSLATORS: sample network name, translate the e.g. but the name is a placeholder
supportingText: Tr.tr("e.g. MyHiddenNetwork")
onAccepted: root.secured ? passwordField.forceActiveFocus() : root.submit()
}
ToggleRow {
id: hiddenToggle
checked: true
first: true
subtext: Tr.tr("Actively probe for a network that doesn't broadcast its name")
text: Tr.tr("Hidden network")
}
SelectRow {
id: securitySelect
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
fallbackIcon: "lock"
// TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated
fallbackText: Tr.tr("WPA/WPA2/WPA3 Personal")
// label: Tr.tr("Security")
last: !root.secured
Behavior on bottomLeftRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on bottomRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
menuItems: [
MenuItem {
icon: "lock"
// TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated
text: Tr.tr("WPA/WPA2/WPA3 Personal")
},
MenuItem {
id: noneItem
icon: "lock_open"
text: Tr.trCtx("None (open)", "wifi security type")
}
]
}
Item {
Layout.bottomMargin: root.secured ? 0 : -parent.spacing
Layout.fillWidth: true
implicitHeight: root.secured ? passwordField.implicitHeight : 0
opacity: root.secured ? 1 : 0
Behavior on Layout.bottomMargin {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on implicitHeight {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
CustomTextField {
id: passwordField
anchors.left: parent.left
anchors.right: parent.right
echoMode: TextInput.Password
enabled: root.secured
errorText: root.failed ? Tr.tr("Connection failed — check the password") : Tr.tr("Password must be at least 8 characters")
leadingIcon: "key"
placeholderText: Tr.tr("Password")
// TRANSLATORS: WPA is a protocol name, leave it untranslated
supportingText: Tr.tr("WPA passwords are at least 8 characters")
onAccepted: root.submit()
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.topMargin: Tokens.spacing.extraSmall - parent.spacing
spacing: Tokens.spacing.small
TextButton {
Layout.fillHeight: true
horizontalPadding: Tokens.padding.extraLarge
isRound: true
text: Tr.trCtx("Cancel", "button")
type: TextButton.Tonal
onClicked: root.nState.closeSubPage()
}
// Connect button — swaps to a loading spinner while connecting,
// matching the Wi-Fi list connect animation.
ButtonBase {
id: connectBtn
implicitHeight: connectMetrics.height + Tokens.padding.medium * 2
implicitWidth: connectMetrics.width + Tokens.padding.extraLarge * 2
// inactiveColour: Colours.palette.m3primary
// inactiveOnColour: Colours.palette.m3onPrimary
isRound: true
shapeMorph: true
// stateLayer.disabled: root.connecting || ssidField.text.trim().length === 0
onClicked: {
if (!root.connecting && ssidField.text.trim().length > 0)
root.submit();
}
TextMetrics {
id: connectMetrics
font: connectBtn.font
text: Tr.tr("Connect")
}
Anim {
id: connectContent
// anchors.centerIn: parent
// inAnimType: Anim.SlowEffects
// outAnimType: Anim.SlowEffects
// sourceComp: root.connecting ? connectLoadingComp : connectTextComp
}
Component {
id: connectLoadingComp
LoadingIndicator {
color: connectBtn.onColour
implicitSize: Math.round(Tokens.font.body.medium.pointSize * 1.4)
}
}
Component {
id: connectTextComp
CustomText {
color: connectBtn.onColour
font: connectBtn.font
text: connectMetrics.text
}
}
}
}
}
}
@@ -0,0 +1,345 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Helpers.Network
import qs.Services
import qs.Modules.Settings.Common
PageBase {
id: root
flickable.bottomMargin: Tokens.padding.extraExtraLarge * 2 // Extra scrolling space at the bottom
isSubPage: true
title: Tr.tr("All networks")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
// spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
Timer {
interval: 300 // GlobalConfig.nexus.networkRescanInterval
repeat: true
running: root.visible && Nmcli.wifiEnabled
triggeredOnStart: true
onTriggered: Nmcli.rescanWifi()
}
ConnectedRect {
Layout.fillWidth: true
first: true
implicitHeight: headerLayout.implicitHeight + Tokens.padding.medium * 2
RowLayout {
id: headerLayout
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1
// spacing: Tokens.spacing.extraSmall / 2
CustomText {
font: Tokens.font.title.small
text: Tr.trCtx("Filters", "network filter heading")
}
Item {
Layout.fillWidth: true
}
FilterButton {
id: savedFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return Nmcli.hasSavedProfile(ap.ssid);
}
bottomLeftRadius: pressed ? pressedRadius : implicitHeight / 2
text: Tr.trCtx("Saved", "network filter")
topLeftRadius: pressed ? pressedRadius : implicitHeight / 2
Behavior on bottomLeftRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on topLeftRadius {
Anim {
type: Anim.DefaultEffects
}
}
}
FilterButton {
id: secureFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return ap.security !== "none";
}
text: Tr.trCtx("Secured", "network filter")
}
FilterButton {
id: highFreqFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return ap.frequency >= 4900 && ap.frequency <= 5900;
}
text: Tr.trCtx("5 GHz", "network filter")
}
FilterButton {
id: lowFreqFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return ap.frequency >= 2400 && ap.frequency <= 2500;
}
bottomRightRadius: pressed ? pressedRadius : implicitHeight / 2
text: Tr.trCtx("2.4 GHz", "network filter")
topRightRadius: pressed ? pressedRadius : implicitHeight / 2
Behavior on bottomRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on topRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}
NetworkList {
id: networkList
function networkFilter(ap: Nmcli.AccessPoint): bool {
return savedFilter.filter(ap) && secureFilter.filter(ap) && highFreqFilter.filter(ap) && lowFreqFilter.filter(ap);
}
enableFilter: true
last: true
// nState: root.nState
}
}
component FilterButton: IconTextButton {
id: filter
property int filterState // 0 = default, 1 = on, 2 = negate
function filter(ap: Nmcli.AccessPoint): bool {
if (filterState === 0)
return true;
if (filterState === 1)
return internalFilter(ap);
return !internalFilter(ap);
}
function internalFilter(ap: Nmcli.AccessPoint): bool {
return true;
}
defaultRadius: filterState > 0 ? implicitHeight / 2 : Tokens.rounding.small
icon: ["check_indeterminate_small", "check", "close"][filterState]
// inactiveColour: [Colours.palette.m3secondaryContainer, Colours.palette.m3secondary, Colours.palette.m3tertiary][filterState]
// inactiveOnColour: [Colours.palette.m3onSecondaryContainer, Colours.palette.m3onSecondary, Colours.palette.m3onTertiary][filterState]
pressedRadius: Tokens.rounding.extraSmall
// spacing: Tokens.spacing.extraSmall
onClicked: filterState = (filterState + 1) % 3
}
component NetworkList: ItemList {
id: netList
property bool enableFilter
property int limit: 0 // 0 = show all
// required property NexusState nState
signal networkSelected(ap: Nmcli.AccessPoint)
function networkFilter(ap: Nmcli.AccessPoint): bool {
return true;
}
extraHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0 // Inline so it isn't affected by anim
list.anchors.top: scanningIndicator.bottom
placeholderIcon: Nmcli.wifiEnabled ? "wifi_find" : "signal_wifi_off"
placeholderText: Nmcli.wifiEnabled ? Tr.tr("No networks found") : Tr.tr("Wi-Fi disabled")
showList: Nmcli.wifiEnabled
delegate: StateLayer {
id: network
property bool currentSelected
required property int index
required property var modelData
property real textOpacity: disabled ? 0.5 : 1
anchors.fill: undefined
anchors.left: netList.list.contentItem.left
anchors.right: netList.list.contentItem.right
bottomLeftRadius: netList?.last && index === netList.list.count - 1 ? Tokens.rounding.extraLarge : radius
bottomRightRadius: netList?.last && index === netList.list.count - 1 ? Tokens.rounding.extraLarge : radius
// disabled: currentSelected || Nmcli.connectingSsid() === modelData.ssid
implicitHeight: networkLayout.implicitHeight + networkLayout.anchors.margins * 2
radius: Tokens.rounding.extraSmall
Behavior on textOpacity {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: {
if (!modelData.active) {
NetworkConnection.handleConnect(modelData);
currentSelected = true;
netList.networkSelected(modelData);
} else {
// Active network: open its detail/settings sub-page.
netList.nState.selectedNetworkSsid = modelData.ssid;
netList.nState.networkDetailsFromSaved = false;
netList.nState.openSubPage(3);
}
}
Connections {
function onActiveChanged(): void {
if (network.modelData.active)
network.currentSelected = false;
}
target: network.modelData
}
Connections {
function onNetworkSelected(ap: Nmcli.AccessPoint): void {
if (ap !== network.modelData)
network.currentSelected = false;
}
target: netList
}
RowLayout {
id: networkLayout
anchors.fill: parent
anchors.leftMargin: Tokens.padding.extraLarge
anchors.margins: Tokens.padding.large
anchors.rightMargin: Tokens.padding.extraLarge
// spacing: Tokens.spacing.extraSmall
MaterialIcon {
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
opacity: network.textOpacity
text: Icons.getNetworkIcon(network.modelData.strength)
}
ColumnLayout {
Layout.fillWidth: true
opacity: network.textOpacity
// spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font: Tokens.font.body.small
text: network.modelData.ssid
}
CustomText {
Layout.fillWidth: true
color: Colours.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.label.small
text: {
const net = network.modelData;
let status = "";
if (net.active)
status = Tr.trCtx("Connected", "network connected");
else if (Nmcli.hasSavedProfile(net.ssid))
status = Tr.trCtx("Saved", "network saved");
if (status)
// TRANSLATORS: %1 = security type, %2 = connection status
return Tr.trCtx("Security: %1 • %2", "network security and status").arg(net.security).arg(status);
return Tr.trCtx("Security: %1", "network security").arg(net.security);
}
}
}
// Anim {
// // sourceComp: Nmcli.connectingSsid() === network.modelData.ssid ? loadingComp : iconComp
//
// Component {
// id: iconComp
//
// MaterialIcon {
// color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
// fontStyle: Tokens.font.icon.medium
// opacity: network.textOpacity
// text: network.modelData.active ? "settings" : "lock"
// }
// }
//
// Component {
// id: loadingComp
//
// LoadingIndicator {
// implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
// }
// }
// }
}
}
// model: ScriptModel {
// values: {
// const connecting = Nmcli.connectingSsid();
// // Lower rank sorts higher in the list
// const rank = n => n.active ? 0 : n.ssid === connecting ? 1 : Nmcli.hasSavedProfile(n.ssid) ? 2 : 3;
// const sorted = [...Nmcli.networks].sort((a, b) => rank(a) - rank(b) || b.strength - a.strength);
// if (netList.limit > 0 && sorted.length > netList.limit)
// sorted.length = netList.limit;
// return netList.enableFilter ? sorted.filter(netList.networkFilter) : sorted;
// }
// }
// StyledProgressBar {
// id: scanningIndicator
//
// anchors.left: parent.left
// anchors.margins: 1
// anchors.right: parent.right
// implicitHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0
// indeterminate: true
//
// Behavior on implicitHeight {
// Anim {
// type: Anim.DefaultEffects
// }
// }
// }
}
}
@@ -0,0 +1,55 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers.Network
import qs.Services
PageBase {
id: root
title: qsTr("Begin page")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
NavRow {
first: true
icon: "dock_to_bottom"
// settingAnchor: "panels-bar"
text: qsTr("Show all networks")
onClicked: root.sState.openSubPage(1)
}
NavRow {
icon: "dock_to_bottom"
text: qsTr("Saved networks")
onClicked: root.sState.openSubPage(2)
}
NavRow {
icon: "dock_to_bottom"
text: qsTr("Add network")
onClicked: root.sState.openSubPage(3)
}
NavRow {
icon: "dock_to_bottom"
last: true
text: qsTr("Old Network Page")
onClicked: root.sState.openSubPage(4)
}
}
}
@@ -0,0 +1,252 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers.Network
import qs.Services
PageBase {
id: root
isSubPage: true
title: qsTr("Old network page")
Component.onCompleted: Network.active = true
Component.onDestruction: Network.active = false
ColumnLayout {
id: networkContent
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
Toggle {
Layout.preferredHeight: visible ? implicitHeight : 0
checked: Network.wifiEnabled
first: true
last: Network.connectedNetworks.length === 0
subtext: qsTr("Toggle WiFi device state")
text: checked ? qsTr("Enabled") : qsTr("Disabled")
onToggled: Network.setWifi(checked)
}
ListView {
id: rows
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
section.property: "connected"
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
}
delegate: DelegateChooser {
role: "isDivider"
DelegateChoice {
roleValue: true
delegate: NetworkDivider {
}
}
DelegateChoice {
roleValue: false
delegate: KnownNetworkManager {
}
}
DelegateChoice {
delegate: NetworkItem {
list: rows
width: rows.width
}
}
}
displaced: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
model: ScriptModel {
values: {
const byStrengthName = (a, b) => b.signalStrength - a.signalStrength || a.name.localeCompare(b.name);
const cNetworks = [...Network.connectedNetworks];
const kNetworks = [...Network.knownNetworks];
const uNetworks = [...Network.unknownNetworks];
return [...cNetworks.sort(byStrengthName), ...(kNetworks.length > 0 ? [
{
isDivider: true,
type: "known"
},
...kNetworks.sort(byStrengthName),
{
isDivider: false
}
] : []), ...(uNetworks.length > 0 ? [
{
isDivider: true,
type: "unknown"
},
...uNetworks.sort(byStrengthName)] : [])];
}
}
move: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
}
section.delegate: Rectangle {
required property string section
color: "transparent"
height: childrenRect.height
width: ListView.view.width
CustomText {
text: parent.section
}
}
}
NetworkOverlay {
anchors.centerIn: parent
}
}
component KnownNetworkManager: NetworkSelect {
acceptLabel: qsTr("Remove")
enabled: Object.keys(model).length > 0
header: qsTr("Manage known networks")
icon: "Network_Manage"
label: qsTr("Manage known networks")
model: Network.networks.filter(n => n.known && !n.connected).map(n => ({
id: n.name,
label: n.name
}))
rootParent: root.flickable
width: rows.width
onAccepted: {
if (!selectedItem)
return;
const networkToForget = Network.networks.find(n => n.name === selectedItem);
Network.forgetNetwork(networkToForget);
}
}
component NetworkSelect: DialogRowButton {
id: klist
required property var model
property var selectedItem
function keyFor(item: var): string {
return item.id;
}
function labelFor(item: var): string {
return item.label;
}
acceptAllowed: !!selectedItem
horizontalContentMargin: -Tokens.padding.extraSmall
separateContent: true
content: Component {
VerticalFadeListView {
bottomMargin: Tokens.padding.large
model: klist.model
spacing: 0
topMargin: Tokens.padding.large
delegate: CustomRect {
id: item
required property var modelData
readonly property bool selected: klist.selectedItem === klist.keyFor(modelData)
anchors.left: ListView.view.contentItem.left
anchors.margins: 1 // Gets cut off for some reason without this
anchors.right: ListView.view.contentItem.right
color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
implicitHeight: label.implicitHeight + Tokens.padding.small * 2
radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
Behavior on radius {
Anim {
type: Anim.SlowEffects
}
}
StateLayer {
id: stateLayer
onClicked: klist.selectedItem = klist.keyFor(item.modelData)
}
CustomText {
id: label
anchors.left: parent.left
anchors.margins: Tokens.padding.large
anchors.right: item.selected ? checkIcon.left : parent.right
anchors.rightMargin: item.selected ? Tokens.spacing.medium : anchors.margins
anchors.verticalCenter: parent.verticalCenter
color: item.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
elide: Text.ElideRight
font.pointSize: Tokens.font.size.smaller
text: klist.labelFor(item.modelData)
}
MaterialIcon {
id: checkIcon
anchors.margins: Tokens.padding.large
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onTertiaryContainer
font.pointSize: Tokens.font.size.normal
opacity: item.selected ? 1 : 0
text: "check"
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
}
}
}
onOpenChanged: {
if (open)
selectedItem = null;
}
}
}
@@ -0,0 +1,118 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Internal
import ZShell.Blobs
import ZShell.Services
import ZShell.Components
import qs.Components
import qs.Modules.Settings.Common
PageBase {
id: root
isSubPage: true
title: Tr.tr("Saved networks")
Component.onCompleted: Nmcli.loadSavedConnections(() => {})
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
ItemList {
id: savedList
first: true
last: true
placeholderIcon: "wifi_find"
placeholderText: Tr.tr("No saved networks")
showList: true
delegate: StateLayer {
id: saved
readonly property var ap: Nmcli.findNetwork(modelData)
required property int index
readonly property bool isActive: !!Nmcli.active && Nmcli.active.ssid === modelData
required property var modelData
anchors.fill: undefined
anchors.left: savedList.list.contentItem.left
anchors.right: savedList.list.contentItem.right
bottomLeftRadius: index === savedList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
bottomRightRadius: index === savedList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
implicitHeight: savedLayout.implicitHeight + savedLayout.anchors.margins * 2
radius: Tokens.rounding.extraSmall
topLeftRadius: index === 0 ? Tokens.rounding.extraLarge : radius
topRightRadius: index === 0 ? Tokens.rounding.extraLarge : radius
onClicked: {
root.nState.selectedNetworkSsid = saved.modelData;
root.nState.networkDetailsFromSaved = true;
root.nState.openSubPage(3); // Shared network detail/edit sub-page
}
RowLayout {
id: savedLayout
anchors.fill: parent
anchors.leftMargin: Tokens.padding.extraLarge
anchors.margins: Tokens.padding.large
anchors.rightMargin: Tokens.padding.extraLarge
spacing: Tokens.spacing.medium
MaterialIcon {
color: saved.isActive ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
text: saved.ap ? Icons.getNetworkIcon(saved.ap.strength, !["", "none"].includes(Nmcli.savedSecurityFor(saved.modelData))) : "signal_wifi_off"
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font: Tokens.font.body.small
text: saved.modelData
}
CustomText {
Layout.fillWidth: true
color: saved.isActive ? Colours.palette.m3primary : Colours.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.label.small
text: {
let security;
if (saved.ap)
security = saved.ap.security || Tr.trCtx("Open", "wifi security type");
else
security = Nmcli.securityLabel(Nmcli.savedSecurityFor(saved.modelData)) || Tr.trCtx("Unknown", "unknown wifi security");
if (saved.isActive)
// TRANSLATORS: %1 = security type
return Tr.trCtx("Connected • %1", "network connected with security").arg(security);
return security;
}
}
}
MaterialIcon {
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
text: "chevron_right"
}
}
}
model: ScriptModel {
values: [...Nmcli.savedConnectionSsids].sort((a, b) => a.localeCompare(b))
}
}
}
}
@@ -8,7 +8,9 @@ PageBase {
readonly property var builtinIcons: ({ readonly property var builtinIcons: ({
audio: qsTr("Speakers"), audio: qsTr("Speakers"),
microphone: qsTr("Microphone"), microphone: qsTr("Microphone"),
power: qsTr("Battery") power: qsTr("Battery"),
network: qsTr("Network"),
bluetooth: qsTr("Bluetooth")
}) })
isSubPage: true isSubPage: true
@@ -93,5 +95,15 @@ PageBase {
onToggled: Config.bar.popouts.statusIcons = checked onToggled: Config.bar.popouts.statusIcons = checked
} }
ToggleRow {
checked: Config.bar.popouts.network
last: true
settingAnchor: "bar-status-network-popout"
subtext: qsTr("Show a details popout when hovering the network icon")
text: qsTr("Network popout on hover")
onToggled: Config.bar.popouts.network = checked
}
} }
} }
+1834
View File
File diff suppressed because it is too large Load Diff