diff --git a/.config/quickshell/ii/modules/common/widgets/DialogButton.qml b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml index 972c29b29..ba00d9b3c 100644 --- a/.config/quickshell/ii/modules/common/widgets/DialogButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml @@ -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) diff --git a/.config/quickshell/ii/modules/common/widgets/MaterialTextArea.qml b/.config/quickshell/ii/modules/common/widgets/MaterialTextArea.qml new file mode 100644 index 000000000..241cc90f7 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/MaterialTextArea.qml @@ -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 +} diff --git a/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml b/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml index 241cc90f7..f160a77d7 100644 --- a/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml +++ b/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml @@ -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 + } } diff --git a/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml b/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml index cf8b065f7..a626bb958 100644 --- a/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml +++ b/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml @@ -3,5 +3,5 @@ import QtQuick MouseArea { anchors.fill: parent onPressed: (mouse) => mouse.accepted = false - cursorShape: Qt.PointingHandCursor + cursorShape: Qt.PointingHandCursor } \ No newline at end of file diff --git a/.config/quickshell/ii/modules/common/widgets/StyledListView.qml b/.config/quickshell/ii/modules/common/widgets/StyledListView.qml index aebf35d77..f005e9f4d 100644 --- a/.config/quickshell/ii/modules/common/widgets/StyledListView.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledListView.qml @@ -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, }), - ] + ] : [] } } diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml new file mode 100644 index 000000000..084f0bbed --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialog.qml @@ -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) + } + + } + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialogButtonRow.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialogButtonRow.qml new file mode 100644 index 000000000..0672e96a5 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialogButtonRow.qml @@ -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 +} diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialogSeparator.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialogSeparator.qml new file mode 100644 index 000000000..52707e51e --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialogSeparator.qml @@ -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 +} diff --git a/.config/quickshell/ii/modules/common/widgets/WindowDialogTitle.qml b/.config/quickshell/ii/modules/common/widgets/WindowDialogTitle.qml new file mode 100644 index 000000000..a450030fa --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/WindowDialogTitle.qml @@ -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 + } +} diff --git a/.config/quickshell/ii/modules/settings/ServicesConfig.qml b/.config/quickshell/ii/modules/settings/ServicesConfig.qml index 712042d65..1b1dc429c 100644 --- a/.config/quickshell/ii/modules/settings/ServicesConfig.qml +++ b/.config/quickshell/ii/modules/settings/ServicesConfig.qml @@ -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 diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml index 3d2c406ca..4147c3abe 100644 --- a/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml @@ -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 {} } diff --git a/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml new file mode 100644 index 000000000..88440d11a --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRightContent.qml @@ -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 + } + } + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml new file mode 100644 index 000000000..907ec9402 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/wifiNetworks/WifiNetworkItem.qml @@ -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) + } + } + } + + } + } +} diff --git a/.config/quickshell/ii/services/Network.qml b/.config/quickshell/ii/services/Network.qml index d318aedbd..bb6e36f9c 100644 --- a/.config/quickshell/ii/services/Network.qml +++ b/.config/quickshell/ii/services/Network.qml @@ -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 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 {} + } } diff --git a/.config/quickshell/ii/services/network/WifiAccessPoint.qml b/.config/quickshell/ii/services/network/WifiAccessPoint.qml new file mode 100644 index 000000000..55ee811f5 --- /dev/null +++ b/.config/quickshell/ii/services/network/WifiAccessPoint.qml @@ -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 +}