forked from Shinonome/dots-hyprland
wifi menu
This commit is contained in:
@@ -1,29 +1,35 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
/**
|
||||
* Material 3 dialog button. See https://m3.material.io/components/dialogs/overview
|
||||
*/
|
||||
RippleButton {
|
||||
id: button
|
||||
id: root
|
||||
|
||||
property string buttonText
|
||||
implicitHeight: 30
|
||||
implicitWidth: buttonTextWidget.implicitWidth + 15 * 2
|
||||
padding: 14
|
||||
implicitHeight: 36
|
||||
implicitWidth: buttonTextWidget.implicitWidth + padding * 2
|
||||
buttonRadius: Appearance?.rounding.full ?? 9999
|
||||
|
||||
property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colDisabled: Appearance?.m3colors.m3outline ?? "#8D8C96"
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
|
||||
colBackgroundHover: Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
|
||||
contentItem: StyledText {
|
||||
id: buttonTextWidget
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 15
|
||||
anchors.rightMargin: 15
|
||||
anchors.leftMargin: root.padding
|
||||
anchors.rightMargin: root.padding
|
||||
text: buttonText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance?.font.pixelSize.small ?? 12
|
||||
color: button.enabled ? button.colEnabled : button.colDisabled
|
||||
color: root.enabled ? root.colEnabled : root.colDisabled
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Material 3 styled TextArea (filled style)
|
||||
* https://m3.material.io/components/text-fields/overview
|
||||
* Note: We don't use NativeRendering because it makes the small placeholder text look weird
|
||||
*/
|
||||
TextArea {
|
||||
id: root
|
||||
Material.theme: Material.System
|
||||
Material.accent: Appearance.m3colors.m3primary
|
||||
Material.primary: Appearance.m3colors.m3primary
|
||||
Material.background: Appearance.m3colors.m3surface
|
||||
Material.foreground: Appearance.m3colors.m3onSurface
|
||||
Material.containerStyle: Material.Filled
|
||||
renderType: Text.QtRendering
|
||||
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
placeholderTextColor: Appearance.m3colors.m3outline
|
||||
|
||||
background: Rectangle {
|
||||
implicitHeight: 56
|
||||
color: Appearance.m3colors.m3surface
|
||||
topLeftRadius: 4
|
||||
topRightRadius: 4
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
height: 1
|
||||
color: root.focus ? Appearance.m3colors.m3primary :
|
||||
root.hovered ? Appearance.m3colors.m3outline : Appearance.m3colors.m3outlineVariant
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
font {
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
wrapMode: TextEdit.Wrap
|
||||
}
|
||||
@@ -4,44 +4,24 @@ import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Material 3 styled TextArea (filled style)
|
||||
* Material 3 styled TextField (filled style)
|
||||
* https://m3.material.io/components/text-fields/overview
|
||||
* Note: We don't use NativeRendering because it makes the small placeholder text look weird
|
||||
*/
|
||||
TextArea {
|
||||
TextField {
|
||||
id: root
|
||||
Material.theme: Material.System
|
||||
Material.accent: Appearance.m3colors.m3primary
|
||||
Material.primary: Appearance.m3colors.m3primary
|
||||
Material.background: Appearance.m3colors.m3surface
|
||||
Material.foreground: Appearance.m3colors.m3onSurface
|
||||
Material.containerStyle: Material.Filled
|
||||
Material.containerStyle: Material.Outlined
|
||||
renderType: Text.QtRendering
|
||||
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
placeholderTextColor: Appearance.m3colors.m3outline
|
||||
|
||||
background: Rectangle {
|
||||
implicitHeight: 56
|
||||
color: Appearance.m3colors.m3surface
|
||||
topLeftRadius: 4
|
||||
topRightRadius: 4
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
height: 1
|
||||
color: root.focus ? Appearance.m3colors.m3primary :
|
||||
root.hovered ? Appearance.m3colors.m3outline : Appearance.m3colors.m3outlineVariant
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
clip: true
|
||||
|
||||
font {
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
@@ -49,4 +29,11 @@ TextArea {
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
wrapMode: TextEdit.Wrap
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ import QtQuick
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: (mouse) => mouse.accepted = false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
@@ -14,6 +14,8 @@ ListView {
|
||||
property int dragIndex: -1
|
||||
property real dragDistance: 0
|
||||
property bool popin: true
|
||||
property bool animateAppearance: true
|
||||
property bool animateMovement: false
|
||||
// Accumulated scroll destination so wheel deltas stack while animating
|
||||
property real scrollTargetY: 0
|
||||
|
||||
@@ -66,17 +68,17 @@ ListView {
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
animations: [
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: popin ? "opacity,scale" : "opacity",
|
||||
from: 0,
|
||||
to: 1,
|
||||
}),
|
||||
]
|
||||
] : []
|
||||
}
|
||||
|
||||
addDisplaced: Transition {
|
||||
animations: [
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
@@ -84,46 +86,46 @@ ListView {
|
||||
properties: popin ? "opacity,scale" : "opacity",
|
||||
to: 1,
|
||||
}),
|
||||
]
|
||||
] : []
|
||||
}
|
||||
|
||||
// displaced: Transition {
|
||||
// animations: [
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// property: "y",
|
||||
// }),
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// properties: "opacity,scale",
|
||||
// to: 1,
|
||||
// }),
|
||||
// ]
|
||||
// }
|
||||
displaced: Transition {
|
||||
animations: root.animateMovement ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
|
||||
// move: Transition {
|
||||
// animations: [
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// property: "y",
|
||||
// }),
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// properties: "opacity,scale",
|
||||
// to: 1,
|
||||
// }),
|
||||
// ]
|
||||
// }
|
||||
// moveDisplaced: Transition {
|
||||
// animations: [
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// property: "y",
|
||||
// }),
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// properties: "opacity,scale",
|
||||
// to: 1,
|
||||
// }),
|
||||
// ]
|
||||
// }
|
||||
move: Transition {
|
||||
animations: root.animateMovement ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
moveDisplaced: Transition {
|
||||
animations: root.animateMovement ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
animations: [
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "x",
|
||||
to: root.width + root.removeOvershoot,
|
||||
@@ -132,12 +134,12 @@ ListView {
|
||||
property: "opacity",
|
||||
to: 0,
|
||||
})
|
||||
]
|
||||
] : []
|
||||
}
|
||||
|
||||
// This is movement when something is removed, not removing animation!
|
||||
removeDisplaced: Transition {
|
||||
animations: [
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
@@ -145,6 +147,6 @@ ListView {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
]
|
||||
] : []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool show: false
|
||||
default property alias data: contentColumn.data
|
||||
property real backgroundHeight: 600
|
||||
property real backgroundAnimationMovementDistance: 60
|
||||
signal dismiss()
|
||||
|
||||
color: root.show ? Appearance.colors.colScrim : ColorUtils.transparentize(Appearance.colors.colScrim)
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
visible: dialogBackground.implicitHeight > 0
|
||||
|
||||
onShowChanged: {
|
||||
dialogBackgroundHeightAnimation.easing.bezierCurve = (show ? Appearance.animationCurves.emphasizedDecel : Appearance.animationCurves.emphasizedAccel)
|
||||
dialogBackground.implicitHeight = show ? backgroundHeight : 0
|
||||
}
|
||||
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
MouseArea { // Clicking outside the dialog should dismiss
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
hoverEnabled: true
|
||||
onPressed: root.dismiss()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dialogBackground
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
radius: Appearance.rounding.large
|
||||
color: Appearance.colors.colLayer3
|
||||
|
||||
property real targetY: root.height / 2 - root.backgroundHeight / 2
|
||||
y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance)
|
||||
implicitWidth: 350
|
||||
implicitHeight: 0
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
id: dialogBackgroundHeightAnimation
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animationCurves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: dialogBackgroundHeightAnimation.duration
|
||||
easing.type: dialogBackgroundHeightAnimation.easing.type
|
||||
easing.bezierCurve: dialogBackgroundHeightAnimation.easing.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea { // So clicking inside the dialog won't dismiss
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: dialogBackground.radius
|
||||
}
|
||||
spacing: 16
|
||||
opacity: root.show ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
spacing: 4
|
||||
|
||||
// These shouldn't be needed but it would be a terrible waste of space to follow the spec
|
||||
Layout.margins: -8
|
||||
Layout.topMargin: 0
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Rectangle {
|
||||
implicitHeight: 1
|
||||
color: Appearance.colors.colOutline
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: -Appearance.rounding.large
|
||||
Layout.rightMargin: -Appearance.rounding.large
|
||||
Layout.topMargin: -8
|
||||
Layout.bottomMargin: -8
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
StyledText {
|
||||
text: "Dialog Title"
|
||||
font {
|
||||
pixelSize: Appearance.font.pixelSize.title
|
||||
family: Appearance.font.family.title
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ ContentPage {
|
||||
}
|
||||
ContentSection {
|
||||
title: Translation.tr("AI")
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("System prompt")
|
||||
text: Config.options.ai.systemPrompt
|
||||
@@ -115,7 +115,7 @@ ContentPage {
|
||||
|
||||
ContentSection {
|
||||
title: Translation.tr("Networking")
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("User agent (for services that require it)")
|
||||
text: Config.options.networking.userAgent
|
||||
@@ -159,7 +159,7 @@ ContentPage {
|
||||
ConfigRow {
|
||||
uniform: true
|
||||
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Action")
|
||||
text: Config.options.search.prefix.action
|
||||
@@ -168,7 +168,7 @@ ContentPage {
|
||||
Config.options.search.prefix.action = text;
|
||||
}
|
||||
}
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Clipboard")
|
||||
text: Config.options.search.prefix.clipboard
|
||||
@@ -177,7 +177,7 @@ ContentPage {
|
||||
Config.options.search.prefix.clipboard = text;
|
||||
}
|
||||
}
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Emojis")
|
||||
text: Config.options.search.prefix.emojis
|
||||
@@ -190,7 +190,7 @@ ContentPage {
|
||||
}
|
||||
ContentSubsection {
|
||||
title: Translation.tr("Web search")
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Base URL")
|
||||
text: Config.options.search.engineBaseUrl
|
||||
|
||||
@@ -3,7 +3,6 @@ import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import "./quickToggles/"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
@@ -16,8 +15,6 @@ import Quickshell.Hyprland
|
||||
Scope {
|
||||
id: root
|
||||
property int sidebarWidth: Appearance.sizes.sidebarWidth
|
||||
property int sidebarPadding: 12
|
||||
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
|
||||
|
||||
PanelWindow {
|
||||
id: sidebarRoot
|
||||
@@ -67,124 +64,7 @@ Scope {
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitHeight: sidebarRightBackground.implicitHeight
|
||||
implicitWidth: sidebarRightBackground.implicitWidth
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: sidebarRightBackground
|
||||
}
|
||||
Rectangle {
|
||||
id: sidebarRightBackground
|
||||
|
||||
anchors.fill: parent
|
||||
implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2
|
||||
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: sidebarPadding
|
||||
spacing: sidebarPadding
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: false
|
||||
spacing: 10
|
||||
Layout.margins: 10
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 0
|
||||
|
||||
CustomIcon {
|
||||
id: distroIcon
|
||||
width: 25
|
||||
height: 25
|
||||
source: SystemInfo.distroIcon
|
||||
colorize: true
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer0
|
||||
text: Translation.tr("Up %1").arg(DateTime.uptime)
|
||||
textFormat: Text.MarkdownText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "restart_alt"
|
||||
onClicked: {
|
||||
Hyprland.dispatch("reload")
|
||||
Quickshell.reload(true)
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Reload Hyprland & Quickshell")
|
||||
}
|
||||
}
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "settings"
|
||||
onClicked: {
|
||||
GlobalStates.sidebarRightOpen = false
|
||||
Quickshell.execDetached(["qs", "-p", root.settingsQmlPath])
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Settings")
|
||||
}
|
||||
}
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "power_settings_new"
|
||||
onClicked: {
|
||||
GlobalStates.sessionOpen = true
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Session")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 5
|
||||
padding: 5
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
NetworkToggle {}
|
||||
BluetoothToggle {}
|
||||
NightLight {}
|
||||
GameMode {}
|
||||
IdleInhibitor {}
|
||||
EasyEffectsToggle {}
|
||||
CloudflareWarp {}
|
||||
}
|
||||
|
||||
// Center widget group
|
||||
CenterWidgetGroup {
|
||||
focus: sidebarRoot.visible
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
BottomWidgetGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: false
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceComponent: SidebarRightContent {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.services.network
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import "./quickToggles/"
|
||||
import "./wifiNetworks/"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property int sidebarWidth: Appearance.sizes.sidebarWidth
|
||||
property int sidebarPadding: 12
|
||||
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
|
||||
property bool showDialog: false
|
||||
property bool dialogIsWifi: true
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onSidebarRightOpenChanged() {
|
||||
if (!GlobalStates.sidebarRightOpen) {
|
||||
root.showDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: sidebarRightBackground.implicitHeight
|
||||
implicitWidth: sidebarRightBackground.implicitWidth
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: sidebarRightBackground
|
||||
}
|
||||
Rectangle {
|
||||
id: sidebarRightBackground
|
||||
|
||||
anchors.fill: parent
|
||||
implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2
|
||||
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: sidebarPadding
|
||||
spacing: sidebarPadding
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: false
|
||||
spacing: 10
|
||||
Layout.margins: 10
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 0
|
||||
|
||||
CustomIcon {
|
||||
id: distroIcon
|
||||
width: 25
|
||||
height: 25
|
||||
source: SystemInfo.distroIcon
|
||||
colorize: true
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer0
|
||||
text: Translation.tr("Up %1").arg(DateTime.uptime)
|
||||
textFormat: Text.MarkdownText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "restart_alt"
|
||||
onClicked: {
|
||||
Hyprland.dispatch("reload")
|
||||
Quickshell.reload(true)
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Reload Hyprland & Quickshell")
|
||||
}
|
||||
}
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "settings"
|
||||
onClicked: {
|
||||
GlobalStates.sidebarRightOpen = false
|
||||
Quickshell.execDetached(["qs", "-p", root.settingsQmlPath])
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Settings")
|
||||
}
|
||||
}
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "power_settings_new"
|
||||
onClicked: {
|
||||
GlobalStates.sessionOpen = true
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Session")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 5
|
||||
padding: 5
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
NetworkToggle {
|
||||
altAction: () => {
|
||||
Network.enableWifi()
|
||||
Network.rescanWifi()
|
||||
root.dialogIsWifi = true
|
||||
root.showDialog = true
|
||||
}
|
||||
}
|
||||
BluetoothToggle {}
|
||||
NightLight {}
|
||||
GameMode {}
|
||||
IdleInhibitor {}
|
||||
EasyEffectsToggle {}
|
||||
CloudflareWarp {}
|
||||
}
|
||||
|
||||
CenterWidgetGroup {
|
||||
focus: sidebarRoot.visible
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
BottomWidgetGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: false
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialog {
|
||||
show: root.showDialog
|
||||
onDismiss: root.showDialog = false
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
|
||||
WindowDialogTitle {
|
||||
text: Translation.tr("Connect to Wi-Fi")
|
||||
}
|
||||
WindowDialogSeparator {
|
||||
// TODO: add indeterminate progress bar when scanning
|
||||
}
|
||||
StyledListView {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -15
|
||||
Layout.bottomMargin: -16
|
||||
Layout.leftMargin: -Appearance.rounding.large
|
||||
Layout.rightMargin: -Appearance.rounding.large
|
||||
|
||||
clip: true
|
||||
spacing: 0
|
||||
animateAppearance: false
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Network.wifiNetworks].sort((a, b) => {
|
||||
if (a.active && !b.active) return -1;
|
||||
if (!a.active && b.active) return 1;
|
||||
return b.strength - a.strength;
|
||||
})
|
||||
}
|
||||
// model: Network.wifiNetworks
|
||||
delegate: WifiNetworkItem {
|
||||
required property WifiAccessPoint modelData
|
||||
wifiNetwork: modelData
|
||||
anchors {
|
||||
left: parent?.left
|
||||
right: parent?.right
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowDialogSeparator {}
|
||||
WindowDialogButtonRow {
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Details")
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`])
|
||||
GlobalStates.sidebarRightOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Done")
|
||||
onClicked: root.showDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import qs.services.network
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
required property WifiAccessPoint wifiNetwork
|
||||
|
||||
horizontalPadding: Appearance.rounding.large
|
||||
verticalPadding: 12
|
||||
implicitWidth: mainLayout.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: mainLayout.implicitHeight + verticalPadding * 2
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
clip: true
|
||||
|
||||
buttonRadius: 0
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
|
||||
colBackgroundHover: wifiNetwork?.askingPassword ? colBackground : Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
|
||||
onClicked: {
|
||||
Network.connectToWifiNetwork(wifiNetwork)
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: root.verticalPadding
|
||||
bottomMargin: root.verticalPadding
|
||||
leftMargin: root.horizontalPadding
|
||||
rightMargin: root.horizontalPadding
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
MaterialSymbol {
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
text: root.wifiNetwork?.strength > 80 ? "signal_wifi_4_bar" :
|
||||
root.wifiNetwork?.strength > 60 ? "network_wifi_3_bar" :
|
||||
root.wifiNetwork?.strength > 40 ? "network_wifi_2_bar" :
|
||||
root.wifiNetwork?.strength > 20 ? "network_wifi_1_bar" :
|
||||
"signal_wifi_0_bar"
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: root.wifiNetwork?.ssid
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
MaterialSymbol {
|
||||
visible: root.wifiNetwork?.isSecure || root.wifiNetwork?.active
|
||||
text: root.wifiNetwork?.active ? "check" : Network.wifiConnectTarget === root.wifiNetwork ? "settings_ethernet" : "lock"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: passwordPrompt
|
||||
visible: root.wifiNetwork?.askingPassword
|
||||
Layout.topMargin: 12
|
||||
|
||||
MaterialTextField {
|
||||
id: passwordField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Password")
|
||||
|
||||
// Password
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
|
||||
onAccepted: {
|
||||
Network.changePassword(root.wifiNetwork, passwordField.text)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Cancel")
|
||||
onClicked: {
|
||||
root.wifiNetwork.askingPassword = false
|
||||
}
|
||||
}
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Connect")
|
||||
onClicked: {
|
||||
Network.changePassword(root.wifiNetwork, passwordField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
// Took many bits from https://github.com/caelestia-dots/shell (GPLv3)
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import "./network"
|
||||
|
||||
/**
|
||||
* Simple polled network state service.
|
||||
* Network service with nmcli.
|
||||
*/
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -15,6 +18,12 @@ Singleton {
|
||||
property bool ethernet: false
|
||||
|
||||
property bool wifiEnabled: false
|
||||
property bool wifiScanning: false
|
||||
property bool wifiConnecting: connectProc.running
|
||||
property WifiAccessPoint wifiConnectTarget
|
||||
readonly property list<WifiAccessPoint> wifiNetworks: []
|
||||
readonly property WifiAccessPoint active: wifiNetworks.find(n => n.active) ?? null
|
||||
|
||||
property string networkName: ""
|
||||
property int networkStrength
|
||||
property string materialSymbol: ethernet ? "lan" :
|
||||
@@ -27,15 +36,99 @@ Singleton {
|
||||
) : "signal_wifi_off"
|
||||
|
||||
// Control
|
||||
function toggleWifi(): void {
|
||||
const cmd = wifiEnabled ? "off" : "on";
|
||||
function enableWifi(enabled = true): void {
|
||||
const cmd = enabled ? "on" : "off";
|
||||
enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]);
|
||||
}
|
||||
|
||||
function toggleWifi(): void {
|
||||
enableWifi(!wifiEnabled);
|
||||
}
|
||||
|
||||
function rescanWifi(): void {
|
||||
wifiScanning = true;
|
||||
rescanProcess.running = true;
|
||||
}
|
||||
|
||||
function connectToWifiNetwork(accessPoint: WifiAccessPoint): void {
|
||||
accessPoint.askingPassword = false;
|
||||
root.wifiConnectTarget = accessPoint;
|
||||
// We use this instead of `nmcli connection up SSID` because this also creates a connection profile
|
||||
connectProc.exec(["nmcli", "dev", "wifi", "connect", accessPoint.ssid])
|
||||
|
||||
}
|
||||
|
||||
function disconnectWifiNetwork(): void {
|
||||
if (active) disconnectProc.exec(["nmcli", "connection", "down", active.ssid]);
|
||||
}
|
||||
|
||||
function changePassword(network: WifiAccessPoint, password: string, username = ""): void {
|
||||
// TODO: enterprise wifi with username
|
||||
network.askingPassword = false;
|
||||
changePasswordProc.exec({
|
||||
"environment": {
|
||||
"PASSWORD": password
|
||||
},
|
||||
"command": ["bash", "-c", `nmcli connection modify ${network.ssid} wifi-sec.psk "$PASSWORD"`]
|
||||
})
|
||||
}
|
||||
|
||||
Process {
|
||||
id: enableWifiProc
|
||||
}
|
||||
|
||||
Process {
|
||||
id: connectProc
|
||||
environment: ({
|
||||
LANG: "C",
|
||||
LC_ALL: "C"
|
||||
})
|
||||
stdout: SplitParser {
|
||||
onRead: line => {
|
||||
// print(line)
|
||||
getNetworks.running = true
|
||||
}
|
||||
}
|
||||
stderr: SplitParser {
|
||||
onRead: line => {
|
||||
// print("err:", line)
|
||||
if (line.includes("Secrets were required")) {
|
||||
root.wifiConnectTarget.askingPassword = true
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.wifiConnectTarget.askingPassword = (exitCode !== 0)
|
||||
root.wifiConnectTarget = null
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: disconnectProc
|
||||
stdout: SplitParser {
|
||||
onRead: getNetworks.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: changePasswordProc
|
||||
onExited: { // Re-attempt connection after changing password
|
||||
connectProc.running = false
|
||||
connectProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: rescanProcess
|
||||
command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"]
|
||||
stdout: SplitParser {
|
||||
onRead: {
|
||||
wifiScanning = false;
|
||||
getNetworks.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status update
|
||||
function update() {
|
||||
updateConnectionType.startCheck();
|
||||
@@ -118,4 +211,78 @@ Singleton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getNetworks
|
||||
running: true
|
||||
command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"]
|
||||
environment: ({
|
||||
LANG: "C",
|
||||
LC_ALL: "C"
|
||||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED";
|
||||
const rep = new RegExp("\\\\:", "g");
|
||||
const rep2 = new RegExp(PLACEHOLDER, "g");
|
||||
|
||||
const allNetworks = text.trim().split("\n").map(n => {
|
||||
const net = n.replace(rep, PLACEHOLDER).split(":");
|
||||
return {
|
||||
active: net[0] === "yes",
|
||||
strength: parseInt(net[1]),
|
||||
frequency: parseInt(net[2]),
|
||||
ssid: net[3],
|
||||
bssid: net[4]?.replace(rep2, ":") ?? "",
|
||||
security: net[5] || ""
|
||||
};
|
||||
}).filter(n => n.ssid && n.ssid.length > 0);
|
||||
|
||||
// Group networks by SSID and prioritize connected ones
|
||||
const networkMap = new Map();
|
||||
for (const network of allNetworks) {
|
||||
const existing = networkMap.get(network.ssid);
|
||||
if (!existing) {
|
||||
networkMap.set(network.ssid, network);
|
||||
} else {
|
||||
// Prioritize active/connected networks
|
||||
if (network.active && !existing.active) {
|
||||
networkMap.set(network.ssid, network);
|
||||
} else if (!network.active && !existing.active) {
|
||||
// If both are inactive, keep the one with better signal
|
||||
if (network.strength > existing.strength) {
|
||||
networkMap.set(network.ssid, network);
|
||||
}
|
||||
}
|
||||
// If existing is active and new is not, keep existing
|
||||
}
|
||||
}
|
||||
|
||||
const wifiNetworks = Array.from(networkMap.values());
|
||||
|
||||
const rNetworks = root.wifiNetworks;
|
||||
|
||||
const destroyed = rNetworks.filter(rn => !wifiNetworks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid));
|
||||
for (const network of destroyed)
|
||||
rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy());
|
||||
|
||||
for (const network of wifiNetworks) {
|
||||
const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid);
|
||||
if (match) {
|
||||
match.lastIpcObject = network;
|
||||
} else {
|
||||
rNetworks.push(apComp.createObject(root, {
|
||||
lastIpcObject: network
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: apComp
|
||||
|
||||
WifiAccessPoint {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
required property var lastIpcObject
|
||||
readonly property string ssid: lastIpcObject.ssid
|
||||
readonly property string bssid: lastIpcObject.bssid
|
||||
readonly property int strength: lastIpcObject.strength
|
||||
readonly property int frequency: lastIpcObject.frequency
|
||||
readonly property bool active: lastIpcObject.active
|
||||
readonly property string security: lastIpcObject.security
|
||||
readonly property bool isSecure: security.length > 0
|
||||
|
||||
property bool askingPassword: false
|
||||
}
|
||||
Reference in New Issue
Block a user