From f04ac180f0c8058e1c0c66aa55c5bbc76636c611 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 14:13:05 -0400 Subject: [PATCH 01/61] NInputAction: use proper label/description + autoformatting --- Modules/Bar/Widgets/Microphone.qml | 2 +- Modules/Bar/Widgets/Volume.qml | 6 +++--- Widgets/NInputAction.qml | 20 ++------------------ Widgets/NPill.qml | 3 +-- 4 files changed, 7 insertions(+), 24 deletions(-) diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 410e041..f4e1c1a 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -100,7 +100,7 @@ Item { AudioService.setInputMuted(!AudioService.inputMuted) } onMiddleClicked: { - Quickshell.execDetached(["pwvucontrol"]); + Quickshell.execDetached(["pwvucontrol"]) } } } diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 5f70998..84f8b22 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -63,8 +63,8 @@ Item { collapsedIconColor: Color.mOnSurface autoHide: false // Important to be false so we can hover as long as we want text: Math.floor(AudioService.volume * 100) + "%" - tooltipText: "Volume: " + Math.round( - AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." + tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." onWheel: function (delta) { wheelAccumulator += delta @@ -85,7 +85,7 @@ Item { AudioService.setMuted(!AudioService.muted) } onMiddleClicked: { - Quickshell.execDetached(["pwvucontrol"]); + Quickshell.execDetached(["pwvucontrol"]) } } } diff --git a/Widgets/NInputAction.qml b/Widgets/NInputAction.qml index 1f8ce5e..d439551 100644 --- a/Widgets/NInputAction.qml +++ b/Widgets/NInputAction.qml @@ -22,24 +22,6 @@ ColumnLayout { // Internal properties property real scaling: 1.0 - // Label - NText { - text: root.label - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - Layout.fillWidth: true - } - - // Description - NText { - text: root.description - font.pointSize: Style.fontSizeS * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.Wrap - Layout.fillWidth: true - } - // Input and button row RowLayout { spacing: Style.marginM * scaling @@ -47,6 +29,8 @@ ColumnLayout { NTextInput { id: textInput + label: root.label + description: root.description placeholderText: root.placeholderText text: root.text onEditingFinished: { diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 1fcccfc..2432544 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -231,8 +231,7 @@ Item { root.clicked() } else if (mouse.button === Qt.RightButton) { root.rightClicked() - } - else if (mouse.button === Qt.MiddleButton) { + } else if (mouse.button === Qt.MiddleButton) { root.middleClicked() } } From 125d844e3b94ecd7f11dbb769c529b662f039a91 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 14:19:08 -0400 Subject: [PATCH 02/61] NInputAction simplification --- Modules/SettingsPanel/Tabs/HooksTab.qml | 2 +- Widgets/NInputAction.qml | 57 +++++++++++-------------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/HooksTab.qml b/Modules/SettingsPanel/Tabs/HooksTab.qml index 195844c..9f08a15 100644 --- a/Modules/SettingsPanel/Tabs/HooksTab.qml +++ b/Modules/SettingsPanel/Tabs/HooksTab.qml @@ -81,7 +81,7 @@ ScrollView { // Info section ColumnLayout { - spacing: Style.marginS * scaling + spacing: Style.marginM * scaling Layout.fillWidth: true NLabel { diff --git a/Widgets/NInputAction.qml b/Widgets/NInputAction.qml index d439551..785b5b0 100644 --- a/Widgets/NInputAction.qml +++ b/Widgets/NInputAction.qml @@ -3,7 +3,8 @@ import QtQuick.Layouts import qs.Commons import qs.Widgets -ColumnLayout { +// Input and button row +RowLayout { id: root // Public properties @@ -21,41 +22,35 @@ ColumnLayout { // Internal properties property real scaling: 1.0 + spacing: Style.marginM * scaling - // Input and button row - RowLayout { - spacing: Style.marginM * scaling + NTextInput { + id: textInput + label: root.label + description: root.description + placeholderText: root.placeholderText + text: root.text + onEditingFinished: { + root.text = text + root.editingFinished() + } Layout.fillWidth: true + } - NTextInput { - id: textInput - label: root.label - description: root.description - placeholderText: root.placeholderText - text: root.text - onEditingFinished: { - root.text = text - root.editingFinished() - } - Layout.fillWidth: true - } + NButton { + Layout.fillWidth: false + Layout.alignment: Qt.AlignBottom - Item { - Layout.fillWidth: true - } + text: root.actionButtonText + icon: root.actionButtonIcon + backgroundColor: Color.mSecondary + textColor: Color.mOnSecondary + hoverColor: Color.mTertiary + pressColor: Color.mPrimary + enabled: root.actionButtonEnabled - NButton { - text: root.actionButtonText - icon: root.actionButtonIcon - backgroundColor: Color.mSecondary - textColor: Color.mOnSecondary - hoverColor: Color.mTertiary - pressColor: Color.mPrimary - enabled: root.actionButtonEnabled - Layout.fillWidth: false - onClicked: { - root.actionClicked() - } + onClicked: { + root.actionClicked() } } } From b69d6f57d493567481192aa0c7c159a8d056b90a Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 14:41:04 -0400 Subject: [PATCH 03/61] Bump dev version --- Services/UpdateService.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index 7ec8557..7937b26 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -8,7 +8,7 @@ Singleton { id: root // Public properties - property string baseVersion: "2.5.0" + property string baseVersion: "2.5.1" property bool isDevelopment: true property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` From 783e9fb1408a247a52f0fc0b607be46fc7c6f510 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 14:46:08 -0400 Subject: [PATCH 04/61] AboutTab: improved look of "Download latest release" --- Modules/SettingsPanel/Tabs/AboutTab.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml index a4cb10e..1fffadb 100644 --- a/Modules/SettingsPanel/Tabs/AboutTab.qml +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -60,7 +60,7 @@ ColumnLayout { Rectangle { Layout.alignment: Qt.AlignCenter Layout.topMargin: Style.marginS * scaling - Layout.preferredWidth: updateText.implicitWidth + 46 * scaling + Layout.preferredWidth: Math.round(updateRow.implicitWidth + (Style.marginL * scaling * 2)) Layout.preferredHeight: Math.round(Style.barHeight * scaling) radius: Style.radiusL * scaling color: updateArea.containsMouse ? Color.mPrimary : Color.transparent @@ -85,11 +85,12 @@ ColumnLayout { } RowLayout { + id: updateRow anchors.centerIn: parent spacing: Style.marginS * scaling NIcon { - text: "system_update" + text: "download" font.pointSize: Style.fontSizeXXL * scaling color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary } From 56fedcf49522ce0d5a74a00e5aa130d8ce9f9d0f Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 15:07:31 -0400 Subject: [PATCH 05/61] HooksTab: removed ScrollView which already exists in parent (SettingsPanel.qml) --- Modules/SettingsPanel/Tabs/HooksTab.qml | 143 +++++++++++------------- 1 file changed, 67 insertions(+), 76 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/HooksTab.qml b/Modules/SettingsPanel/Tabs/HooksTab.qml index 9f08a15..461a4b8 100644 --- a/Modules/SettingsPanel/Tabs/HooksTab.qml +++ b/Modules/SettingsPanel/Tabs/HooksTab.qml @@ -5,94 +5,85 @@ import qs.Commons import qs.Services import qs.Widgets -ScrollView { - id: root +ColumnLayout { + id: contentColumn + spacing: Style.marginL * scaling + width: root.width - property real scaling: 1.0 - - contentWidth: contentColumn.width - contentHeight: contentColumn.height + // Enable/Disable Toggle + NToggle { + label: "Enable Hooks" + description: "Enable or disable all hook commands." + checked: Settings.data.hooks.enabled + onToggled: checked => Settings.data.hooks.enabled = checked + } ColumnLayout { - id: contentColumn + visible: Settings.data.hooks.enabled spacing: Style.marginL * scaling - width: root.width + Layout.fillWidth: true - // Enable/Disable Toggle - NToggle { - label: "Enable Hooks" - description: "Enable or disable all hook commands." - checked: Settings.data.hooks.enabled - onToggled: checked => Settings.data.hooks.enabled = checked + NDivider { + Layout.fillWidth: true } + // Wallpaper Hook Section + NInputAction { + id: wallpaperHookInput + label: "Wallpaper Change Hook" + description: "Command to be executed when wallpaper changes." + placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\"" + text: Settings.data.hooks.wallpaperChange + onEditingFinished: { + Settings.data.hooks.wallpaperChange = wallpaperHookInput.text + } + onActionClicked: { + if (wallpaperHookInput.text) { + HooksService.executeWallpaperHook("test", "test-screen") + } + } + Layout.fillWidth: true + } + + NDivider { + Layout.fillWidth: true + } + + // Dark Mode Hook Section + NInputAction { + id: darkModeHookInput + label: "Theme Toggle Hook" + description: "Command to be executed when theme toggles between dark and light mode." + placeholderText: "e.g., notify-send \"Theme\" \"Toggled\"" + text: Settings.data.hooks.darkModeChange + onEditingFinished: { + Settings.data.hooks.darkModeChange = darkModeHookInput.text + } + onActionClicked: { + if (darkModeHookInput.text) { + HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode) + } + } + Layout.fillWidth: true + } + + NDivider { + Layout.fillWidth: true + } + + // Info section ColumnLayout { - visible: Settings.data.hooks.enabled - spacing: Style.marginL * scaling + spacing: Style.marginM * scaling Layout.fillWidth: true - NDivider { - Layout.fillWidth: true + NLabel { + label: "Hook Command Information" + description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values" } - // Wallpaper Hook Section - NInputAction { - id: wallpaperHookInput - label: "Wallpaper Change Hook" - description: "Command to be executed when wallpaper changes." - placeholderText: "e.g., notify-send \"Wallpaper\" \"Changed\"" - text: Settings.data.hooks.wallpaperChange - onEditingFinished: { - Settings.data.hooks.wallpaperChange = wallpaperHookInput.text - } - onActionClicked: { - if (wallpaperHookInput.text) { - HooksService.executeWallpaperHook("test", "test-screen") - } - } - Layout.fillWidth: true - } - - NDivider { - Layout.fillWidth: true - } - - // Dark Mode Hook Section - NInputAction { - id: darkModeHookInput - label: "Theme Toggle Hook" - description: "Command to be executed when theme toggles between dark and light mode." - placeholderText: "e.g., notify-send \"Theme\" \"Toggled\"" - text: Settings.data.hooks.darkModeChange - onEditingFinished: { - Settings.data.hooks.darkModeChange = darkModeHookInput.text - } - onActionClicked: { - if (darkModeHookInput.text) { - HooksService.executeDarkModeHook(Settings.data.colorSchemes.darkMode) - } - } - Layout.fillWidth: true - } - - NDivider { - Layout.fillWidth: true - } - - // Info section - ColumnLayout { - spacing: Style.marginM * scaling - Layout.fillWidth: true - - NLabel { - label: "Hook Command Information" - description: "• Commands are executed via shell (sh -c)\n• Commands run in background (detached)\n• Test buttons execute with current values" - } - - NLabel { - label: "Available Parameters" - description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)" - } + NLabel { + label: "Available Parameters" + description: "• Wallpaper Hook: $1 = wallpaper path, $2 = screen name\n• Theme Toggle Hook: $1 = true/false (dark mode state)" } } } From 3140039ccbb997b85fc697fbbe4c794147eae67e Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 15:08:45 -0400 Subject: [PATCH 06/61] NTextInput: simplified code in an attempt to fix text selection issues with mouse. Not fixed yet, but I know where the conflict is! --- Modules/Launcher/Launcher.qml | 73 ++++++++++----------- Modules/SettingsPanel/SettingsPanel.qml | 32 +++++---- Modules/SettingsPanel/Tabs/GeneralTab.qml | 2 + Modules/SettingsPanel/Tabs/WallpaperTab.qml | 1 - Widgets/NTextInput.qml | 65 +++++++++--------- 5 files changed, 81 insertions(+), 92 deletions(-) diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 4726770..0ad25da 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -243,52 +243,45 @@ NPanel { anchors.margins: Style.marginL * scaling spacing: Style.marginM * scaling - Item { - id: searchInputWrap + NTextInput { + id: searchInput Layout.fillWidth: true - Layout.preferredHeight: Math.round(Style.barHeight * scaling) - NTextInput { - id: searchInput - anchors.fill: parent - inputMaxWidth: Number.MAX_SAFE_INTEGER + fontSize: Style.fontSizeL * scaling + fontWeight: Style.fontWeightSemiBold - fontSize: Style.fontSizeL * scaling - fontWeight: Style.fontWeightSemiBold + text: searchText + placeholderText: "Search entries... or use > for commands" - text: searchText - placeholderText: "Search entries... or use > for commands" + onTextChanged: searchText = text - onTextChanged: searchText = text + Component.onCompleted: { + if (searchInput.inputItem && searchInput.inputItem.visible) { + searchInput.inputItem.forceActiveFocus() - Component.onCompleted: { - if (searchInput.inputItem && searchInput.inputItem.visible) { - searchInput.inputItem.forceActiveFocus() - - // Override the TextField's default Home/End behavior - searchInput.inputItem.Keys.priority = Keys.BeforeItem - searchInput.inputItem.Keys.onPressed.connect(function (event) { - // Intercept Home and End BEFORE the TextField handles them - if (event.key === Qt.Key_Home) { - ui.selectFirst() - event.accepted = true - return - } else if (event.key === Qt.Key_End) { - ui.selectLast() - event.accepted = true - return - } - }) - searchInput.inputItem.Keys.onDownPressed.connect(function (event) { - ui.selectNext() - }) - searchInput.inputItem.Keys.onUpPressed.connect(function (event) { - ui.selectPrevious() - }) - searchInput.inputItem.Keys.onReturnPressed.connect(function (event) { - ui.activate() - }) - } + // Override the TextField's default Home/End behavior + searchInput.inputItem.Keys.priority = Keys.BeforeItem + searchInput.inputItem.Keys.onPressed.connect(function (event) { + // Intercept Home and End BEFORE the TextField handles them + if (event.key === Qt.Key_Home) { + ui.selectFirst() + event.accepted = true + return + } else if (event.key === Qt.Key_End) { + ui.selectLast() + event.accepted = true + return + } + }) + searchInput.inputItem.Keys.onDownPressed.connect(function (event) { + ui.selectNext() + }) + searchInput.inputItem.Keys.onUpPressed.connect(function (event) { + ui.selectPrevious() + }) + searchInput.inputItem.Keys.onReturnPressed.connect(function (event) { + ui.activate() + }) } } } diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 2e1e4aa..4454037 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -477,25 +477,23 @@ NPanel { } } - sourceComponent: ColumnLayout { - ScrollView { - id: scrollView - Layout.fillWidth: true - Layout.fillHeight: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - padding: Style.marginL * scaling - clip: true + sourceComponent: ScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + padding: Style.marginL * scaling + clip: true - Component.onCompleted: { - root.activeScrollView = scrollView - } + Component.onCompleted: { + root.activeScrollView = scrollView + } - Loader { - active: true - sourceComponent: root.tabsModel[index].source - width: scrollView.availableWidth - } + Loader { + active: true + sourceComponent: root.tabsModel[index].source + width: scrollView.availableWidth } } } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index c0d5f54..0ec0064 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -22,9 +22,11 @@ ColumnLayout { fallbackIcon: "person" borderColor: Color.mPrimary borderWidth: Math.max(1, Style.borderM * scaling) + Layout.alignment: Qt.AlignTop } NTextInput { + Layout.fillWidth: true label: `${Quickshell.env("USER") || "user"}'s profile picture` description: "Your profile picture that appears throughout the interface." text: Settings.data.general.avatarImage diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index e2832dd..3115f03 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -278,7 +278,6 @@ ColumnLayout { NTextInput { label: "Custom Interval" description: "Enter time as HH:MM (e.g., 01:30)." - inputMaxWidth: 100 * scaling text: { const s = Settings.data.wallpaper.randomIntervalSec const h = Math.floor(s / 3600) diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 1cb7141..3db9d9b 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -11,7 +11,6 @@ ColumnLayout { property string description: "" property bool readOnly: false property bool enabled: true - property int inputMaxWidth: Math.round(420 * scaling) property color labelColor: Color.mOnSurface property color descriptionColor: Color.mOnSurfaceVariant property string fontFamily: Settings.data.ui.fontDefault @@ -26,7 +25,6 @@ ColumnLayout { signal editingFinished spacing: Style.marginS * scaling - implicitHeight: frame.height NLabel { label: root.label @@ -34,6 +32,7 @@ ColumnLayout { labelColor: root.labelColor descriptionColor: root.descriptionColor visible: root.label !== "" || root.description !== "" + Layout.fillWidth: true } // Container @@ -42,50 +41,48 @@ ColumnLayout { Layout.fillWidth: true Layout.minimumWidth: 80 * scaling - Layout.maximumWidth: root.inputMaxWidth - - implicitWidth: parent.width implicitHeight: Style.baseWidgetSize * 1.1 * scaling + radius: Style.radiusM * scaling color: Color.mSurface - border.color: Color.mOutline + border.color: input.activeFocus ? Color.mSecondary : Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - // Focus ring - Rectangle { - anchors.fill: parent - radius: frame.radius - color: Color.transparent - border.color: input.activeFocus ? Color.mSecondary : Color.transparent - border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0 - - Behavior on border.color { - ColorAnimation { - duration: Style.animationFast - } + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast } } - RowLayout { + TextField { + id: input + anchors.fill: parent anchors.leftMargin: Style.marginM * scaling anchors.rightMargin: Style.marginM * scaling - spacing: Style.marginS * scaling - TextField { - id: input - Layout.fillWidth: true - echoMode: TextInput.Normal - readOnly: root.readOnly - enabled: root.enabled - color: Color.mOnSurface - placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6) - background: null - font.family: fontFamily - font.pointSize: fontSize - font.weight: fontWeight - onEditingFinished: root.editingFinished() - } + verticalAlignment: TextInput.AlignVCenter + + echoMode: TextInput.Normal + readOnly: root.readOnly + enabled: root.enabled + color: Color.mOnSurface + placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6) + + selectByMouse: true + + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + + background: null + + font.family: root.fontFamily + font.pointSize: root.fontSize + font.weight: root.fontWeight + + onEditingFinished: root.editingFinished() } } } From f9a48beccec55eeaae7f37e88b08fa548060629b Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 15:47:07 -0400 Subject: [PATCH 07/61] SettingsPanel: finaly fixed the conflict between scrollview and textinput! --- Modules/SettingsPanel/SettingsPanel.qml | 37 +++++++++++++++---------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 4454037..0cd9b9c 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -477,23 +477,30 @@ NPanel { } } - sourceComponent: ScrollView { - id: scrollView - Layout.fillWidth: true - Layout.fillHeight: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - padding: Style.marginL * scaling - clip: true + sourceComponent: Flickable { + // Using a Flickable here with a pressDelay to fix conflict between + // ScrollView and NTextInput. This fix the weird text selection issue. + id: flickable + anchors.fill: parent + pressDelay: 200 - Component.onCompleted: { - root.activeScrollView = scrollView - } + ScrollView { + id: scrollView + anchors.fill: parent + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + padding: Style.marginL * scaling + clip: true - Loader { - active: true - sourceComponent: root.tabsModel[index].source - width: scrollView.availableWidth + Component.onCompleted: { + root.activeScrollView = scrollView + } + + Loader { + active: true + sourceComponent: root.tabsModel[index].source + width: scrollView.availableWidth + } } } } From a3aba8d0db660284ca6ea6509bedf1893b3ed265 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 5 Sep 2025 22:29:20 +0200 Subject: [PATCH 08/61] Toast: update visibility for newest toast --- Services/ToastService.qml | 21 ++++++++++++--------- Widgets/NToast.qml | 9 +++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Services/ToastService.qml b/Services/ToastService.qml index 6c0be38..edff04b 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -165,13 +165,21 @@ Singleton { "timestamp": Date.now() } + // If there's already a toast showing, instantly start hide animation and show new one + if (isShowingToast) { + // Instantly start hide animation of current toast + for (var i = 0; i < allToasts.length; i++) { + allToasts[i].hide() + } + // Clear the queue since we're showing the new toast immediately + messageQueue = [] + } + // Add to queue messageQueue.push(toastData) - // Process queue if not currently showing a toast - if (!isShowingToast) { - processQueue() - } + // Always process immediately for instant display + processQueue() } // Process the message queue @@ -181,11 +189,6 @@ Singleton { return } - if (isShowingToast) { - // Wait for current toast to finish - return - } - var toastData = messageQueue.shift() isShowingToast = true diff --git a/Widgets/NToast.qml b/Widgets/NToast.qml index 44a0bb5..079bd3c 100644 --- a/Widgets/NToast.qml +++ b/Widgets/NToast.qml @@ -37,7 +37,16 @@ Item { // NToast updates its scaling when showing. scaling = ScalingService.getScreenScale(screen) + // Stop any running animations and reset state + showAnimation.stop() + hideAnimation.stop() + autoHideTimer.stop() + + // Ensure we start from the hidden position + y = hiddenY visible = true + + // Start the show animation showAnimation.start() if (duration > 0 && !persistent) { autoHideTimer.start() From 60950fb461047e4a43e659b57e9835dc4c352e69 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 5 Sep 2025 22:36:04 +0200 Subject: [PATCH 09/61] dock: add opacity slider as requested in #222 --- Commons/Settings.qml | 1 + Modules/Dock/Dock.qml | 4 ++- Modules/SettingsPanel/Tabs/GeneralTab.qml | 39 +++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 0c1dd55..a416e7f 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -284,6 +284,7 @@ Singleton { property JsonObject dock: JsonObject { property bool autoHide: false property bool exclusive: false + property real backgroundOpacity: 1.0 property list monitors: [] } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 07056ff..2fa9e98 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -32,6 +32,8 @@ Variants { screen: modelData + WlrLayershell.namespace: "noctalia-dock" + property bool autoHide: Settings.data.dock.autoHide property bool hidden: autoHide property int hideDelay: 500 @@ -130,7 +132,7 @@ Variants { id: dockContainer width: dock.width + 48 * scaling height: iconSize * 1.4 * scaling - color: Color.mSurface + color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: dockSpacing diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 0ec0064..7407112 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -77,6 +77,45 @@ ColumnLayout { onToggled: checked => Settings.data.dock.autoHide = checked } + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NText { + text: "Dock Background Opacity" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Adjust the background opacity of the dock." + font.pointSize: Style.fontSizeXS * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + NSlider { + Layout.fillWidth: true + from: 0 + to: 1 + stepSize: 0.01 + value: Settings.data.dock.backgroundOpacity + onMoved: Settings.data.dock.backgroundOpacity = value + cutoutColor: Color.mSurface + } + + NText { + text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%" + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS * scaling + color: Color.mOnSurface + } + } + } + ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true From c8a056f332296ee21abf55b991118d2b2ec0ae83 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 5 Sep 2025 22:42:40 +0200 Subject: [PATCH 10/61] Notification: add DND option to widget and notification panel as requested in #212 --- Commons/Settings.qml | 1 + Modules/Bar/Widgets/NotificationHistory.qml | 15 ++++++++++++--- Modules/Notification/NotificationHistoryPanel.qml | 13 +++++++++++++ Services/NotificationService.qml | 4 ++-- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index a416e7f..9565e2c 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -296,6 +296,7 @@ Singleton { // notifications property JsonObject notifications: JsonObject { + property bool doNotDisturb: false property list monitors: [] } diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 222a0eb..cb11314 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -14,11 +14,20 @@ NIconButton { property real scaling: 1.0 sizeRatio: 0.8 - icon: "notifications" - tooltipText: "Notification history" + icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications" + tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history (Do Not Disturb ON)\nRight-click to toggle Do Not Disturb" : "Notification history\nRight-click to toggle Do Not Disturb" colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface + colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent + onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this) + + onRightClicked: { + Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + ToastService.showNotice( + Settings.data.notifications.doNotDisturb ? "Do Not Disturb enabled" : "Do Not Disturb disabled", + Settings.data.notifications.doNotDisturb ? "Notifications will be hidden but saved to history" : "Notifications will be shown normally", + "notice", false, 2000) + } } diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 3b10aec..7bc2378 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -43,6 +43,19 @@ NPanel { Layout.fillWidth: true } + NIconButton { + icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications_active" + tooltipText: Settings.data.notifications.doNotDisturb ? "Do Not Disturb (ON)" : "Do Not Disturb (OFF)" + sizeRatio: 0.8 + onClicked: { + Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + ToastService.showNotice( + Settings.data.notifications.doNotDisturb ? "Do Not Disturb enabled" : "Do Not Disturb disabled", + Settings.data.notifications.doNotDisturb ? "Notifications will be hidden but saved to history" : "Notifications will be shown normally", + "notice", false, 2000) + } + } + NIconButton { icon: "delete" tooltipText: "Clear history" diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index a79b812..62d88fb 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -29,8 +29,8 @@ Singleton { // Signal when notification is received onNotification: function (notification) { - // Check if notifications are suppressed - if (Settings.data.notifications && Settings.data.notifications.suppressed) { + // Check if do-not-disturb is enabled + if (Settings.data.notifications && Settings.data.notifications.doNotDisturb) { // Still add to history but don't show notification root.addToHistory(notification) return From c16e6e742372a7abd5dc19df6b0edf748ded9a70 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 5 Sep 2025 23:00:03 +0200 Subject: [PATCH 11/61] Notification: adjust layout --- Modules/IPC/IPCManager.qml | 7 ++++++- Modules/Notification/Notification.qml | 12 ++++++------ Widgets/NToast.qml | 5 ++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 7436829..b1ba1d7 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -38,7 +38,12 @@ Item { function toggleHistory() { notificationHistoryPanel.toggle(getActiveScreen()) } - function toggleDoNotDisturb() {// TODO + function toggleDND() { + Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + ToastService.showNotice( + Settings.data.notifications.doNotDisturb ? "Do Not Disturb enabled" : "Do Not Disturb disabled", + Settings.data.notifications.doNotDisturb ? "Notifications will be hidden but saved to history" : "Notifications will be shown normally", + "notice", false, 2000) } } diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 009d717..54f21ce 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -159,8 +159,8 @@ Variants { RowLayout { id: contentRow anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginL * scaling + anchors.margins: Style.marginM * scaling + spacing: Style.marginM * scaling // Right: header on top, then avatar + texts ColumnLayout { @@ -248,12 +248,12 @@ Variants { NIconButton { icon: "close" tooltipText: "Close" - // Compact target (~24dp) and glyph (~16dp) - sizeRatio: 0.75 - fontPointSize: 16 + sizeRatio: 0.6 + fontPointSize: 12 anchors.top: parent.top + anchors.topMargin: Style.marginM * scaling anchors.right: parent.right - anchors.margins: Style.marginS * scaling + anchors.rightMargin: Style.marginM * scaling onClicked: { animateOut() diff --git a/Widgets/NToast.qml b/Widgets/NToast.qml index 079bd3c..cdbf399 100644 --- a/Widgets/NToast.qml +++ b/Widgets/NToast.qml @@ -180,7 +180,10 @@ Item { icon: "close" visible: root.persistent || root.duration === 0 - color: Color.mOnSurface + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: Color.transparent + colorBorderHover: Color.mOutline fontPointSize: Style.fontSizeM * scaling sizeRatio: 0.8 From 8fedd7612d33164e2519d6d03ee7440fad707fb4 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 17:06:09 -0400 Subject: [PATCH 12/61] NToast: Column => ColumnLayout --- Widgets/NToast.qml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Widgets/NToast.qml b/Widgets/NToast.qml index 079bd3c..ddcf69a 100644 --- a/Widgets/NToast.qml +++ b/Widgets/NToast.qml @@ -90,7 +90,6 @@ Item { // Main toast container Rectangle { - id: container anchors.fill: parent radius: Style.radiusL * scaling @@ -146,37 +145,33 @@ Item { } // Label and description - Column { - id: textColumn + ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter NText { - id: labelText + Layout.fillWidth: true text: root.label color: Color.mOnSurface font.pointSize: Style.fontSizeM * scaling font.weight: Style.fontWeightBold wrapMode: Text.WordWrap - width: parent.width visible: text.length > 0 } NText { - id: descriptionText + Layout.fillWidth: true text: root.description color: Color.mOnSurface font.pointSize: Style.fontSizeM * scaling wrapMode: Text.WordWrap - width: parent.width visible: text.length > 0 } } // Close button (only if persistent or manual dismiss needed) NIconButton { - id: closeButton icon: "close" visible: root.persistent || root.duration === 0 From b30d3df15cf864d73ca9dc689611ff5adee10d64 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 5 Sep 2025 23:07:05 +0200 Subject: [PATCH 13/61] Notification: only display app icon/avatar if the notification requested it --- Modules/Notification/Notification.qml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 54f21ce..d15e305 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -198,15 +198,14 @@ Variants { Layout.preferredWidth: 40 * scaling Layout.preferredHeight: 40 * scaling Layout.alignment: Qt.AlignTop - // Start avatar aligned with body (below the summary) anchors.topMargin: textContent.childrenRect.y - // Prefer notification-provided image (e.g., user avatar) then fall back to app icon - imagePath: (model.image && model.image !== "") ? model.image : Icons.iconFromName( - model.appIcon, "application-x-executable") - fallbackIcon: "apps" + imagePath: model.image && model.image !== "" ? model.image : "" + fallbackIcon: "" borderColor: Color.transparent borderWidth: 0 - visible: (imagePath && imagePath !== "") + visible: (model.image && model.image !== "") + Layout.fillWidth: false + Layout.fillHeight: false } Column { From c85a309aeb9c78db3e20e1bc2d8747b5bdeaffa2 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 17:23:02 -0400 Subject: [PATCH 14/61] MediaMini: converted to Layout --- Modules/Bar/Widgets/MediaMini.qml | 258 +++++++++++++----------------- 1 file changed, 111 insertions(+), 147 deletions(-) diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 7d2ffb7..6ce7a59 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -7,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -Row { +RowLayout { id: root property ShellScreen screen @@ -15,7 +15,7 @@ Row { readonly property real minWidth: 160 readonly property real maxWidth: 400 - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling visible: MediaService.currentPlayer !== null && MediaService.canPlay width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 @@ -24,7 +24,6 @@ Row { return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") } - // A hidden text element to safely measure the full title width NText { id: fullTitleMetrics visible: false @@ -34,169 +33,134 @@ Row { Rectangle { id: mediaMini - - // Let the Rectangle size itself based on its content (the Row) - width: row.width + Style.marginM * 2 * scaling - + width: contentLayout.implicitWidth + Style.marginS * 2 * scaling height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant + Layout.alignment: Qt.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - - // Used to anchor the tooltip, so the tooltip does not move when the content expands - Item { - id: anchor - height: parent.height - width: 200 * scaling + // --- Visualizer Loaders --- + Loader { + anchors.centerIn: parent + active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" + && MediaService.isPlaying && MediaService.trackLength > 0 + z: 0 + sourceComponent: LinearSpectrum { + width: mediaMini.width - Style.marginS * scaling + height: 20 * scaling + values: CavaService.values + fillColor: Color.mOnSurfaceVariant + opacity: 0.4 + } } - Item { - id: mainContainer - anchors.fill: parent + Loader { + anchors.centerIn: parent + active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" + && MediaService.isPlaying && MediaService.trackLength > 0 + z: 0 + sourceComponent: MirroredSpectrum { + width: mediaMini.width - Style.marginS * scaling + height: mediaMini.height - Style.marginS * scaling + values: CavaService.values + fillColor: Color.mOnSurfaceVariant + opacity: 0.4 + } + } + + Loader { + anchors.centerIn: parent + active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" + && MediaService.isPlaying && MediaService.trackLength > 0 + z: 0 + sourceComponent: WaveSpectrum { + width: mediaMini.width - Style.marginS * scaling + height: mediaMini.height - Style.marginS * scaling + values: CavaService.values + fillColor: Color.mOnSurfaceVariant + opacity: 0.4 + } + } + + // --- Main Content Layout --- + RowLayout { + id: contentLayout + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Style.marginS * scaling - anchors.rightMargin: Style.marginS * scaling + spacing: Style.marginS * scaling + z: 1 - Loader { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" - && MediaService.isPlaying && MediaService.trackLength > 0 - z: 0 + NIcon { + id: windowIcon + text: MediaService.isPlaying ? "pause" : "play_arrow" + font.pointSize: Style.fontSizeL * scaling + Layout.alignment: Qt.AlignVCenter + visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible + } - sourceComponent: LinearSpectrum { - width: mainContainer.width - Style.marginS * scaling - height: 20 * scaling - values: CavaService.values - fillColor: Color.mOnSurfaceVariant - opacity: 0.4 - } + NImageCircled { + id: trackArt + imagePath: MediaService.trackArtUrl + fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow" + borderWidth: 0 + border.color: Color.transparent + Layout.preferredWidth: Math.round(18 * scaling) + Layout.preferredHeight: Math.round(18 * scaling) + Layout.alignment: Qt.AlignVCenter + visible: Settings.data.audio.showMiniplayerAlbumArt + } - Loader { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" - && MediaService.isPlaying && MediaService.trackLength > 0 - z: 0 - - sourceComponent: MirroredSpectrum { - width: mainContainer.width - Style.marginS * scaling - height: mainContainer.height - Style.marginS * scaling - values: CavaService.values - fillColor: Color.mOnSurfaceVariant - opacity: 0.4 + NText { + id: titleText + Layout.preferredWidth: { + if (mouseArea.containsMouse) { + return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) + } else { + return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) } } + text: getTitle() + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + elide: Text.ElideRight + color: Color.mTertiary + Layout.alignment: Qt.AlignVCenter - Loader { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" - && MediaService.isPlaying && MediaService.trackLength > 0 - z: 0 - - sourceComponent: WaveSpectrum { - width: mainContainer.width - Style.marginS * scaling - height: mainContainer.height - Style.marginS * scaling - values: CavaService.values - fillColor: Color.mOnSurfaceVariant - opacity: 0.4 + Behavior on Layout.preferredWidth { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.InOutCubic } } } + } - Row { - id: row - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS * scaling - z: 1 // Above the visualizer - - NIcon { - id: windowIcon - text: MediaService.isPlaying ? "pause" : "play_arrow" - font.pointSize: Style.fontSizeL * scaling - verticalAlignment: Text.AlignVCenter - anchors.verticalCenter: parent.verticalCenter - visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible - } - - Column { - anchors.verticalCenter: parent.verticalCenter - visible: Settings.data.audio.showMiniplayerAlbumArt - - Item { - width: Math.round(18 * scaling) - height: Math.round(18 * scaling) - - NImageCircled { - id: trackArt - anchors.fill: parent - imagePath: MediaService.trackArtUrl - fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow" - borderWidth: 0 - border.color: Color.transparent - } - } - } - - NText { - id: titleText - - // For short titles, show full. For long titles, truncate and expand on hover - width: { - if (mouseArea.containsMouse) { - return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) - } else { - return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) - } - } - text: getTitle() - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - color: Color.mTertiary - - Behavior on width { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.InOutCubic - } - } - } - } - - // Mouse area for hover detection - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - MediaService.playPause() - } else if (mouse.button == Qt.RightButton) { - MediaService.next() - // Need to hide the tooltip instantly - tooltip.visible = false - } else if (mouse.button == Qt.MiddleButton) { - MediaService.previous() - // Need to hide the tooltip instantly - tooltip.visible = false - } + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + MediaService.playPause() + } else if (mouse.button == Qt.RightButton) { + MediaService.next() + tooltip.visible = false + } else if (mouse.button == Qt.MiddleButton) { + MediaService.previous() + tooltip.visible = false } - - onEntered: { - if (tooltip.text !== "") { - tooltip.show() - } - } - onExited: { - tooltip.hide() + } + onEntered: { + if (tooltip.text !== "") { + tooltip.show() } } + onExited: { + tooltip.hide() + } } } @@ -212,7 +176,7 @@ Row { } return str } - target: anchor + target: mediaMini positionAbove: Settings.data.bar.position === "bottom" } } From a5fc9d9ca93d9d86d7c94337062129ad30c7345a Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 5 Sep 2025 23:31:55 +0200 Subject: [PATCH 15/61] Notification: add actions README: add fix for niri action buttons for notifications --- Modules/Notification/Notification.qml | 195 +++++++++++++++++--------- README.md | 6 + 2 files changed, 133 insertions(+), 68 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index d15e305..7abb6cc 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -93,7 +93,10 @@ Variants { model: notificationModel delegate: Rectangle { width: 360 * scaling - height: Math.max(80 * scaling, contentRow.implicitHeight + (Style.marginL * 2 * scaling)) + height: Math.max( + 80 * scaling, + contentRow.implicitHeight + (actionsRow.visible ? actionsRow.implicitHeight + Style.marginM + * scaling : 0) + (Style.marginL * 2 * scaling)) clip: true radius: Style.radiusL * scaling border.color: Color.mOutline @@ -156,92 +159,148 @@ Variants { } } - RowLayout { - id: contentRow + ColumnLayout { anchors.fill: parent anchors.margins: Style.marginM * scaling spacing: Style.marginM * scaling - // Right: header on top, then avatar + texts - ColumnLayout { - id: textColumn - spacing: Style.marginS * scaling + RowLayout { + id: contentRow + spacing: Style.marginM * scaling Layout.fillWidth: true - RowLayout { + // Right: header on top, then avatar + texts + ColumnLayout { + id: textColumn spacing: Style.marginS * scaling - id: appHeaderRow - NText { - text: `${(model.appName || model.desktopEntry) - || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` - color: Color.mSecondary - font.pointSize: Style.fontSizeXS * scaling - } - Rectangle { - width: 6 * scaling - height: 6 * scaling - radius: Style.radiusXS * scaling - color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary - Layout.alignment: Qt.AlignVCenter - } - Item { - Layout.fillWidth: true - } - } + Layout.fillWidth: true - RowLayout { - id: bodyRow - spacing: Style.marginM * scaling - - NImageCircled { - id: appAvatar - Layout.preferredWidth: 40 * scaling - Layout.preferredHeight: 40 * scaling - Layout.alignment: Qt.AlignTop - anchors.topMargin: textContent.childrenRect.y - imagePath: model.image && model.image !== "" ? model.image : "" - fallbackIcon: "" - borderColor: Color.transparent - borderWidth: 0 - visible: (model.image && model.image !== "") - Layout.fillWidth: false - Layout.fillHeight: false - } - - Column { - id: textContent + RowLayout { spacing: Style.marginS * scaling - Layout.fillWidth: true - // Ensure a concrete width so text wraps - width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0)) - + id: appHeaderRow NText { - text: model.summary || "No summary" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightMedium - color: Color.mOnSurface - wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: `${(model.appName || model.desktopEntry) + || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` + color: Color.mSecondary + font.pointSize: Style.fontSizeXS * scaling + } + Rectangle { + width: 6 * scaling + height: 6 * scaling + radius: Style.radiusXS * scaling + color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary + Layout.alignment: Qt.AlignVCenter + } + Item { Layout.fillWidth: true - width: parent.width - maximumLineCount: 3 - elide: Text.ElideRight + } + } + + RowLayout { + id: bodyRow + spacing: Style.marginM * scaling + + NImageCircled { + id: appAvatar + Layout.preferredWidth: 40 * scaling + Layout.preferredHeight: 40 * scaling + Layout.alignment: Qt.AlignTop + anchors.topMargin: textContent.childrenRect.y + imagePath: model.image && model.image !== "" ? model.image : "" + fallbackIcon: "" + borderColor: Color.transparent + borderWidth: 0 + visible: (model.image && model.image !== "") + Layout.fillWidth: false + Layout.fillHeight: false } - NText { - text: model.body || "" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurface - wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Column { + id: textContent + spacing: Style.marginS * scaling Layout.fillWidth: true - width: parent.width - maximumLineCount: 5 - elide: Text.ElideRight + // Ensure a concrete width so text wraps + width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0)) + + NText { + text: model.summary || "No summary" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Layout.fillWidth: true + width: parent.width + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: model.body || "" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Layout.fillWidth: true + width: parent.width + maximumLineCount: 5 + elide: Text.ElideRight + } } } } } - // Actions removed + // Notification actions - positioned below the main content + RowLayout { + id: actionsRow + Layout.fillWidth: true + spacing: Style.marginS * scaling + visible: model.rawNotification && model.rawNotification.actions + && model.rawNotification.actions.length > 0 + + property var notificationActions: model.rawNotification ? model.rawNotification.actions : [] + + Component.onCompleted: { + console.log("Actions row created, rawNotification:", model.rawNotification) + if (model.rawNotification) { + console.log("Actions:", model.rawNotification.actions) + console.log("Actions length:", + model.rawNotification.actions ? model.rawNotification.actions.length : "null") + } + } + + Repeater { + model: actionsRow.notificationActions + + delegate: NButton { + text: { + var actionText = modelData.text || "Action" + // If text contains comma, take the part after the comma (the display text) + if (actionText.includes(",")) { + return actionText.split(",")[1] || actionText + } + return actionText + } + fontSize: Style.fontSizeS * scaling + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + hoverColor: Color.mSecondary + pressColor: Color.mTertiary + outlined: false + customHeight: 32 * scaling + + Component.onCompleted: { + console.log("Action button created:", modelData.text, "Display text:", text) + } + + onClicked: { + console.log("Action clicked:", modelData.text) + if (modelData && modelData.invoke) { + modelData.invoke() + } + } + } + } + } } NIconButton { diff --git a/README.md b/README.md index 223936c..60414c3 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,10 @@ The launcher supports special commands for enhanced functionality: For Niri: ``` +debug { + honor-xdg-activation-with-invalid-serial +} + window-rule { geometry-corner-radius 20 clip-to-geometry true @@ -279,6 +283,8 @@ layer-rule { place-within-backdrop true } ``` +`honor-xdg-activation-with-invalid-serial` allows notification actions (like view etc) to work. + --- From 94293e4c6378be2c5d352d6184a6db172b727829 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 17:44:04 -0400 Subject: [PATCH 16/61] Bar SysMon: converted to Layout --- Modules/Bar/Widgets/SystemMonitor.qml | 207 +++++++++++++------------- Services/SystemStatService.qml | 2 +- 2 files changed, 105 insertions(+), 104 deletions(-) diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index ce16aa3..6c2346c 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -1,145 +1,146 @@ import QtQuick +import QtQuick.Layouts import Quickshell import qs.Commons import qs.Services import qs.Widgets -Row { +RowLayout { id: root property ShellScreen screen property real scaling: 1.0 - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling Rectangle { - // Let the Rectangle size itself based on its content (the Row) - width: row.width + Style.marginM * scaling * 2 + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2 + Layout.alignment: Qt.AlignVCenter - height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant - anchors.verticalCenter: parent.verticalCenter - - Item { - id: mainContainer + RowLayout { + id: mainLayout anchors.fill: parent anchors.leftMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling + spacing: Style.marginS * scaling - Row { - id: row - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS * scaling - Row { - id: cpuUsageLayout - spacing: Style.marginXS * scaling + // CPU Usage Component + RowLayout { + id: cpuUsageLayout + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignVCenter - NIcon { - id: cpuUsageIcon - text: "speed" - anchors.verticalCenter: parent.verticalCenter - } - - NText { - id: cpuUsageText - text: `${SystemStatService.cpuUsage}%` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NIcon { + id: cpuUsageIcon + text: "speed" + Layout.alignment: Qt.AlignVCenter } - // CPU Temperature Component - Row { - id: cpuTempLayout - // spacing is thin here to compensate for the vertical thermometer icon - spacing: Style.marginXXS * scaling + NText { + id: cpuUsageText + text: `${SystemStatService.cpuUsage}%` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } + } - NIcon { - text: "thermometer" - anchors.verticalCenter: parent.verticalCenter - } + // CPU Temperature Component + RowLayout { + id: cpuTempLayout + // spacing is thin here to compensate for the vertical thermometer icon + spacing: Style.marginXXS * scaling + Layout.alignment: Qt.AlignVCenter - NText { - text: `${SystemStatService.cpuTemp}°C` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NIcon { + text: "thermometer" + Layout.alignment: Qt.AlignVCenter } - // Memory Usage Component - Row { - id: memoryUsageLayout - spacing: Style.marginXS * scaling + NText { + text: `${SystemStatService.cpuTemp}°C` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } + } - NIcon { - text: "memory" - anchors.verticalCenter: parent.verticalCenter - } + // Memory Usage Component + RowLayout { + id: memoryUsageLayout + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignVCenter - NText { - text: `${SystemStatService.memoryUsageGb}G` - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NIcon { + text: "memory" + Layout.alignment: Qt.AlignVCenter } - // Network Download Speed Component - Row { - id: networkDownloadLayout - spacing: Style.marginXS * scaling - visible: Settings.data.bar.showNetworkStats + NText { + text: `${SystemStatService.memoryUsageGb}G` + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } + } - NIcon { - text: "download" - anchors.verticalCenter: parent.verticalCenter - } + // Network Download Speed Component + RowLayout { + id: networkDownloadLayout + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignVCenter + visible: Settings.data.bar.showNetworkStats - NText { - text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NIcon { + text: "download" + Layout.alignment: Qt.AlignVCenter } - // Network Upload Speed Component - Row { - id: networkUploadLayout - spacing: Style.marginXS * scaling - visible: Settings.data.bar.showNetworkStats + NText { + text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } + } - NIcon { - text: "upload" - anchors.verticalCenter: parent.verticalCenter - } + // Network Upload Speed Component + RowLayout { + id: networkUploadLayout + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignVCenter + visible: Settings.data.bar.showNetworkStats - NText { - text: SystemStatService.formatSpeed(SystemStatService.txSpeed) - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - color: Color.mPrimary - } + NIcon { + text: "upload" + Layout.alignment: Qt.AlignVCenter + } + + NText { + text: SystemStatService.formatSpeed(SystemStatService.txSpeed) + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary } } } diff --git a/Services/SystemStatService.qml b/Services/SystemStatService.qml index 57a7346..4f09c1d 100644 --- a/Services/SystemStatService.qml +++ b/Services/SystemStatService.qml @@ -22,7 +22,7 @@ Singleton { if (bytesPerSecond < 1024) { return bytesPerSecond.toFixed(0) + "B/s" } else if (bytesPerSecond < 1024 * 1024) { - return (bytesPerSecond / 1024).toFixed(1) + "KB/s" + return (bytesPerSecond / 1024).toFixed(0) + "KB/s" } else if (bytesPerSecond < 1024 * 1024 * 1024) { return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s" } else { From 86734f17c493b142312efb4fd48320396c41aea8 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 5 Sep 2025 23:44:49 +0200 Subject: [PATCH 17/61] Notification: remove some logging, implement #223 --- Modules/Notification/Notification.qml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 7abb6cc..b607876 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -108,6 +108,17 @@ Variants { property real opacityValue: 0.0 property bool isRemoving: false + // Right-click to dismiss + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: { + if (mouse.button === Qt.RightButton) { + animateOut() + } + } + } + // Scale and fade-in animation scale: scaleValue opacity: opacityValue @@ -259,15 +270,6 @@ Variants { property var notificationActions: model.rawNotification ? model.rawNotification.actions : [] - Component.onCompleted: { - console.log("Actions row created, rawNotification:", model.rawNotification) - if (model.rawNotification) { - console.log("Actions:", model.rawNotification.actions) - console.log("Actions length:", - model.rawNotification.actions ? model.rawNotification.actions.length : "null") - } - } - Repeater { model: actionsRow.notificationActions @@ -288,12 +290,7 @@ Variants { outlined: false customHeight: 32 * scaling - Component.onCompleted: { - console.log("Action button created:", modelData.text, "Display text:", text) - } - onClicked: { - console.log("Action clicked:", modelData.text) if (modelData && modelData.invoke) { modelData.invoke() } From 93a3bc20908afd74f3489596a75a3944a705cc50 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 17:45:17 -0400 Subject: [PATCH 18/61] SysMonCard: converted to layout --- Modules/SidePanel/Cards/SystemMonitorCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index 9f05e66..87e7188 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -11,7 +11,7 @@ NBox { Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling implicitHeight: content.implicitHeight + Style.marginXS * 2 * scaling - Column { + ColumnLayout { id: content anchors.left: parent.left anchors.right: parent.right From 01aeceddf4b20ce4848c71a41c65096f02b79153 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 17:47:19 -0400 Subject: [PATCH 19/61] PowerPanel: converted to Layout --- Modules/PowerPanel/PowerPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml index e6cfb01..e383c1f 100644 --- a/Modules/PowerPanel/PowerPanel.qml +++ b/Modules/PowerPanel/PowerPanel.qml @@ -264,7 +264,7 @@ NPanel { } // Text content in the middle - Column { + ColumnLayout { anchors.left: iconElement.right anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right anchors.verticalCenter: parent.verticalCenter From e41c35cb5b67815987b52f505df30c12c4344477 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 5 Sep 2025 14:47:32 -0700 Subject: [PATCH 20/61] make default notification action text "Open" --- Modules/Notification/Notification.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 7abb6cc..c1328d9 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -273,7 +273,7 @@ Variants { delegate: NButton { text: { - var actionText = modelData.text || "Action" + var actionText = modelData.text || "Open" // If text contains comma, take the part after the comma (the display text) if (actionText.includes(",")) { return actionText.split(",")[1] || actionText From cbffc1a14c3aebc6130f8719b8692dc2c82aef16 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:05:23 -0400 Subject: [PATCH 21/61] SidePanel: height fix --- Modules/SidePanel/Cards/SystemMonitorCard.qml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index 87e7188..2fc18de 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -22,11 +22,6 @@ NBox { anchors.bottomMargin: Style.marginM * scaling spacing: Style.marginS * scaling - // Slight top padding - Item { - height: Style.marginXS * scaling - } - NCircleStat { value: SystemStatService.cpuUsage icon: "speed" @@ -60,10 +55,5 @@ NBox { width: 72 * scaling height: 68 * scaling } - - // Extra bottom padding to shift the perceived stack slightly upward - Item { - height: Style.marginM * scaling - } } } From 9a14a5cc1040f986366cd163fad67dbc86e06a43 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:05:42 -0400 Subject: [PATCH 22/61] SettingsPanel: converted to layout --- Modules/SettingsPanel/SettingsPanel.qml | 434 +++++++++++++----------- 1 file changed, 232 insertions(+), 202 deletions(-) diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 0cd9b9c..bc4d23b 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -267,239 +267,269 @@ NPanel { } panelContent: Rectangle { - anchors.fill: parent - anchors.margins: Style.marginL * scaling color: Color.transparent - // Scrolling via keyboard - Shortcut { - sequence: "Down" - onActivated: root.scrollDown() - enabled: root.opened - } - - Shortcut { - sequence: "Up" - onActivated: root.scrollUp() - enabled: root.opened - } - - Shortcut { - sequence: "Ctrl+J" - onActivated: root.scrollDown() - enabled: root.opened - } - - Shortcut { - sequence: "Ctrl+K" - onActivated: root.scrollUp() - enabled: root.opened - } - - Shortcut { - sequence: "PgDown" - onActivated: root.scrollPageDown() - enabled: root.opened - } - - Shortcut { - sequence: "PgUp" - onActivated: root.scrollPageUp() - enabled: root.opened - } - - // Changing tab via keyboard - Shortcut { - sequence: "Tab" - onActivated: root.selectNextTab() - enabled: root.opened - } - - Shortcut { - sequence: "Shift+Tab" - onActivated: root.selectPreviousTab() - enabled: root.opened - } - - RowLayout { + // Main layout container that fills the panel + ColumnLayout { anchors.fill: parent - spacing: Style.marginM * scaling + anchors.margins: Style.marginL * scaling + spacing: 0 - Rectangle { - id: sidebar - Layout.preferredWidth: 220 * scaling - Layout.fillHeight: true - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - radius: Style.radiusM * scaling + // Keyboard shortcuts container + Item { + Layout.preferredWidth: 0 + Layout.preferredHeight: 0 - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton // Don't interfere with clicks - property int wheelAccumulator: 0 - onWheel: wheel => { - wheelAccumulator += wheel.angleDelta.y - if (wheelAccumulator >= 120) { - root.selectPreviousTab() - wheelAccumulator = 0 - } else if (wheelAccumulator <= -120) { - root.selectNextTab() - wheelAccumulator = 0 - } - wheel.accepted = true - } + // Scrolling via keyboard + Shortcut { + sequence: "Down" + onActivated: root.scrollDown() + enabled: root.opened } - Column { - anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginXS * 1.5 * scaling + Shortcut { + sequence: "Up" + onActivated: root.scrollUp() + enabled: root.opened + } - Repeater { - id: sections - model: root.tabsModel - delegate: Rectangle { - id: tabItem - width: parent.width - height: 32 * scaling - radius: Style.radiusS * scaling - color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) - readonly property bool selected: index === currentTabIndex - property bool hovering: false - property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) + Shortcut { + sequence: "Ctrl+J" + onActivated: root.scrollDown() + enabled: root.opened + } - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } + Shortcut { + sequence: "Ctrl+K" + onActivated: root.scrollUp() + enabled: root.opened + } - Behavior on tabTextColor { - ColorAnimation { - duration: Style.animationFast - } - } + Shortcut { + sequence: "PgDown" + onActivated: root.scrollPageDown() + enabled: root.opened + } - RowLayout { - anchors.fill: parent - anchors.leftMargin: Style.marginS * scaling - anchors.rightMargin: Style.marginS * scaling - spacing: Style.marginS * scaling - // Tab icon on the left side - NIcon { - text: modelData.icon - color: tabTextColor - font.pointSize: Style.fontSizeL * scaling - } - // Tab label on the left side - NText { - text: modelData.label - color: tabTextColor - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - Layout.fillWidth: true - } - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton - onEntered: tabItem.hovering = true - onExited: tabItem.hovering = false - onCanceled: tabItem.hovering = false - onClicked: currentTabIndex = index - } - } - } + Shortcut { + sequence: "PgUp" + onActivated: root.scrollPageUp() + enabled: root.opened + } + + // Changing tab via keyboard + Shortcut { + sequence: "Tab" + onActivated: root.selectNextTab() + enabled: root.opened + } + + Shortcut { + sequence: "Shift+Tab" + onActivated: root.selectPreviousTab() + enabled: root.opened } } - // Content - Rectangle { - id: contentPane + // Main content area + RowLayout { Layout.fillWidth: true Layout.fillHeight: true - radius: Style.radiusM * scaling - color: Color.mSurfaceVariant - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - clip: true + spacing: Style.marginM * scaling - ColumnLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginS * scaling + // Sidebar + Rectangle { + id: sidebar + Layout.preferredWidth: 220 * scaling + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: Style.radiusM * scaling - RowLayout { - id: headerRow - Layout.fillWidth: true - spacing: Style.marginS * scaling - - // Tab label on the main right side - NText { - text: root.tabsModel[currentTabIndex].label - font.pointSize: Style.fontSizeXL * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.fillWidth: true - } - NIconButton { - icon: "close" - tooltipText: "Close" - Layout.alignment: Qt.AlignVCenter - onClicked: root.close() - } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton // Don't interfere with clicks + property int wheelAccumulator: 0 + onWheel: wheel => { + wheelAccumulator += wheel.angleDelta.y + if (wheelAccumulator >= 120) { + root.selectPreviousTab() + wheelAccumulator = 0 + } else if (wheelAccumulator <= -120) { + root.selectNextTab() + wheelAccumulator = 0 + } + wheel.accepted = true + } } - NDivider { - Layout.fillWidth: true - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS * scaling + spacing: Style.marginXS * 1.5 * scaling Repeater { + id: sections model: root.tabsModel - delegate: Loader { - anchors.fill: parent - active: index === root.currentTabIndex + delegate: Rectangle { + id: tabItem + Layout.fillWidth: true + Layout.preferredHeight: tabEntryRow.implicitHeight + Style.marginS * scaling * 2 + radius: Style.radiusS * scaling + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) + readonly property bool selected: index === currentTabIndex + property bool hovering: false + property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) - onStatusChanged: { - if (status === Loader.Ready && item) { - // Find and store reference to the ScrollView - const scrollView = item.children[0] - if (scrollView && scrollView.toString().includes("ScrollView")) { - root.activeScrollView = scrollView - } + Behavior on color { + ColorAnimation { + duration: Style.animationFast } } - sourceComponent: Flickable { - // Using a Flickable here with a pressDelay to fix conflict between - // ScrollView and NTextInput. This fix the weird text selection issue. - id: flickable + Behavior on tabTextColor { + ColorAnimation { + duration: Style.animationFast + } + } + + RowLayout { + id: tabEntryRow anchors.fill: parent - pressDelay: 200 + anchors.margins: Style.marginS * scaling + spacing: Style.marginS * scaling - ScrollView { - id: scrollView - anchors.fill: parent - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - padding: Style.marginL * scaling - clip: true + // Tab icon + NIcon { + text: modelData.icon + color: tabTextColor + font.pointSize: Style.fontSizeL * scaling + } + + // Tab label + NText { + text: modelData.label + color: tabTextColor + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + } - Component.onCompleted: { - root.activeScrollView = scrollView + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onEntered: tabItem.hovering = true + onExited: tabItem.hovering = false + onCanceled: tabItem.hovering = false + onClicked: currentTabIndex = index + } + } + } + + Item { + Layout.fillHeight: true + } + } + } + + // Content pane + Rectangle { + id: contentPane + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + radius: Style.radiusM * scaling + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + clip: true + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginS * scaling + + // Header row + RowLayout { + id: headerRow + Layout.fillWidth: true + spacing: Style.marginS * scaling + + // Tab title + NText { + text: root.tabsModel[currentTabIndex]?.label || "" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + // Close button + NIconButton { + icon: "close" + tooltipText: "Close" + Layout.alignment: Qt.AlignVCenter + onClicked: root.close() + } + } + + // Divider + NDivider { + Layout.fillWidth: true + } + + // Tab content area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.transparent + + Repeater { + model: root.tabsModel + delegate: Loader { + anchors.fill: parent + active: index === root.currentTabIndex + + onStatusChanged: { + if (status === Loader.Ready && item) { + // Find and store reference to the ScrollView + const scrollView = item.children[0] + if (scrollView && scrollView.toString().includes("ScrollView")) { + root.activeScrollView = scrollView + } } + } - Loader { - active: true - sourceComponent: root.tabsModel[index].source - width: scrollView.availableWidth + sourceComponent: Flickable { + // Using a Flickable here with a pressDelay to fix conflict between + // ScrollView and NTextInput. This fixes the weird text selection issue. + id: flickable + anchors.fill: parent + pressDelay: 200 + + ScrollView { + id: scrollView + anchors.fill: parent + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + padding: Style.marginL * scaling + clip: true + + Component.onCompleted: { + root.activeScrollView = scrollView + } + + Loader { + active: true + sourceComponent: root.tabsModel[index]?.source + width: scrollView.availableWidth + } } } } @@ -510,4 +540,4 @@ NPanel { } } } -} +} \ No newline at end of file From 476848597431f9ac1952bffa4f1f6ceb6f845e4c Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:15:28 -0400 Subject: [PATCH 23/61] LockScreen: converted to Layout --- Modules/LockScreen/LockScreen.qml | 714 ++++++++++++------------ Modules/SettingsPanel/SettingsPanel.qml | 4 +- 2 files changed, 357 insertions(+), 361 deletions(-) diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index d46dcfd..3382b69 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -155,7 +155,7 @@ Loader { anchors.topMargin: 80 * scaling spacing: 40 * scaling - Column { + ColumnLayout { spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignHCenter @@ -168,6 +168,7 @@ Loader { font.letterSpacing: -2 * scaling color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter SequentialAnimation on scale { loops: Animation.Infinite @@ -192,22 +193,23 @@ Loader { font.weight: Font.Light color: Color.mOnSurface horizontalAlignment: Text.AlignHCenter - width: timeText.width + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: timeText.implicitWidth } } - Column { + ColumnLayout { spacing: Style.marginM * scaling Layout.alignment: Qt.AlignHCenter Rectangle { - width: 108 * scaling - height: 108 * scaling + Layout.preferredWidth: 108 * scaling + Layout.preferredHeight: 108 * scaling + Layout.alignment: Qt.AlignHCenter radius: width * 0.5 color: Color.transparent border.color: Color.mPrimary border.width: Math.max(1, Style.borderL * scaling) - anchors.horizontalCenter: parent.horizontalCenter z: 10 Loader { @@ -375,377 +377,371 @@ Loader { anchors.centerIn: parent anchors.verticalCenterOffset: 50 * scaling - Item { - width: parent.width - height: 280 * scaling - Layout.fillWidth: true - - Rectangle { - id: terminalBackground - anchors.fill: parent - radius: Style.radiusM * scaling - color: Qt.alpha(Color.mSurface, 0.9) - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderM * scaling) - - Repeater { - model: 20 - Rectangle { - width: parent.width - height: 1 - color: Qt.alpha(Color.mPrimary, 0.1) - y: index * 10 * scaling - opacity: Style.opacityMedium - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { - to: 0.6 - duration: 2000 + Math.random() * 1000 - } - NumberAnimation { - to: 0.1 - duration: 2000 + Math.random() * 1000 - } - } - } - } + Rectangle { + id: terminalBackground + anchors.fill: parent + radius: Style.radiusM * scaling + color: Qt.alpha(Color.mSurface, 0.9) + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderM * scaling) + Repeater { + model: 20 Rectangle { width: parent.width - height: 40 * scaling - color: Qt.alpha(Color.mPrimary, 0.2) - topLeftRadius: Style.radiusS * scaling - topRightRadius: Style.radiusS * scaling - - RowLayout { - anchors.fill: parent - anchors.topMargin: Style.marginM * scaling - anchors.bottomMargin: Style.marginM * scaling - anchors.leftMargin: Style.marginL * scaling - anchors.rightMargin: Style.marginL * scaling - spacing: Style.marginM * scaling - - NText { - text: "SECURE TERMINAL" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - Layout.fillWidth: true - } - - Row { - spacing: Style.marginS * scaling - visible: batteryIndicator.batteryVisible - NIcon { - text: batteryIndicator.getIcon() - font.pointSize: Style.fontSizeM * scaling - color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface - } - NText { - text: Math.round(batteryIndicator.percent) + "%" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - } - - Row { - spacing: Style.marginS * scaling - NText { - text: keyboardLayout.currentLayout - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - NIcon { - text: "keyboard_alt" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurface - } - } - } - } - - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Style.marginL * scaling - anchors.topMargin: 70 * scaling - spacing: Style.marginM * scaling - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: Quickshell.env("USER") + "@noctalia:~$" - color: Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - } - - NText { - id: welcomeText - text: "" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - property int currentIndex: 0 - property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" - - Timer { - interval: Style.animationFast - running: true - repeat: true - onTriggered: { - if (parent.currentIndex < parent.fullText.length) { - parent.text = parent.fullText.substring(0, parent.currentIndex + 1) - parent.currentIndex++ - } else { - running = false - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Style.marginM * scaling - - NText { - text: Quickshell.env("USER") + "@noctalia:~$" - color: Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - } - - NText { - text: "sudo unlock-session" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - } - - TextInput { - id: passwordInput - width: 0 - height: 0 - visible: false - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - echoMode: TextInput.Password - passwordCharacter: "*" - passwordMaskDelay: 0 - - text: lockContext.currentText - onTextChanged: { - lockContext.currentText = text - } - - Keys.onPressed: function (event) { - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - lockContext.tryUnlock() - } - } - - Component.onCompleted: { - forceActiveFocus() - } - } - - NText { - id: asterisksText - text: "*".repeat(passwordInput.text.length) - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeL * scaling - visible: passwordInput.activeFocus - - SequentialAnimation { - id: typingEffect - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.01 - duration: 50 - } - NumberAnimation { - target: passwordInput - property: "scale" - to: 1.0 - duration: 50 - } - } - } - - Rectangle { - width: 8 * scaling - height: 20 * scaling - color: Color.mPrimary - visible: passwordInput.activeFocus - Layout.leftMargin: -Style.marginS * scaling - Layout.alignment: Qt.AlignVCenter - - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 500 - } - NumberAnimation { - to: 0.0 - duration: 500 - } - } - } - } - - NText { - text: { - if (lockContext.unlockInProgress) - return "Authenticating..." - if (lockContext.showFailure && lockContext.errorMessage) - return lockContext.errorMessage - if (lockContext.showFailure) - return "Authentication failed." - return "" - } - color: { - if (lockContext.unlockInProgress) - return Color.mPrimary - if (lockContext.showFailure) - return Color.mError - return Color.transparent - } - font.family: "DejaVu Sans Mono" - font.pointSize: Style.fontSizeL * scaling - Layout.fillWidth: true - - SequentialAnimation on opacity { - running: lockContext.unlockInProgress - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - } - NumberAnimation { - to: 0.5 - duration: 800 - } - } - } - - Row { - Layout.alignment: Qt.AlignRight - Layout.bottomMargin: -10 * scaling - Rectangle { - width: 120 * scaling - height: 40 * scaling - radius: Style.radiusS * scaling - color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2) - border.color: Color.mPrimary - border.width: Math.max(1, Style.borderS * scaling) - enabled: !lockContext.unlockInProgress - - NText { - anchors.centerIn: parent - text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE" - color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - - MouseArea { - id: executeButtonArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - lockContext.tryUnlock() - } - - SequentialAnimation on scale { - running: executeButtonArea.containsMouse - NumberAnimation { - to: 1.05 - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - - SequentialAnimation on scale { - running: !executeButtonArea.containsMouse - NumberAnimation { - to: 1.0 - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - } - - SequentialAnimation on scale { - loops: Animation.Infinite - running: lockContext.unlockInProgress - NumberAnimation { - to: 1.02 - duration: 600 - easing.type: Easing.InOutQuad - } - NumberAnimation { - to: 1.0 - duration: 600 - easing.type: Easing.InOutQuad - } - } - } - } - } - - Rectangle { - anchors.fill: parent - radius: parent.radius - color: Color.transparent - border.color: Qt.alpha(Color.mPrimary, 0.3) - border.width: Math.max(1, Style.borderS * scaling) - z: -1 - + height: 1 + color: Qt.alpha(Color.mPrimary, 0.1) + y: index * 10 * scaling + opacity: Style.opacityMedium SequentialAnimation on opacity { loops: Animation.Infinite NumberAnimation { to: 0.6 - duration: 2000 - easing.type: Easing.InOutQuad + duration: 2000 + Math.random() * 1000 } NumberAnimation { - to: 0.2 - duration: 2000 - easing.type: Easing.InOutQuad + to: 0.1 + duration: 2000 + Math.random() * 1000 } } } } + + Rectangle { + width: parent.width + height: 40 * scaling + color: Qt.alpha(Color.mPrimary, 0.2) + topLeftRadius: Style.radiusS * scaling + topRightRadius: Style.radiusS * scaling + + RowLayout { + anchors.fill: parent + anchors.topMargin: Style.marginM * scaling + anchors.bottomMargin: Style.marginM * scaling + anchors.leftMargin: Style.marginL * scaling + anchors.rightMargin: Style.marginL * scaling + spacing: Style.marginM * scaling + + NText { + text: "SECURE TERMINAL" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + + RowLayout { + spacing: Style.marginS * scaling + visible: batteryIndicator.batteryVisible + NIcon { + text: batteryIndicator.getIcon() + font.pointSize: Style.fontSizeM * scaling + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface + } + NText { + text: Math.round(batteryIndicator.percent) + "%" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + } + + RowLayout { + spacing: Style.marginS * scaling + NText { + text: keyboardLayout.currentLayout + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + NIcon { + text: "keyboard_alt" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + } + } + } + } + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Style.marginL * scaling + anchors.topMargin: 70 * scaling + spacing: Style.marginM * scaling + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: Quickshell.env("USER") + "@noctalia:~$" + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + } + + NText { + id: welcomeText + text: "" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + property int currentIndex: 0 + property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" + + Timer { + interval: Style.animationFast + running: true + repeat: true + onTriggered: { + if (parent.currentIndex < parent.fullText.length) { + parent.text = parent.fullText.substring(0, parent.currentIndex + 1) + parent.currentIndex++ + } else { + running = false + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: Quickshell.env("USER") + "@noctalia:~$" + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + } + + NText { + text: "sudo unlock-session" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + } + + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + echoMode: TextInput.Password + passwordCharacter: "*" + passwordMaskDelay: 0 + + text: lockContext.currentText + onTextChanged: { + lockContext.currentText = text + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + lockContext.tryUnlock() + } + } + + Component.onCompleted: { + forceActiveFocus() + } + } + + NText { + id: asterisksText + text: "*".repeat(passwordInput.text.length) + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + visible: passwordInput.activeFocus + + SequentialAnimation { + id: typingEffect + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.01 + duration: 50 + } + NumberAnimation { + target: passwordInput + property: "scale" + to: 1.0 + duration: 50 + } + } + } + + Rectangle { + width: 8 * scaling + height: 20 * scaling + color: Color.mPrimary + visible: passwordInput.activeFocus + Layout.leftMargin: -Style.marginS * scaling + Layout.alignment: Qt.AlignVCenter + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 500 + } + NumberAnimation { + to: 0.0 + duration: 500 + } + } + } + } + + NText { + text: { + if (lockContext.unlockInProgress) + return "Authenticating..." + if (lockContext.showFailure && lockContext.errorMessage) + return lockContext.errorMessage + if (lockContext.showFailure) + return "Authentication failed." + return "" + } + color: { + if (lockContext.unlockInProgress) + return Color.mPrimary + if (lockContext.showFailure) + return Color.mError + return Color.transparent + } + font.family: "DejaVu Sans Mono" + font.pointSize: Style.fontSizeL * scaling + Layout.fillWidth: true + + SequentialAnimation on opacity { + running: lockContext.unlockInProgress + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + } + NumberAnimation { + to: 0.5 + duration: 800 + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignRight + Layout.bottomMargin: -10 * scaling + Rectangle { + Layout.preferredWidth: 120 * scaling + Layout.preferredHeight: 40 * scaling + radius: Style.radiusS * scaling + color: executeButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.2) + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderS * scaling) + enabled: !lockContext.unlockInProgress + + NText { + anchors.centerIn: parent + text: lockContext.unlockInProgress ? "EXECUTING" : "EXECUTE" + color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + + MouseArea { + id: executeButtonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + lockContext.tryUnlock() + } + + SequentialAnimation on scale { + running: executeButtonArea.containsMouse + NumberAnimation { + to: 1.05 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + SequentialAnimation on scale { + running: !executeButtonArea.containsMouse + NumberAnimation { + to: 1.0 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + SequentialAnimation on scale { + loops: Animation.Infinite + running: lockContext.unlockInProgress + NumberAnimation { + to: 1.02 + duration: 600 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 600 + easing.type: Easing.InOutQuad + } + } + } + } + } + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: Color.transparent + border.color: Qt.alpha(Color.mPrimary, 0.3) + border.width: Math.max(1, Style.borderS * scaling) + z: -1 + + SequentialAnimation on opacity { + loops: Animation.Infinite + NumberAnimation { + to: 0.6 + duration: 2000 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 0.2 + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } } } - // Power buttons at bottom - Row { + // Power buttons at bottom right + RowLayout { anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 50 * scaling spacing: 20 * scaling Rectangle { - width: 60 * scaling - height: 60 * scaling + Layout.preferredWidth: 60 * scaling + Layout.preferredHeight: 60 * scaling radius: width * 0.5 color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2) border.color: Color.mError @@ -769,8 +765,8 @@ Loader { } Rectangle { - width: 60 * scaling - height: 60 * scaling + Layout.preferredWidth: 60 * scaling + Layout.preferredHeight: 60 * scaling radius: width * 0.5 color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight) border.color: Color.mPrimary @@ -794,8 +790,8 @@ Loader { } Rectangle { - width: 60 * scaling - height: 60 * scaling + Layout.preferredWidth: 60 * scaling + Layout.preferredHeight: 60 * scaling radius: width * 0.5 color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2) border.color: Color.mSecondary diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index bc4d23b..1e6d6cc 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -407,7 +407,7 @@ NPanel { color: tabTextColor font.pointSize: Style.fontSizeL * scaling } - + // Tab label NText { text: modelData.label @@ -540,4 +540,4 @@ NPanel { } } } -} \ No newline at end of file +} From a4c98f13829d064dc7480a29622a0838e1cf73cc Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:19:27 -0400 Subject: [PATCH 24/61] NotificationHistory: fully converted to Layout --- .../Notification/NotificationHistoryPanel.qml | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 7bc2378..0d4bff9 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -25,6 +25,7 @@ NPanel { anchors.margins: Style.marginL * scaling spacing: Style.marginM * scaling + // Header section RowLayout { Layout.fillWidth: true spacing: Style.marginM * scaling @@ -78,38 +79,36 @@ NPanel { } // Empty state when no notifications - Item { + ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true + Layout.alignment: Qt.AlignCenter visible: NotificationService.historyModel.count === 0 + spacing: Style.marginM * scaling - ColumnLayout { - anchors.centerIn: parent - spacing: Style.marginM * scaling + NIcon { + text: "notifications_off" + font.pointSize: Style.fontSizeXXXL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } - NIcon { - text: "notifications_off" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurface - Layout.alignment: Qt.AlignHCenter - } + NText { + text: "No notifications" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } - NText { - text: "No notifications" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Your notifications will show up here as they arrive." - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + NText { + text: "Your notifications will show up here as they arrive." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter } } + // Notification list ListView { id: notificationList Layout.fillWidth: true @@ -121,21 +120,21 @@ NPanel { visible: NotificationService.historyModel.count > 0 delegate: Rectangle { - width: notificationList ? notificationList.width : 380 * scaling - height: Math.max(80, notificationContent.height + 30) + width: notificationList.width + height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2) radius: Style.radiusM * scaling color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant + border.color: Qt.alpha(Color.mOutline, Style.opacityMedium) + border.width: Math.max(1, Style.borderS * scaling) RowLayout { - anchors { - fill: parent - margins: Style.marginM * scaling - } + id: notificationLayout + anchors.fill: parent + anchors.margins: Style.marginM * scaling spacing: Style.marginM * scaling - // Notification content - Column { - id: notificationContent + // Notification content column + ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter spacing: Style.marginXXS * scaling @@ -146,7 +145,8 @@ NPanel { font.weight: Font.Medium color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary wrapMode: Text.Wrap - width: parent.width - 60 + Layout.fillWidth: true + Layout.maximumWidth: parent.width maximumLineCount: 2 elide: Text.ElideRight } @@ -156,23 +156,27 @@ NPanel { font.pointSize: Style.fontSizeXS * scaling color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface wrapMode: Text.Wrap - width: parent.width - 60 + Layout.fillWidth: true + Layout.maximumWidth: parent.width maximumLineCount: 3 elide: Text.ElideRight + visible: text.length > 0 } NText { text: NotificationService.formatTimestamp(timestamp) font.pointSize: Style.fontSizeXS * scaling color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface + Layout.fillWidth: true } } - // Trash icon button + // Delete button NIconButton { icon: "delete" tooltipText: "Delete notification" sizeRatio: 0.7 + Layout.alignment: Qt.AlignTop onClicked: { Logger.log("NotificationHistory", "Removing notification:", summary) @@ -185,7 +189,7 @@ NPanel { MouseArea { id: notificationMouseArea anchors.fill: parent - anchors.rightMargin: Style.marginL * 3 * scaling + anchors.rightMargin: Style.marginXL * scaling hoverEnabled: true } } From cf624f4d65fd80e45fda417bc6d037cdd99eb399 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:29:06 -0400 Subject: [PATCH 25/61] Notification: Converted to Layout + removed fontPointSize on NIconButton. use sizeRatio instead. --- Modules/Notification/Notification.qml | 161 ++++++++++++-------------- Widgets/NIconButton.qml | 3 +- Widgets/NToast.qml | 1 - 3 files changed, 78 insertions(+), 87 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index c9a0b0b..757549c 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -78,7 +78,7 @@ Variants { } // Main notification container - Column { + ColumnLayout { id: notificationStack // Position based on bar location anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined @@ -92,11 +92,9 @@ Variants { Repeater { model: notificationModel delegate: Rectangle { - width: 360 * scaling - height: Math.max( - 80 * scaling, - contentRow.implicitHeight + (actionsRow.visible ? actionsRow.implicitHeight + Style.marginM - * scaling : 0) + (Style.marginL * 2 * scaling)) + Layout.preferredWidth: 360 * scaling + Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling) + Layout.maximumHeight: Layout.preferredHeight clip: true radius: Style.radiusL * scaling border.color: Color.mOutline @@ -171,98 +169,87 @@ Variants { } ColumnLayout { + id: notificationLayout anchors.fill: parent anchors.margins: Style.marginM * scaling + anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button spacing: Style.marginM * scaling + // Header section with app name and timestamp RowLayout { - id: contentRow - spacing: Style.marginM * scaling Layout.fillWidth: true + spacing: Style.marginS * scaling - // Right: header on top, then avatar + texts - ColumnLayout { - id: textColumn - spacing: Style.marginS * scaling + NText { + text: `${(model.appName || model.desktopEntry) + || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` + color: Color.mSecondary + font.pointSize: Style.fontSizeXS * scaling + + } + + Rectangle { + Layout.preferredWidth: 6 * scaling + Layout.preferredHeight: 6 * scaling + radius: Style.radiusXS * scaling + color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary + Layout.alignment: Qt.AlignVCenter + } + + Item { Layout.fillWidth: true + } + } - RowLayout { - spacing: Style.marginS * scaling - id: appHeaderRow - NText { - text: `${(model.appName || model.desktopEntry) - || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` - color: Color.mSecondary - font.pointSize: Style.fontSizeXS * scaling - } - Rectangle { - width: 6 * scaling - height: 6 * scaling - radius: Style.radiusXS * scaling - color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary - Layout.alignment: Qt.AlignVCenter - } - Item { - Layout.fillWidth: true - } + // Main content section + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + // Avatar + NImageCircled { + id: appAvatar + Layout.preferredWidth: 40 * scaling + Layout.preferredHeight: 40 * scaling + Layout.alignment: Qt.AlignTop + imagePath: model.image && model.image !== "" ? model.image : "" + fallbackIcon: "" + borderColor: Color.transparent + borderWidth: 0 + visible: (model.image && model.image !== "") + } + + // Text content + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS * scaling + + NText { + text: model.summary || "No summary" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Layout.fillWidth: true + maximumLineCount: 3 + elide: Text.ElideRight } - RowLayout { - id: bodyRow - spacing: Style.marginM * scaling - - NImageCircled { - id: appAvatar - Layout.preferredWidth: 40 * scaling - Layout.preferredHeight: 40 * scaling - Layout.alignment: Qt.AlignTop - anchors.topMargin: textContent.childrenRect.y - imagePath: model.image && model.image !== "" ? model.image : "" - fallbackIcon: "" - borderColor: Color.transparent - borderWidth: 0 - visible: (model.image && model.image !== "") - Layout.fillWidth: false - Layout.fillHeight: false - } - - Column { - id: textContent - spacing: Style.marginS * scaling - Layout.fillWidth: true - // Ensure a concrete width so text wraps - width: (textColumn.width - (appAvatar.visible ? (appAvatar.width + Style.marginM * scaling) : 0)) - - NText { - text: model.summary || "No summary" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightMedium - color: Color.mOnSurface - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - Layout.fillWidth: true - width: parent.width - maximumLineCount: 3 - elide: Text.ElideRight - } - - NText { - text: model.body || "" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurface - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - Layout.fillWidth: true - width: parent.width - maximumLineCount: 5 - elide: Text.ElideRight - } - } + NText { + text: model.body || "" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurface + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + Layout.fillWidth: true + maximumLineCount: 5 + elide: Text.ElideRight + visible: text.length > 0 } } } - // Notification actions - positioned below the main content + // Notification actions RowLayout { - id: actionsRow Layout.fillWidth: true spacing: Style.marginS * scaling visible: model.rawNotification && model.rawNotification.actions @@ -271,7 +258,7 @@ Variants { property var notificationActions: model.rawNotification ? model.rawNotification.actions : [] Repeater { - model: actionsRow.notificationActions + model: parent.notificationActions delegate: NButton { text: { @@ -289,6 +276,7 @@ Variants { pressColor: Color.mTertiary outlined: false customHeight: 32 * scaling + Layout.preferredHeight: 32 * scaling onClicked: { if (modelData && modelData.invoke) { @@ -297,14 +285,19 @@ Variants { } } } + + // Spacer to push buttons to the left if needed + Item { + Layout.fillWidth: true + } } } + // Close button positioned absolutely NIconButton { icon: "close" tooltipText: "Close" sizeRatio: 0.6 - fontPointSize: 12 anchors.top: parent.top anchors.topMargin: Style.marginM * scaling anchors.right: parent.right diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 0650839..c9755b3 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -15,7 +15,6 @@ Rectangle { property string tooltipText property bool enabled: true property bool hovering: false - property real fontPointSize: Style.fontSizeM property color colorBg: Color.mSurfaceVariant property color colorFg: Color.mPrimary @@ -41,7 +40,7 @@ Rectangle { NIcon { text: root.icon - font.pointSize: root.fontPointSize * scaling + font.pointSize: Style.fontSizeM * scaling color: root.hovering ? colorFgHover : colorFg // Center horizontally x: (root.width - width) / 2 diff --git a/Widgets/NToast.qml b/Widgets/NToast.qml index 3ed8c6b..7a60c6c 100644 --- a/Widgets/NToast.qml +++ b/Widgets/NToast.qml @@ -180,7 +180,6 @@ Item { colorBorder: Color.transparent colorBorderHover: Color.mOutline - fontPointSize: Style.fontSizeM * scaling sizeRatio: 0.8 Layout.alignment: Qt.AlignTop From 2533c52e275eff0dc720f3260e8f43c063fd879b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 00:30:47 +0200 Subject: [PATCH 26/61] Launcher: add app2unit options (hopefully) --- Commons/Settings.qml | 1 + Modules/Launcher/Plugins/ApplicationsPlugin.qml | 6 +++++- Modules/SettingsPanel/Tabs/LauncherTab.qml | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 9565e2c..097f5e9 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -278,6 +278,7 @@ Singleton { property string position: "center" property real backgroundOpacity: 1.0 property list pinnedExecs: [] + property bool useApp2Unit: false } // dock diff --git a/Modules/Launcher/Plugins/ApplicationsPlugin.qml b/Modules/Launcher/Plugins/ApplicationsPlugin.qml index 4c02e6b..8b88523 100644 --- a/Modules/Launcher/Plugins/ApplicationsPlugin.qml +++ b/Modules/Launcher/Plugins/ApplicationsPlugin.qml @@ -82,7 +82,11 @@ Item { "isImage": false, "onActivate": function () { Logger.log("ApplicationsPlugin", `Launching: ${app.name}`) - if (app.execute) { + + if (Settings.data.appLauncher.useApp2Unit && app.id) { + Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`) + Quickshell.execDetached(["app2unit", "--", app.id]) + } else if (app.execute) { app.execute() } else if (app.exec) { // Fallback to manual execution diff --git a/Modules/SettingsPanel/Tabs/LauncherTab.qml b/Modules/SettingsPanel/Tabs/LauncherTab.qml index f303273..6ca4ece 100644 --- a/Modules/SettingsPanel/Tabs/LauncherTab.qml +++ b/Modules/SettingsPanel/Tabs/LauncherTab.qml @@ -59,6 +59,13 @@ ColumnLayout { onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked } + NToggle { + label: "Use App2Unit for Launching" + description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration." + checked: Settings.data.appLauncher.useApp2Unit + onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked + } + ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true From 5d7e168a57a09db4e1b2cf30a2304b7b7fe4a410 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:33:51 -0400 Subject: [PATCH 27/61] NCircleStat + KeyboardLayout: converted to Layout --- Modules/Bar/Widgets/KeyboardLayout.qml | 15 ++++++++----- Widgets/NCircleStat.qml | 29 +++++++++++++------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index 24f0c26..db9dc0a 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Io @@ -6,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -Row { +RowLayout { id: root property ShellScreen screen @@ -18,12 +19,17 @@ Row { // Use the shared service for keyboard layout property string currentLayout: KeyboardLayoutService.currentLayout - width: pill.width - height: pill.height + Layout.preferredWidth: pill.implicitWidth + Layout.preferredHeight: pill.implicitHeight + spacing: 0 NPill { id: pill + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: false + Layout.fillHeight: false + rightOpen: BarWidgetRegistry.getNPillDirection(root) icon: "keyboard_alt" iconCircleColor: Color.mPrimary @@ -33,9 +39,8 @@ Row { tooltipText: "Keyboard layout: " + currentLayout onClicked: { - // You could open keyboard settings here if needed // For now, just show the current layout } } -} +} \ No newline at end of file diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index 1bd9e67..f6c1b40 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -1,9 +1,10 @@ import QtQuick +import QtQuick.Layouts import qs.Commons import qs.Services import qs.Widgets -// Compact circular statistic display used in the SidePanel +// Compact circular statistic display using Layout management Rectangle { id: root @@ -28,20 +29,20 @@ Rectangle { // Repaint gauge when the bound value changes onValueChanged: gauge.requestPaint() - Row { - id: innerRow + ColumnLayout { + id: mainLayout anchors.fill: parent anchors.margins: Style.marginS * scaling * contentScale - spacing: Style.marginS * scaling * contentScale - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter + spacing: 0 - // Gauge with percentage label placed inside the open gap (right side) + // Main gauge container Item { - id: gaugeWrap - anchors.verticalCenter: innerRow.verticalCenter - width: 68 * scaling * contentScale - height: 68 * scaling * contentScale + id: gaugeContainer + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: 68 * scaling * contentScale + Layout.preferredHeight: 68 * scaling * contentScale Canvas { id: gauge @@ -84,15 +85,13 @@ Rectangle { horizontalAlignment: Text.AlignHCenter } - // Tiny circular badge for the icon, inside the right-side gap + // Tiny circular badge for the icon, positioned using anchors within the gauge Rectangle { id: iconBadge width: 28 * scaling * contentScale height: width radius: width / 2 color: Color.mSurface - // border.color: Color.mPrimary - // border.width: Math.max(1, Style.borderS * scaling) anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: -6 * scaling * contentScale @@ -109,4 +108,4 @@ Rectangle { } } } -} +} \ No newline at end of file From 59bf98e04c8ac5fa0d56c1937a9c342a01d7c9ec Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 00:44:05 +0200 Subject: [PATCH 28/61] Vesktop Template: fix placeholder text --- Assets/Matugen/templates/vesktop.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Matugen/templates/vesktop.css b/Assets/Matugen/templates/vesktop.css index 9876c8a..ac5b166 100644 --- a/Assets/Matugen/templates/vesktop.css +++ b/Assets/Matugen/templates/vesktop.css @@ -510,7 +510,7 @@ } .visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before { - content: "Message #general" !important; + content: "send a message" !important; color: {{colors.on_surface_variant.default.hex}} !important; } From 7b5c97f38ab6e78c88c1ca8cf18efe488da8406a Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:49:34 -0400 Subject: [PATCH 29/61] Tray: converted to Layout --- Modules/Bar/Widgets/KeyboardLayout.qml | 3 ++- Modules/Bar/Widgets/Tray.qml | 21 +++++++++++---------- Modules/Notification/Notification.qml | 1 - Widgets/NCircleStat.qml | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index db9dc0a..e304b7e 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -39,8 +39,9 @@ RowLayout { tooltipText: "Keyboard layout: " + currentLayout onClicked: { + // You could open keyboard settings here if needed // For now, just show the current layout } } -} \ No newline at end of file +} diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index f29d7b7..06de40f 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -26,26 +26,26 @@ Rectangle { } visible: SystemTray.items.values.length > 0 - implicitWidth: tray.width + Style.marginM * scaling * 2 + implicitWidth: trayLayout.implicitWidth + Style.marginM * scaling * 2 implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant Layout.alignment: Qt.AlignVCenter - Row { - id: tray - - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter + RowLayout { + id: trayLayout + anchors.centerIn: parent spacing: Style.marginS * scaling Repeater { id: repeater model: SystemTray.items + delegate: Item { - width: itemSize - height: itemSize + Layout.preferredWidth: itemSize + Layout.preferredHeight: itemSize + Layout.alignment: Qt.AlignCenter visible: modelData IconImage { @@ -146,13 +146,14 @@ Rectangle { function open() { visible = true - PanelService.willOpenPanel(trayPanel) } function close() { visible = false - trayMenu.item.hideMenu() + if (trayMenu.item) { + trayMenu.item.hideMenu() + } } // Clicking outside of the rectangle to close diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 757549c..fdbe0d2 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -185,7 +185,6 @@ Variants { || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` color: Color.mSecondary font.pointSize: Style.fontSizeXS * scaling - } Rectangle { diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index f6c1b40..e16cb12 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -108,4 +108,4 @@ Rectangle { } } } -} \ No newline at end of file +} From 78cb7d4c1505a84092127d4a5555b9984a86ab61 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:49:59 -0400 Subject: [PATCH 30/61] MediaMini: fix visualizer not showing when track length is unknown (twitch) --- Modules/Bar/Widgets/MediaMini.qml | 283 +++++++++++++++++------------- 1 file changed, 159 insertions(+), 124 deletions(-) diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 6ce7a59..2483dbc 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -18,12 +18,13 @@ RowLayout { Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling visible: MediaService.currentPlayer !== null && MediaService.canPlay - width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 + Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 function getTitle() { return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") } + // A hidden text element to safely measure the full title width NText { id: fullTitleMetrics visible: false @@ -33,133 +34,167 @@ RowLayout { Rectangle { id: mediaMini - width: contentLayout.implicitWidth + Style.marginS * 2 * scaling - height: Math.round(Style.capsuleHeight * scaling) - radius: Math.round(Style.radiusM * scaling) - color: Color.mSurfaceVariant + + Layout.preferredWidth: rowLayout.implicitWidth + Style.marginM * 2 * scaling + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) Layout.alignment: Qt.AlignVCenter - // --- Visualizer Loaders --- - Loader { - anchors.centerIn: parent - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" - && MediaService.isPlaying && MediaService.trackLength > 0 - z: 0 - sourceComponent: LinearSpectrum { - width: mediaMini.width - Style.marginS * scaling - height: 20 * scaling - values: CavaService.values - fillColor: Color.mOnSurfaceVariant - opacity: 0.4 - } + radius: Math.round(Style.radiusM * scaling) + color: Color.mSurfaceVariant + + // Used to anchor the tooltip, so the tooltip does not move when the content expands + Item { + id: anchor + height: parent.height + width: 200 * scaling } - Loader { - anchors.centerIn: parent - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" - && MediaService.isPlaying && MediaService.trackLength > 0 - z: 0 - sourceComponent: MirroredSpectrum { - width: mediaMini.width - Style.marginS * scaling - height: mediaMini.height - Style.marginS * scaling - values: CavaService.values - fillColor: Color.mOnSurfaceVariant - opacity: 0.4 - } - } - - Loader { - anchors.centerIn: parent - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" - && MediaService.isPlaying && MediaService.trackLength > 0 - z: 0 - sourceComponent: WaveSpectrum { - width: mediaMini.width - Style.marginS * scaling - height: mediaMini.height - Style.marginS * scaling - values: CavaService.values - fillColor: Color.mOnSurfaceVariant - opacity: 0.4 - } - } - - // --- Main Content Layout --- - RowLayout { - id: contentLayout - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginS * scaling - spacing: Style.marginS * scaling - z: 1 - - NIcon { - id: windowIcon - text: MediaService.isPlaying ? "pause" : "play_arrow" - font.pointSize: Style.fontSizeL * scaling - Layout.alignment: Qt.AlignVCenter - visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible - } - - NImageCircled { - id: trackArt - imagePath: MediaService.trackArtUrl - fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow" - borderWidth: 0 - border.color: Color.transparent - Layout.preferredWidth: Math.round(18 * scaling) - Layout.preferredHeight: Math.round(18 * scaling) - Layout.alignment: Qt.AlignVCenter - visible: Settings.data.audio.showMiniplayerAlbumArt - } - - NText { - id: titleText - Layout.preferredWidth: { - if (mouseArea.containsMouse) { - return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) - } else { - return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) - } - } - text: getTitle() - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightMedium - elide: Text.ElideRight - color: Color.mTertiary - Layout.alignment: Qt.AlignVCenter - - Behavior on Layout.preferredWidth { - NumberAnimation { - duration: Style.animationSlow - easing.type: Easing.InOutCubic - } - } - } - } - - MouseArea { - id: mouseArea + Item { + id: mainContainer anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - MediaService.playPause() - } else if (mouse.button == Qt.RightButton) { - MediaService.next() - tooltip.visible = false - } else if (mouse.button == Qt.MiddleButton) { - MediaService.previous() - tooltip.visible = false - } - } - onEntered: { - if (tooltip.text !== "") { - tooltip.show() + anchors.leftMargin: Style.marginS * scaling + anchors.rightMargin: Style.marginS * scaling + + Loader { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" + && MediaService.isPlaying + z: 0 + + sourceComponent: LinearSpectrum { + width: mainContainer.width - Style.marginS * scaling + height: 20 * scaling + values: CavaService.values + fillColor: Color.mOnSurfaceVariant + opacity: 0.4 } } - onExited: { - tooltip.hide() + + Loader { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" + && MediaService.isPlaying + z: 0 + + sourceComponent: MirroredSpectrum { + width: mainContainer.width - Style.marginS * scaling + height: mainContainer.height - Style.marginS * scaling + values: CavaService.values + fillColor: Color.mOnSurfaceVariant + opacity: 0.4 + } + } + + Loader { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" + && MediaService.isPlaying + z: 0 + + sourceComponent: WaveSpectrum { + width: mainContainer.width - Style.marginS * scaling + height: mainContainer.height - Style.marginS * scaling + values: CavaService.values + fillColor: Color.mOnSurfaceVariant + opacity: 0.4 + } + } + + RowLayout { + id: rowLayout + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS * scaling + z: 1 // Above the visualizer + + NIcon { + id: windowIcon + text: MediaService.isPlaying ? "pause" : "play_arrow" + font.pointSize: Style.fontSizeL * scaling + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignVCenter + visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible + } + + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + visible: Settings.data.audio.showMiniplayerAlbumArt + spacing: 0 + + Item { + Layout.preferredWidth: Math.round(18 * scaling) + Layout.preferredHeight: Math.round(18 * scaling) + + NImageCircled { + id: trackArt + anchors.fill: parent + imagePath: MediaService.trackArtUrl + fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow" + borderWidth: 0 + border.color: Color.transparent + } + } + } + + NText { + id: titleText + + Layout.preferredWidth: { + if (mouseArea.containsMouse) { + return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) + } else { + return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) + } + } + Layout.alignment: Qt.AlignVCenter + + text: getTitle() + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightMedium + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + color: Color.mTertiary + + Behavior on Layout.preferredWidth { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.InOutCubic + } + } + } + } + + // Mouse area for hover detection + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + MediaService.playPause() + } else if (mouse.button == Qt.RightButton) { + MediaService.next() + // Need to hide the tooltip instantly + tooltip.visible = false + } else if (mouse.button == Qt.MiddleButton) { + MediaService.previous() + // Need to hide the tooltip instantly + tooltip.visible = false + } + } + + onEntered: { + if (tooltip.text !== "") { + tooltip.show() + } + } + onExited: { + tooltip.hide() + } } } } @@ -169,14 +204,14 @@ RowLayout { text: { var str = "" if (MediaService.canGoNext) { - str += "Right click for next\n" + str += "Right click for next.\n" } if (MediaService.canGoPrevious) { - str += "Middle click for previous\n" + str += "Middle click for previous." } return str } - target: mediaMini + target: anchor positionAbove: Settings.data.bar.position === "bottom" } } From ad305b3754deac4f70e0cf7fc51626894398666f Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:53:24 -0400 Subject: [PATCH 31/61] Dock: converted to Layout --- Modules/Dock/Dock.qml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 2fa9e98..9a71bba 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -130,7 +130,7 @@ Variants { Rectangle { id: dockContainer - width: dock.width + 48 * scaling + width: dockLayout.implicitWidth + 48 * scaling height: iconSize * 1.4 * scaling color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) anchors.horizontalCenter: parent.horizontalCenter @@ -178,7 +178,7 @@ Variants { Item { id: dock - width: runningAppsRow.width + width: dockLayout.implicitWidth height: parent.height - (20 * scaling) anchors.centerIn: parent @@ -194,10 +194,10 @@ Variants { return Icons.iconForAppId(toplevel.appId?.toLowerCase()) } - Row { - id: runningAppsRow + RowLayout { + id: dockLayout spacing: Style.marginL * scaling - height: parent.height + Layout.preferredHeight: parent.height anchors.centerIn: parent Repeater { @@ -205,8 +205,10 @@ Variants { delegate: Rectangle { id: appButton - width: iconSize * scaling - height: iconSize * scaling + Layout.preferredWidth: iconSize * scaling + Layout.preferredHeight: iconSize * scaling + Layout.alignment: Qt.AlignCenter + color: Color.transparent radius: Style.radiusM * scaling From 0a48e5f34f8902502121cf6a72a1c4c62c8cec40 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 18:59:53 -0400 Subject: [PATCH 32/61] Clock: text was too big --- Widgets/NClock.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index f1c0a9b..aa8ce33 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -18,6 +18,7 @@ Rectangle { id: textItem text: Time.time anchors.centerIn: parent + font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightBold } From 1efa1f4aa3658b35e71b79b5d5b1ec0db226add2 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 19:06:15 -0400 Subject: [PATCH 33/61] ActiveWindow: Converted to Layout --- Modules/Bar/Widgets/ActiveWindow.qml | 33 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index c7387d1..3a7dd36 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -7,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -Row { +RowLayout { id: root property ShellScreen screen @@ -15,7 +15,7 @@ Row { readonly property real minWidth: 160 readonly property real maxWidth: 400 - anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling visible: getTitle() !== "" @@ -33,7 +33,7 @@ Row { return Icons.iconForAppId(focusedWindow.appId) } - // A hidden text element to safely measure the full title width + // A hidden text element to safely measure the full title width NText { id: fullTitleMetrics visible: false @@ -43,15 +43,13 @@ Row { } Rectangle { - // Let the Rectangle size itself based on its content (the Row) + id: windowTitleRect visible: root.visible - width: row.width + Style.marginM * 2 * scaling - height: Math.round(Style.capsuleHeight * scaling) + Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling + Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant - anchors.verticalCenter: parent.verticalCenter - Item { id: mainContainer anchors.fill: parent @@ -59,16 +57,16 @@ Row { anchors.rightMargin: Style.marginS * scaling clip: true - Row { - id: row - anchors.verticalCenter: parent.verticalCenter + RowLayout { + id: contentLayout + anchors.centerIn: parent spacing: Style.marginS * scaling // Window icon Item { - width: Style.fontSizeL * scaling * 1.2 - height: Style.fontSizeL * scaling * 1.2 - anchors.verticalCenter: parent.verticalCenter + Layout.preferredWidth: Style.fontSizeL * scaling * 1.2 + Layout.preferredHeight: Style.fontSizeL * scaling * 1.2 + Layout.alignment: Qt.AlignVCenter visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon IconImage { @@ -85,24 +83,25 @@ Row { id: titleText // For short titles, show full. For long titles, truncate and expand on hover - width: { + Layout.preferredWidth: { if (mouseArea.containsMouse) { return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) } else { return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling)) } } + Layout.alignment: Qt.AlignVCenter + horizontalAlignment: Text.AlignLeft text: getTitle() font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightMedium elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight - anchors.verticalCenter: parent.verticalCenter verticalAlignment: Text.AlignVCenter color: Color.mSecondary clip: true - Behavior on width { + Behavior on Layout.preferredWidth { NumberAnimation { duration: Style.animationSlow easing.type: Easing.InOutCubic From 8ec1ad7255a6a67002554e2a5c665f1ee96bc2e7 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 19:12:32 -0400 Subject: [PATCH 34/61] TaskBar converted to Layout --- Modules/Bar/Widgets/Taskbar.qml | 16 +++++++++------- Modules/Bar/Widgets/Workspace.qml | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml index 623d7e7..0eef32c 100644 --- a/Modules/Bar/Widgets/Taskbar.qml +++ b/Modules/Bar/Widgets/Taskbar.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior import QtQuick import QtQuick.Controls +import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Wayland @@ -17,15 +18,14 @@ Rectangle { readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling // Always visible when there are toplevels - implicitWidth: taskbarRow.width + Style.marginM * scaling * 2 + implicitWidth: taskbarLayout.implicitWidth + Style.marginM * scaling * 2 implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant - Row { - id: taskbarRow - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter + RowLayout { + id: taskbarLayout + anchors.centerIn: parent spacing: Style.marginXXS * root.scaling Repeater { @@ -35,8 +35,10 @@ Rectangle { required property Toplevel modelData property Toplevel toplevel: modelData property bool isActive: ToplevelManager.activeToplevel === modelData - width: root.itemSize - height: root.itemSize + + Layout.preferredWidth: root.itemSize + Layout.preferredHeight: root.itemSize + Layout.alignment: Qt.AlignCenter Rectangle { id: iconBackground diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 6c53e65..051bdea 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -11,7 +11,7 @@ import qs.Services Item { id: root - property ShellScreen screen: null + property ShellScreen screen property real scaling: 1.0 property bool isDestroying: false From 966b2410d3cec6b9a9efab8ce4d2480445dd04aa Mon Sep 17 00:00:00 2001 From: Lemmy Date: Fri, 5 Sep 2025 19:49:01 -0400 Subject: [PATCH 35/61] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 60414c3..dcdcd20 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,7 @@ Alternatively, you can add it to your NixOS configuration or flake: | Toggle Settings Window | `qs -c noctalia-shell ipc call settings toggle` | | Toggle Lock Screen | `qs -c noctalia-shell ipc call lockScreen toggle` | | Toggle Notification History | `qs -c noctalia-shell ipc call notifications toggleHistory` | +| Toggle Notification DND | `qs -c noctalia-shell ipc call notifications toggleDND` | | Change Wallpaper | `qs -c noctalia-shell ipc call wallpaper set $path $monitor` | | Assign a Random Wallpaper | `qs -c noctalia-shell ipc call wallpaper random` | | Toggle Dark Mode | `qs -c noctalia-shell ipc call darkMode toggle` | From 05bfb6fc37213ee8faaa3d25b6c637123a097140 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 19:57:22 -0400 Subject: [PATCH 36/61] Do Not Disturb: factorized logic and toast in its proper service. --- Modules/Bar/Widgets/NotificationHistory.qml | 8 +------- Modules/IPC/IPCManager.qml | 4 ---- Modules/Notification/NotificationHistoryPanel.qml | 8 +------- Services/NotificationService.qml | 15 +++++++++++---- 4 files changed, 13 insertions(+), 22 deletions(-) diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index cb11314..274c61f 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -23,11 +23,5 @@ NIconButton { onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this) - onRightClicked: { - Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb - ToastService.showNotice( - Settings.data.notifications.doNotDisturb ? "Do Not Disturb enabled" : "Do Not Disturb disabled", - Settings.data.notifications.doNotDisturb ? "Notifications will be hidden but saved to history" : "Notifications will be shown normally", - "notice", false, 2000) - } + onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb } diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index b1ba1d7..8c541a3 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -40,10 +40,6 @@ Item { } function toggleDND() { Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb - ToastService.showNotice( - Settings.data.notifications.doNotDisturb ? "Do Not Disturb enabled" : "Do Not Disturb disabled", - Settings.data.notifications.doNotDisturb ? "Notifications will be hidden but saved to history" : "Notifications will be shown normally", - "notice", false, 2000) } } diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 0d4bff9..97e0a9a 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -48,13 +48,7 @@ NPanel { icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications_active" tooltipText: Settings.data.notifications.doNotDisturb ? "Do Not Disturb (ON)" : "Do Not Disturb (OFF)" sizeRatio: 0.8 - onClicked: { - Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb - ToastService.showNotice( - Settings.data.notifications.doNotDisturb ? "Do Not Disturb enabled" : "Do Not Disturb disabled", - Settings.data.notifications.doNotDisturb ? "Notifications will be hidden but saved to history" : "Notifications will be shown normally", - "notice", false, 2000) - } + onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb } NIconButton { diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 62d88fb..1607be8 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -28,11 +28,11 @@ Singleton { // Signal when notification is received onNotification: function (notification) { + // Always add notification to history + root.addToHistory(notification) // Check if do-not-disturb is enabled if (Settings.data.notifications && Settings.data.notifications.doNotDisturb) { - // Still add to history but don't show notification - root.addToHistory(notification) return } @@ -46,8 +46,6 @@ Singleton { // Add to our model root.addNotification(notification) - // Also add to history - root.addToHistory(notification) } } @@ -109,6 +107,15 @@ Singleton { } } + Connections { + target: Settings.data.notifications + function onDoNotDisturbChanged() { + const label = Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' enabled" : "'Do Not Disturb' disabled" + const description = Settings.data.notifications.doNotDisturb ? "You'll find these notifications in your history." : "Showing all notifications." + ToastService.showNotice(label, description, "notice", false, 2000) + } + } + // Function to add notification to model function addNotification(notification) { notificationModel.insert(0, { From 0c4046b993a244cbed96c49b61f87c8d2b346b44 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 02:15:51 +0200 Subject: [PATCH 37/61] ColorPicker: UI overhaul --- Modules/Notification/Notification.qml | 1 - Widgets/NColorPicker.qml | 611 +++++++++++++++++++++----- 2 files changed, 497 insertions(+), 115 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 757549c..fdbe0d2 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -185,7 +185,6 @@ Variants { || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}` color: Color.mSecondary font.pointSize: Style.fontSizeXS * scaling - } Rectangle { diff --git a/Widgets/NColorPicker.qml b/Widgets/NColorPicker.qml index 8dd348e..112932f 100644 --- a/Widgets/NColorPicker.qml +++ b/Widgets/NColorPicker.qml @@ -9,51 +9,98 @@ Rectangle { property color selectedColor: "#000000" property bool expanded: false + property real scaling: 1.0 + property real currentHue: 0 + property real currentSaturation: 0 signal colorSelected(color color) signal colorCancelled - implicitWidth: expanded ? 320 * scaling : 150 * scaling - implicitHeight: expanded ? 300 * scaling : 40 * scaling + implicitWidth: 150 * scaling + implicitHeight: 40 * scaling - radius: Style.radiusM * scaling + radius: Style.radiusM color: Color.mSurface border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) + border.width: Math.max(1, Style.borderS) - property var presetColors: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000", "#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E"] - - Behavior on implicitWidth { - NumberAnimation { - duration: Style.animationFast + function rgbToHsv(r, g, b) { + r /= 255 + g /= 255 + b /= 255 + var max = Math.max(r, g, b), min = Math.min(r, g, b) + var h, s, v = max + var d = max - min + s = max === 0 ? 0 : d / max + if (max === min) { + h = 0 + } else { + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0) + break + case g: + h = (b - r) / d + 2 + break + case b: + h = (r - g) / d + 4 + break + } + h /= 6 } + return [h * 360, s * 100, v * 100] } - Behavior on implicitHeight { - NumberAnimation { - duration: Style.animationFast + function hsvToRgb(h, s, v) { + h /= 360 + s /= 100 + v /= 100 + var r, g, b + var i = Math.floor(h * 6) + var f = h * 6 - i + var p = v * (1 - s) + var q = v * (1 - f * s) + var t = v * (1 - (1 - f) * s) + switch (i % 6) { + case 0: + r = vg = tb = p + break + case 1: + r = qg = vb = p + break + case 2: + r = pg = vb = t + break + case 3: + r = pg = qb = v + break + case 4: + r = tg = pb = v + break + case 5: + r = vg = pb = q + break } + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)] } - // Collapsed view - just show current color MouseArea { - visible: !root.expanded anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: root.expanded = true + onClicked: colorPickerPopup.open() RowLayout { anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginS * scaling + anchors.margins: Style.marginS + spacing: Style.marginS Rectangle { - Layout.preferredWidth: 24 * scaling - Layout.preferredHeight: 24 * scaling + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 radius: Layout.preferredWidth * 0.5 color: root.selectedColor border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) + border.width: Math.max(1, Style.borderS) } NText { @@ -69,118 +116,454 @@ Rectangle { } } - // Expanded view - color selection - ColumnLayout { - visible: root.expanded - anchors.fill: parent - anchors.margins: Style.marginM * scaling - spacing: Style.marginS * scaling + Popup { + id: colorPickerPopup - // Header - RowLayout { - Layout.fillWidth: true + width: 580 * scaling + height: 750 * scaling + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + modal: true + clip: true - NText { - text: "Select Color" - font.weight: Style.fontWeightBold - Layout.fillWidth: true - } - - NIconButton { - icon: "close" - onClicked: root.expanded = false - } + background: Rectangle { + color: Color.mSurface + radius: 12 * scaling + border.color: Color.mPrimary + border.width: 2 * scaling } - // Preset colors grid - Grid { - columns: 9 - spacing: Style.marginXS * scaling - Layout.fillWidth: true + ScrollView { + id: scrollView + anchors.fill: parent + anchors.margins: 24 * scaling - Repeater { - model: root.presetColors + ScrollBar.vertical.policy: ScrollBar.AlwaysOff + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + clip: true + ColumnLayout { + width: scrollView.availableWidth + spacing: 20 * scaling + + // Header + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 10 * scaling + + RowLayout { + spacing: 8 * scaling + + NIcon { + text: "palette" + font.pointSize: 20 * scaling + color: Color.mPrimary + } + + NText { + text: "Color Picker" + font.pointSize: 20 * scaling + font.weight: Font.Bold + color: Color.mPrimary + } + } + + Item { + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + onClicked: colorPickerPopup.close() + } + } + + // Color preview section Rectangle { - width: Math.round(29 * scaling) - height: width - radius: Style.radiusXS * scaling - color: modelData - border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline - border.width: root.selectedColor === modelData ? 2 : 1 + Layout.fillWidth: true + Layout.preferredHeight: 100 * scaling + radius: 8 * scaling + color: root.selectedColor + border.color: Color.mOutline + border.width: 1 * scaling - MouseArea { + ColumnLayout { + anchors.centerIn: parent + spacing: 5 * scaling + + NText { + text: root.selectedColor.toString().toUpperCase() + font.family: Settings.data.ui.fontFixed + font.pointSize: 18 * scaling + font.weight: Font.Bold + color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF" + horizontalAlignment: Text.AlignHCenter + } + + NText { + text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round( + root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")" + font.family: Settings.data.ui.fontFixed + font.pointSize: 12 * scaling + color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF" + horizontalAlignment: Text.AlignHCenter + } + } + } + + NDivider { + Layout.fillWidth: true + } + + // Hex input + ColumnLayout { + Layout.fillWidth: true + spacing: 8 * scaling + + NLabel { + label: "Hex Color" + description: "Enter a hexadecimal color code" + Layout.fillWidth: true + } + + NTextInput { + text: root.selectedColor.toString().toUpperCase() + fontFamily: Settings.data.ui.fontFixed + Layout.fillWidth: true + onEditingFinished: { + if (/^#[0-9A-F]{6}$/i.test(text)) { + root.selectedColor = text + } + } + } + } + + NDivider { + Layout.fillWidth: true + } + + // RGB sliders section + NBox { + Layout.fillWidth: true + Layout.preferredHeight: 240 * scaling + + ColumnLayout { anchors.fill: parent - cursorShape: Qt.PointingHandCursor + anchors.margins: 15 * scaling + spacing: 10 * scaling + + NLabel { + label: "RGB Values" + description: "Adjust red, green, blue, and brightness values" + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: 15 * scaling + + NText { + text: "R" + font.weight: Font.Bold + Layout.preferredWidth: 20 * scaling + } + + NSlider { + id: redSlider + Layout.fillWidth: true + from: 0 + to: 255 + value: Math.round(root.selectedColor.r * 255) + onMoved: { + root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1) + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + + NText { + text: Math.round(redSlider.value) + font.family: Settings.data.ui.fontFixed + Layout.preferredWidth: 30 * scaling + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 15 * scaling + + NText { + text: "G" + font.weight: Font.Bold + Layout.preferredWidth: 20 * scaling + } + + NSlider { + id: greenSlider + Layout.fillWidth: true + from: 0 + to: 255 + value: Math.round(root.selectedColor.g * 255) + onMoved: { + root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1) + // Update stored hue and saturation when RGB changes + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + + NText { + text: Math.round(greenSlider.value) + font.family: Settings.data.ui.fontFixed + Layout.preferredWidth: 30 * scaling + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 15 * scaling + + NText { + text: "B" + font.weight: Font.Bold + Layout.preferredWidth: 20 * scaling + } + + NSlider { + id: blueSlider + Layout.fillWidth: true + from: 0 + to: 255 + value: Math.round(root.selectedColor.b * 255) + onMoved: { + root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1) + // Update stored hue and saturation when RGB changes + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + + NText { + text: Math.round(blueSlider.value) + font.family: Settings.data.ui.fontFixed + Layout.preferredWidth: 30 * scaling + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 15 * scaling + + NText { + text: "Brightness" + font.weight: Font.Bold + Layout.preferredWidth: 80 * scaling + } + + NSlider { + id: brightnessSlider + Layout.fillWidth: true + from: 0 + to: 100 + value: { + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + return hsv[2] + } + onMoved: { + var hue = root.currentHue + var saturation = root.currentSaturation + + if (hue === 0 && saturation === 0) { + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + hue = hsv[0] + saturation = hsv[1] + root.currentHue = hue + root.currentSaturation = saturation + } + + var rgb = root.hsvToRgb(hue, saturation, value) + root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1) + } + } + + NText { + text: Math.round(brightnessSlider.value) + "%" + font.family: Settings.data.ui.fontFixed + Layout.preferredWidth: 40 * scaling + } + } + } + } + + NDivider { + Layout.fillWidth: true + } + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: 120 * scaling + + ColumnLayout { + anchors.fill: parent + anchors.margins: 15 * scaling + spacing: 10 * scaling + + NLabel { + label: "Theme Colors" + description: "Quick access to your theme's color palette" + Layout.fillWidth: true + } + + Flow { + spacing: 6 * scaling + Layout.fillWidth: true + flow: Flow.LeftToRight + + Repeater { + model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000"] + + Rectangle { + width: 24 * scaling + height: 24 * scaling + radius: 4 * scaling + color: modelData + border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline + border.width: root.selectedColor === modelData ? 2 * scaling : 1 * scaling + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.selectedColor = modelData + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + } + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: 170 * scaling + + ColumnLayout { + anchors.fill: parent + anchors.margins: 15 * scaling + spacing: 10 * scaling + + NLabel { + label: "Color Palettes" + description: "Choose from a wide range of predefined colors" + Layout.fillWidth: true + } + + Flow { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 6 * scaling + flow: Flow.LeftToRight + + Repeater { + model: ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E", "#E74C3C", "#C0392B", "#E67E22", "#D35400", "#F39C12", "#F1C40F", "#2ECC71", "#27AE60", "#1ABC9C", "#16A085", "#3498DB", "#2980B9", "#9B59B6", "#8E44AD", "#34495E", "#2C3E50", "#95A5A6", "#7F8C8D", "#FFFFFF", "#000000"] + + Rectangle { + width: 24 * scaling + height: 24 * scaling + radius: 4 * scaling + color: modelData + border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline + border.width: root.selectedColor === modelData ? 2 * scaling : 1 * scaling + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.selectedColor = modelData + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 20 * scaling + Layout.bottomMargin: 20 * scaling + spacing: 10 * scaling + + Item { + Layout.fillWidth: true + } + + NButton { + text: "Cancel" + icon: "close" + outlined: true + customHeight: 36 * scaling + customWidth: 100 * scaling onClicked: { - root.selectedColor = modelData - // root.colorSelected(modelData) + root.colorCancelled() + colorPickerPopup.close() + } + } + + NButton { + text: "Apply" + icon: "check" + customHeight: 36 * scaling + customWidth: 100 * scaling + onClicked: { + root.colorSelected(root.selectedColor) + colorPickerPopup.close() } } } } } + } - // Custom color input - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS * scaling - - NTextInput { - id: hexInput - label: "Hex Color" - text: root.selectedColor.toString().toUpperCase() - fontFamily: Settings.data.ui.fontFixed - Layout.minimumWidth: 100 * scaling - onEditingFinished: { - if (/^#[0-9A-F]{6}$/i.test(text)) { - root.selectedColor = text - root.colorSelected(text) - } - } - } - - Rectangle { - Layout.preferredWidth: 32 * scaling - Layout.preferredHeight: 32 * scaling - radius: Layout.preferredWidth * 0.5 - color: root.selectedColor - border.color: Color.mOutline - border.width: 1 - Layout.alignment: Qt.AlignBottom - Layout.bottomMargin: 5 * scaling - } + NSlider { + id: hueSlider + visible: false + from: 0 + to: 360 + value: { + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) + return hsv[0] } + } - // Action buttons row - RowLayout { - Layout.fillWidth: true - spacing: Style.marginS * scaling - - Item { - Layout.fillWidth: true - } // Spacer - - NButton { - text: "Cancel" - outlined: true - customHeight: Style.baseWidgetSize * scaling - fontSize: Style.fontSizeS * scaling - onClicked: { - root.colorCancelled() - root.expanded = false - } - } - - NButton { - text: "Apply" - customHeight: Style.baseWidgetSize * scaling - fontSize: Style.fontSizeS * scaling - onClicked: { - root.colorSelected(root.selectedColor) - root.expanded = false - } - } + NSlider { + id: saturationSlider + visible: false + from: 0 + to: 100 + value: { + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) + return hsv[1] } } } From 39c7089cbc954357deaec94f923d4c811c81dbb9 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 21:04:02 -0400 Subject: [PATCH 38/61] Notification: fixed persistent DND toast. --- Modules/Bar/Widgets/NotificationHistory.qml | 2 +- Modules/Notification/NotificationHistoryPanel.qml | 2 +- Services/NotificationService.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 274c61f..48a62fd 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -15,7 +15,7 @@ NIconButton { sizeRatio: 0.8 icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications" - tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history (Do Not Disturb ON)\nRight-click to toggle Do Not Disturb" : "Notification history\nRight-click to toggle Do Not Disturb" + tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'." colorBg: Color.mSurfaceVariant colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface colorBorder: Color.transparent diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 97e0a9a..46a0ccb 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -46,7 +46,7 @@ NPanel { NIconButton { icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications_active" - tooltipText: Settings.data.notifications.doNotDisturb ? "Do Not Disturb (ON)" : "Do Not Disturb (OFF)" + tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled." sizeRatio: 0.8 onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 1607be8..4ce9747 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -112,7 +112,7 @@ Singleton { function onDoNotDisturbChanged() { const label = Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' enabled" : "'Do Not Disturb' disabled" const description = Settings.data.notifications.doNotDisturb ? "You'll find these notifications in your history." : "Showing all notifications." - ToastService.showNotice(label, description, "notice", false, 2000) + ToastService.showNotice(label, description) } } From 8426e36f46c9f77bf7b5fe8112e3f07afa8b471f Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 21:08:30 -0400 Subject: [PATCH 39/61] Time: improved human readable time + fixed a few tooltips. --- Commons/Time.qml | 34 +++++++++++++++---------- Modules/Bar/Widgets/NightLight.qml | 2 +- Modules/Bar/Widgets/SidePanelToggle.qml | 2 +- Modules/SidePanel/Cards/ProfileCard.qml | 20 +++------------ 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/Commons/Time.qml b/Commons/Time.qml index fe1f3fd..fddb4db 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -78,23 +78,31 @@ Singleton { } // Format an easy to read approximate duration ex: 4h32m -// Used to display the time remaining on the Battery widget +// Used to display the time remaining on the Battery widget, computer uptime, etc.. function formatVagueHumanReadableDuration(totalSeconds) { - const hours = Math.floor(totalSeconds / 3600) - const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60) - const seconds = totalSeconds - (hours * 3600) - (minutes * 60) - - var str = "" - if (hours) { - str += hours.toString() + "h" - } - if (minutes) { - str += minutes.toString() + "m" + if (typeof totalSeconds !== 'number' || totalSeconds < 0) { + return '0s'; } + + // Floor the input to handle decimal seconds + totalSeconds = Math.floor(totalSeconds); + + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + const parts = []; + if (days) parts.push(`${days}d`); + if (hours) parts.push(`${hours}h`); + if (minutes) parts.push(`${minutes}m`); + + // Only show seconds if no hours and no minutes if (!hours && !minutes) { - str += seconds.toString() + "s" + parts.push(`${seconds}s`); } - return str + + return parts.join(''); } Timer { diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 342a424..8021e74 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -21,7 +21,7 @@ NIconButton { colorBorderHover: Color.transparent icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off" - tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled" : "disabled"}\nLeft click to toggle.\nRight click to access settings.` + tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled." : "disabled"}\nLeft click to toggle.\nRight click to access settings.` onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled onRightClicked: { diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index 1b0b4ce..b9572fb 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -12,7 +12,7 @@ NIconButton { property real scaling: 1.0 icon: Settings.data.bar.useDistroLogo ? "" : "widgets" - tooltipText: "Open side panel" + tooltipText: "Open side panel." sizeRatio: 0.8 colorBg: Color.mSurfaceVariant diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index 951beba..4c2d1ce 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -59,7 +59,7 @@ NBox { } NIconButton { icon: "settings" - tooltipText: "Open settings" + tooltipText: "Open settings." onClicked: { settingsPanel.requestedTab = SettingsPanel.Tab.General settingsPanel.open(screen) @@ -69,7 +69,7 @@ NBox { NIconButton { id: powerButton icon: "power_settings_new" - tooltipText: "Power menu" + tooltipText: "Power menu." onClicked: { powerPanel.open(screen) sidePanel.close() @@ -79,7 +79,7 @@ NBox { NIconButton { id: closeButton icon: "close" - tooltipText: "Close side panel" + tooltipText: "Close side panel." onClicked: { sidePanel.close() } @@ -104,19 +104,7 @@ NBox { stdout: StdioCollector { onStreamFinished: { var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]) - var minutes = Math.floor(uptimeSeconds / 60) % 60 - var hours = Math.floor(uptimeSeconds / 3600) % 24 - var days = Math.floor(uptimeSeconds / 86400) - - // Format the output - if (days > 0) { - uptimeText = days + "d " + hours + "h" - } else if (hours > 0) { - uptimeText = hours + "h" + minutes + "m" - } else { - uptimeText = minutes + "m" - } - + uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds) uptimeProcess.running = false } } From 55b74ad38f4bd3026787b4b95853dddb33987cf6 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Sat, 6 Sep 2025 09:46:42 +0800 Subject: [PATCH 40/61] fix: align KeyboardLayout widget with other bar components --- Modules/Bar/Widgets/KeyboardLayout.qml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index e304b7e..b9b44a5 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -7,7 +7,7 @@ import qs.Commons import qs.Services import qs.Widgets -RowLayout { +Item { id: root property ShellScreen screen @@ -19,17 +19,13 @@ RowLayout { // Use the shared service for keyboard layout property string currentLayout: KeyboardLayoutService.currentLayout - Layout.preferredWidth: pill.implicitWidth - Layout.preferredHeight: pill.implicitHeight - spacing: 0 + implicitWidth: pill.width + implicitHeight: pill.height NPill { id: pill - Layout.alignment: Qt.AlignCenter - Layout.fillWidth: false - Layout.fillHeight: false - + anchors.verticalCenter: parent.verticalCenter rightOpen: BarWidgetRegistry.getNPillDirection(root) icon: "keyboard_alt" iconCircleColor: Color.mPrimary From 4f871296ae995f8182689049d9bc6585c06a7caf Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 22:17:58 -0400 Subject: [PATCH 41/61] ColorPicker: splitted in two NColorPicker + NColorPickerDialog + fixed layout and a few little bugs --- Modules/SettingsPanel/Tabs/WallpaperTab.qml | 1 - Widgets/NColorPicker.qml | 545 +------------------- Widgets/NColorPickerDialog.qml | 516 ++++++++++++++++++ 3 files changed, 537 insertions(+), 525 deletions(-) create mode 100644 Widgets/NColorPickerDialog.qml diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 3115f03..0d40d33 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -115,7 +115,6 @@ ColumnLayout { NColorPicker { selectedColor: Settings.data.wallpaper.fillColor onColorSelected: color => Settings.data.wallpaper.fillColor = color - onColorCancelled: selectedColor = Settings.data.wallpaper.fillColor } } diff --git a/Widgets/NColorPicker.qml b/Widgets/NColorPicker.qml index 112932f..830ba84 100644 --- a/Widgets/NColorPicker.qml +++ b/Widgets/NColorPicker.qml @@ -8,99 +8,47 @@ Rectangle { id: root property color selectedColor: "#000000" - property bool expanded: false - property real scaling: 1.0 - property real currentHue: 0 - property real currentSaturation: 0 signal colorSelected(color color) - signal colorCancelled implicitWidth: 150 * scaling implicitHeight: 40 * scaling - radius: Style.radiusM + radius: Style.radiusM * scaling color: Color.mSurface border.color: Color.mOutline - border.width: Math.max(1, Style.borderS) - - function rgbToHsv(r, g, b) { - r /= 255 - g /= 255 - b /= 255 - var max = Math.max(r, g, b), min = Math.min(r, g, b) - var h, s, v = max - var d = max - min - s = max === 0 ? 0 : d / max - if (max === min) { - h = 0 - } else { - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0) - break - case g: - h = (b - r) / d + 2 - break - case b: - h = (r - g) / d + 4 - break - } - h /= 6 - } - return [h * 360, s * 100, v * 100] - } - - function hsvToRgb(h, s, v) { - h /= 360 - s /= 100 - v /= 100 - var r, g, b - var i = Math.floor(h * 6) - var f = h * 6 - i - var p = v * (1 - s) - var q = v * (1 - f * s) - var t = v * (1 - (1 - f) * s) - switch (i % 6) { - case 0: - r = vg = tb = p - break - case 1: - r = qg = vb = p - break - case 2: - r = pg = vb = t - break - case 3: - r = pg = qb = v - break - case 4: - r = tg = pb = v - break - case 5: - r = vg = pb = q - break - } - return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)] - } + border.width: Math.max(1, Style.borderS * scaling) + // Minimized Look MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: colorPickerPopup.open() + onClicked: { + var dialog = Qt.createComponent("NColorPickerDialog.qml").createObject(root, { + "selectedColor": selectedColor, + "parent": Overlay.overlay + }) + // Connect the dialog's signal to the picker's signal + dialog.colorSelected.connect(function (color) { + root.selectedColor = color + root.colorSelected(color) + }) + + dialog.open() + } RowLayout { anchors.fill: parent - anchors.margins: Style.marginS - spacing: Style.marginS + anchors.margins: Style.marginS * scaling + spacing: Style.marginS * scaling Rectangle { - Layout.preferredWidth: 24 - Layout.preferredHeight: 24 + Layout.preferredWidth: 24 * scaling + Layout.preferredHeight: 24 * scaling radius: Layout.preferredWidth * 0.5 color: root.selectedColor border.color: Color.mOutline - border.width: Math.max(1, Style.borderS) + border.width: Math.max(1, Style.borderS * scaling) } NText { @@ -115,455 +63,4 @@ Rectangle { } } } - - Popup { - id: colorPickerPopup - - width: 580 * scaling - height: 750 * scaling - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - modal: true - clip: true - - background: Rectangle { - color: Color.mSurface - radius: 12 * scaling - border.color: Color.mPrimary - border.width: 2 * scaling - } - - ScrollView { - id: scrollView - anchors.fill: parent - anchors.margins: 24 * scaling - - ScrollBar.vertical.policy: ScrollBar.AlwaysOff - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - clip: true - - ColumnLayout { - width: scrollView.availableWidth - spacing: 20 * scaling - - // Header - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 10 * scaling - - RowLayout { - spacing: 8 * scaling - - NIcon { - text: "palette" - font.pointSize: 20 * scaling - color: Color.mPrimary - } - - NText { - text: "Color Picker" - font.pointSize: 20 * scaling - font.weight: Font.Bold - color: Color.mPrimary - } - } - - Item { - Layout.fillWidth: true - } - - NIconButton { - icon: "close" - onClicked: colorPickerPopup.close() - } - } - - // Color preview section - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 100 * scaling - radius: 8 * scaling - color: root.selectedColor - border.color: Color.mOutline - border.width: 1 * scaling - - ColumnLayout { - anchors.centerIn: parent - spacing: 5 * scaling - - NText { - text: root.selectedColor.toString().toUpperCase() - font.family: Settings.data.ui.fontFixed - font.pointSize: 18 * scaling - font.weight: Font.Bold - color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF" - horizontalAlignment: Text.AlignHCenter - } - - NText { - text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round( - root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")" - font.family: Settings.data.ui.fontFixed - font.pointSize: 12 * scaling - color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF" - horizontalAlignment: Text.AlignHCenter - } - } - } - - NDivider { - Layout.fillWidth: true - } - - // Hex input - ColumnLayout { - Layout.fillWidth: true - spacing: 8 * scaling - - NLabel { - label: "Hex Color" - description: "Enter a hexadecimal color code" - Layout.fillWidth: true - } - - NTextInput { - text: root.selectedColor.toString().toUpperCase() - fontFamily: Settings.data.ui.fontFixed - Layout.fillWidth: true - onEditingFinished: { - if (/^#[0-9A-F]{6}$/i.test(text)) { - root.selectedColor = text - } - } - } - } - - NDivider { - Layout.fillWidth: true - } - - // RGB sliders section - NBox { - Layout.fillWidth: true - Layout.preferredHeight: 240 * scaling - - ColumnLayout { - anchors.fill: parent - anchors.margins: 15 * scaling - spacing: 10 * scaling - - NLabel { - label: "RGB Values" - description: "Adjust red, green, blue, and brightness values" - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - spacing: 15 * scaling - - NText { - text: "R" - font.weight: Font.Bold - Layout.preferredWidth: 20 * scaling - } - - NSlider { - id: redSlider - Layout.fillWidth: true - from: 0 - to: 255 - value: Math.round(root.selectedColor.r * 255) - onMoved: { - root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1) - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - - NText { - text: Math.round(redSlider.value) - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 30 * scaling - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 15 * scaling - - NText { - text: "G" - font.weight: Font.Bold - Layout.preferredWidth: 20 * scaling - } - - NSlider { - id: greenSlider - Layout.fillWidth: true - from: 0 - to: 255 - value: Math.round(root.selectedColor.g * 255) - onMoved: { - root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1) - // Update stored hue and saturation when RGB changes - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - - NText { - text: Math.round(greenSlider.value) - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 30 * scaling - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 15 * scaling - - NText { - text: "B" - font.weight: Font.Bold - Layout.preferredWidth: 20 * scaling - } - - NSlider { - id: blueSlider - Layout.fillWidth: true - from: 0 - to: 255 - value: Math.round(root.selectedColor.b * 255) - onMoved: { - root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1) - // Update stored hue and saturation when RGB changes - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - - NText { - text: Math.round(blueSlider.value) - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 30 * scaling - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 15 * scaling - - NText { - text: "Brightness" - font.weight: Font.Bold - Layout.preferredWidth: 80 * scaling - } - - NSlider { - id: brightnessSlider - Layout.fillWidth: true - from: 0 - to: 100 - value: { - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - return hsv[2] - } - onMoved: { - var hue = root.currentHue - var saturation = root.currentSaturation - - if (hue === 0 && saturation === 0) { - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - hue = hsv[0] - saturation = hsv[1] - root.currentHue = hue - root.currentSaturation = saturation - } - - var rgb = root.hsvToRgb(hue, saturation, value) - root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1) - } - } - - NText { - text: Math.round(brightnessSlider.value) + "%" - font.family: Settings.data.ui.fontFixed - Layout.preferredWidth: 40 * scaling - } - } - } - } - - NDivider { - Layout.fillWidth: true - } - - NBox { - Layout.fillWidth: true - Layout.preferredHeight: 120 * scaling - - ColumnLayout { - anchors.fill: parent - anchors.margins: 15 * scaling - spacing: 10 * scaling - - NLabel { - label: "Theme Colors" - description: "Quick access to your theme's color palette" - Layout.fillWidth: true - } - - Flow { - spacing: 6 * scaling - Layout.fillWidth: true - flow: Flow.LeftToRight - - Repeater { - model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000"] - - Rectangle { - width: 24 * scaling - height: 24 * scaling - radius: 4 * scaling - color: modelData - border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline - border.width: root.selectedColor === modelData ? 2 * scaling : 1 * scaling - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - root.selectedColor = modelData - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - } - } - } - } - } - - NDivider { - Layout.fillWidth: true - } - - NBox { - Layout.fillWidth: true - Layout.preferredHeight: 170 * scaling - - ColumnLayout { - anchors.fill: parent - anchors.margins: 15 * scaling - spacing: 10 * scaling - - NLabel { - label: "Color Palettes" - description: "Choose from a wide range of predefined colors" - Layout.fillWidth: true - } - - Flow { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 6 * scaling - flow: Flow.LeftToRight - - Repeater { - model: ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E", "#E74C3C", "#C0392B", "#E67E22", "#D35400", "#F39C12", "#F1C40F", "#2ECC71", "#27AE60", "#1ABC9C", "#16A085", "#3498DB", "#2980B9", "#9B59B6", "#8E44AD", "#34495E", "#2C3E50", "#95A5A6", "#7F8C8D", "#FFFFFF", "#000000"] - - Rectangle { - width: 24 * scaling - height: 24 * scaling - radius: 4 * scaling - color: modelData - border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline - border.width: root.selectedColor === modelData ? 2 * scaling : 1 * scaling - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - root.selectedColor = modelData - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, - root.selectedColor.b * 255) - root.currentHue = hsv[0] - root.currentSaturation = hsv[1] - } - } - } - } - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.topMargin: 20 * scaling - Layout.bottomMargin: 20 * scaling - spacing: 10 * scaling - - Item { - Layout.fillWidth: true - } - - NButton { - text: "Cancel" - icon: "close" - outlined: true - customHeight: 36 * scaling - customWidth: 100 * scaling - onClicked: { - root.colorCancelled() - colorPickerPopup.close() - } - } - - NButton { - text: "Apply" - icon: "check" - customHeight: 36 * scaling - customWidth: 100 * scaling - onClicked: { - root.colorSelected(root.selectedColor) - colorPickerPopup.close() - } - } - } - } - } - } - - NSlider { - id: hueSlider - visible: false - from: 0 - to: 360 - value: { - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) - return hsv[0] - } - } - - NSlider { - id: saturationSlider - visible: false - from: 0 - to: 100 - value: { - var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, root.selectedColor.b * 255) - return hsv[1] - } - } } diff --git a/Widgets/NColorPickerDialog.qml b/Widgets/NColorPickerDialog.qml new file mode 100644 index 0000000..324e5b6 --- /dev/null +++ b/Widgets/NColorPickerDialog.qml @@ -0,0 +1,516 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +Popup { + id: root + + property color selectedColor: "#000000" + property real currentHue: 0 + property real currentSaturation: 0 + + signal colorSelected(color color) + + width: 580 * scaling + height: { + const h = scrollView.implicitHeight + padding * 2 + Math.min(h, screen?.height - Style.barHeight * scaling - Style.marginL * 2) + } + padding: Style.marginXL * scaling + + // Center popup in parent + x: (parent.width - width) * 0.5 + y: (parent.height - height) * 0.5 + + modal: true + clip: true + + function rgbToHsv(r, g, b) { + r /= 255 + g /= 255 + b /= 255 + var max = Math.max(r, g, b), min = Math.min(r, g, b) + var h, s, v = max + var d = max - min + s = max === 0 ? 0 : d / max + if (max === min) { + h = 0 + } else { + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0) + break + case g: + h = (b - r) / d + 2 + break + case b: + h = (r - g) / d + 4 + break + } + h /= 6 + } + return [h * 360, s * 100, v * 100] + } + + function hsvToRgb(h, s, v) { + h /= 360 + s /= 100 + v /= 100 + + var r, g, b + var i = Math.floor(h * 6) + var f = h * 6 - i + var p = v * (1 - s) + var q = v * (1 - f * s) + var t = v * (1 - (1 - f) * s) + + switch (i % 6) { + case 0: + r = v + g = t + b = p + break + case 1: + r = q + g = v + b = p + break + case 2: + r = p + g = v + b = t + break + case 3: + r = p + g = q + b = v + break + case 4: + r = t + g = p + b = v + break + case 5: + r = v + g = p + b = q + break + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)] + } + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusS * scaling + border.color: Color.mPrimary + border.width: Math.max(1, Style.borderM * scaling) + } + + ScrollView { + id: scrollView + anchors.fill: parent + + ScrollBar.vertical.policy: ScrollBar.AlwaysOff + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + clip: true + + ColumnLayout { + width: scrollView.availableWidth + spacing: Style.marginL * scaling + + // Header + RowLayout { + Layout.fillWidth: true + + RowLayout { + spacing: Style.marginS * scaling + + NIcon { + text: "palette" + font.pointSize: Style.fontSizeXXL * scaling + color: Color.mPrimary + } + + NText { + text: "Color Picker" + font.pointSize: Style.fontSizeXL * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + } + } + + Item { + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + onClicked: root.close() + } + } + + // Color preview section + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 80 * scaling + radius: Style.radiusS * scaling + color: root.selectedColor + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + ColumnLayout { + spacing: 0 + anchors.fill: parent + + Item { + Layout.fillHeight: true + } + + NText { + text: root.selectedColor.toString().toUpperCase() + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeL * scaling + font.weight: Font.Bold + color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF" + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round( + root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")" + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "#000000" : "#FFFFFF" + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + // Hex input + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NLabel { + label: "Hex Color" + description: "Enter a hexadecimal color code" + Layout.fillWidth: true + } + + NTextInput { + text: root.selectedColor.toString().toUpperCase() + fontFamily: Settings.data.ui.fontFixed + Layout.fillWidth: true + onEditingFinished: { + if (/^#[0-9A-F]{6}$/i.test(text)) { + root.selectedColor = text + } + } + } + } + + // RGB sliders section + NBox { + Layout.fillWidth: true + Layout.preferredHeight: slidersSection.implicitHeight + Style.marginL * scaling * 2 + + ColumnLayout { + id: slidersSection + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling + + NLabel { + label: "RGB Values" + description: "Adjust red, green, blue, and brightness values" + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: "R" + font.weight: Font.Bold + Layout.preferredWidth: 20 * scaling + } + + NSlider { + id: redSlider + Layout.fillWidth: true + from: 0 + to: 255 + value: Math.round(root.selectedColor.r * 255) + onMoved: { + root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1) + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + + NText { + text: Math.round(redSlider.value) + font.family: Settings.data.ui.fontFixed + Layout.preferredWidth: 30 * scaling + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: "G" + font.weight: Font.Bold + Layout.preferredWidth: 20 * scaling + } + + NSlider { + id: greenSlider + Layout.fillWidth: true + from: 0 + to: 255 + value: Math.round(root.selectedColor.g * 255) + onMoved: { + root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1) + // Update stored hue and saturation when RGB changes + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + + NText { + text: Math.round(greenSlider.value) + font.family: Settings.data.ui.fontFixed + Layout.preferredWidth: 30 * scaling + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: "B" + font.weight: Font.Bold + Layout.preferredWidth: 20 * scaling + } + + NSlider { + id: blueSlider + Layout.fillWidth: true + from: 0 + to: 255 + value: Math.round(root.selectedColor.b * 255) + onMoved: { + root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1) + // Update stored hue and saturation when RGB changes + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + + NText { + text: Math.round(blueSlider.value) + font.family: Settings.data.ui.fontFixed + Layout.preferredWidth: 30 * scaling + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: "Brightness" + font.weight: Font.Bold + Layout.preferredWidth: 80 * scaling + } + + NSlider { + id: brightnessSlider + Layout.fillWidth: true + from: 0 + to: 100 + value: { + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + return hsv[2] + } + onMoved: { + var hue = root.currentHue + var saturation = root.currentSaturation + + if (hue === 0 && saturation === 0) { + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + hue = hsv[0] + saturation = hsv[1] + root.currentHue = hue + root.currentSaturation = saturation + } + + var rgb = root.hsvToRgb(hue, saturation, value) + root.selectedColor = Qt.rgba(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1) + } + } + + NText { + text: Math.round(brightnessSlider.value) + "%" + font.family: Settings.data.ui.fontFixed + Layout.preferredWidth: 40 * scaling + } + } + } + } + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: themePalette.implicitHeight + Style.marginL * scaling * 2 + + ColumnLayout { + id: themePalette + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginS * scaling + + NLabel { + label: "Theme Colors" + description: "Quick access to your theme's color palette" + Layout.fillWidth: true + } + + Flow { + spacing: 6 * scaling + Layout.fillWidth: true + flow: Flow.LeftToRight + + Repeater { + model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError, Color.mSurface, Color.mSurfaceVariant, Color.mOutline, "#FFFFFF", "#000000"] + + Rectangle { + width: 24 * scaling + height: 24 * scaling + radius: 4 * scaling + color: modelData + border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline + border.width: root.selectedColor === modelData ? 2 * scaling : 1 * scaling + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.selectedColor = modelData + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + } + } + } + } + } + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: genericPalette.implicitHeight + Style.marginL * scaling * 2 + + ColumnLayout { + id: genericPalette + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginS * scaling + + NLabel { + label: "Colors Palette" + description: "Choose from a wide range of predefined colors" + Layout.fillWidth: true + } + + Flow { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 6 * scaling + flow: Flow.LeftToRight + + Repeater { + model: ["#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#9E9E9E", "#E74C3C", "#E67E22", "#F1C40F", "#2ECC71", "#1ABC9C", "#3498DB", "#2980B9", "#9B59B6", "#34495E", "#2C3E50", "#95A5A6", "#7F8C8D", "#FFFFFF", "#000000"] + + Rectangle { + width: 24 * scaling + height: 24 * scaling + radius: Style.radiusXXS * scaling + color: modelData + border.color: root.selectedColor === modelData ? Color.mPrimary : Color.mOutline + border.width: Math.max( + 1, root.selectedColor === modelData ? Style.borderM * scaling : Style.borderS * scaling) + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.selectedColor = modelData + var hsv = root.rgbToHsv(root.selectedColor.r * 255, root.selectedColor.g * 255, + root.selectedColor.b * 255) + root.currentHue = hsv[0] + root.currentSaturation = hsv[1] + } + } + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 20 * scaling + Layout.bottomMargin: 20 * scaling + spacing: 10 * scaling + + Item { + Layout.fillWidth: true + } + + NButton { + id: cancelButton + text: "Cancel" + icon: "close" + outlined: cancelButton.hovered ? false : true + customHeight: 36 * scaling + customWidth: 100 * scaling + onClicked: { + root.close() + } + } + + NButton { + text: "Apply" + icon: "check" + customHeight: 36 * scaling + customWidth: 100 * scaling + onClicked: { + root.colorSelected(root.selectedColor) + root.close() + } + } + } + } + } +} From 561b55cb9e9601f59227eb2b1d01c149ab7c8810 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 22:18:08 -0400 Subject: [PATCH 42/61] Autoformatting --- Commons/Style.qml | 1 + Commons/Time.qml | 37 ++++++++++++++++++++----------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Commons/Style.qml b/Commons/Style.qml index d2af5a8..902a225 100644 --- a/Commons/Style.qml +++ b/Commons/Style.qml @@ -29,6 +29,7 @@ Singleton { property int fontWeightBold: 700 // Radii + property int radiusXXS: 4 * Settings.data.general.radiusRatio property int radiusXS: 8 * Settings.data.general.radiusRatio property int radiusS: 12 * Settings.data.general.radiusRatio property int radiusM: 16 * Settings.data.general.radiusRatio diff --git a/Commons/Time.qml b/Commons/Time.qml index fddb4db..d7ec78e 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -81,28 +81,31 @@ Singleton { // Used to display the time remaining on the Battery widget, computer uptime, etc.. function formatVagueHumanReadableDuration(totalSeconds) { if (typeof totalSeconds !== 'number' || totalSeconds < 0) { - return '0s'; + return '0s' } - + // Floor the input to handle decimal seconds - totalSeconds = Math.floor(totalSeconds); - - const days = Math.floor(totalSeconds / 86400); - const hours = Math.floor((totalSeconds % 86400) / 3600); - const minutes = Math.floor((totalSeconds % 3600) / 60); - const seconds = totalSeconds % 60; - - const parts = []; - if (days) parts.push(`${days}d`); - if (hours) parts.push(`${hours}h`); - if (minutes) parts.push(`${minutes}m`); - + totalSeconds = Math.floor(totalSeconds) + + const days = Math.floor(totalSeconds / 86400) + const hours = Math.floor((totalSeconds % 86400) / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const seconds = totalSeconds % 60 + + const parts = [] + if (days) + parts.push(`${days}d`) + if (hours) + parts.push(`${hours}h`) + if (minutes) + parts.push(`${minutes}m`) + // Only show seconds if no hours and no minutes if (!hours && !minutes) { - parts.push(`${seconds}s`); + parts.push(`${seconds}s`) } - - return parts.join(''); + + return parts.join('') } Timer { From ae12d77e2950699529170179c72a8e2b82dbc653 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 22:37:54 -0400 Subject: [PATCH 43/61] Tooltips: should end with a coma. --- Modules/Bar/Widgets/Clock.qml | 2 +- Modules/Bar/Widgets/CustomButton.qml | 6 +++--- Modules/Bar/Widgets/NightLight.qml | 2 +- Modules/SidePanel/Cards/PowerProfilesCard.qml | 6 +++--- Modules/SidePanel/Cards/UtilitiesCard.qml | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 9da63f5..ee57b57 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -23,7 +23,7 @@ Rectangle { NTooltip { id: tooltip - text: Time.dateString + text: `${Time.dateString}.` target: clock positionAbove: Settings.data.bar.position === "bottom" } diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index e99537a..de8f96d 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -47,13 +47,13 @@ NIconButton { } else { var lines = [] if (userLeftClickExec !== "") { - lines.push(`Left click: ${userLeftClickExec}`) + lines.push(`Left click: ${userLeftClickExec}.`) } if (userRightClickExec !== "") { - lines.push(`Right click: ${userRightClickExec}`) + lines.push(`Right click: ${userRightClickExec}.`) } if (userMiddleClickExec !== "") { - lines.push(`Middle click: ${userMiddleClickExec}`) + lines.push(`Middle click: ${userMiddleClickExec}.`) } return lines.join("
") } diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 8021e74..6ea2e20 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -21,7 +21,7 @@ NIconButton { colorBorderHover: Color.transparent icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off" - tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled." : "disabled"}\nLeft click to toggle.\nRight click to access settings.` + tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled." : "disabled."}\nLeft click to toggle.\nRight click to access settings.` onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled onRightClicked: { diff --git a/Modules/SidePanel/Cards/PowerProfilesCard.qml b/Modules/SidePanel/Cards/PowerProfilesCard.qml index 2bdd88a..8eb28e8 100644 --- a/Modules/SidePanel/Cards/PowerProfilesCard.qml +++ b/Modules/SidePanel/Cards/PowerProfilesCard.qml @@ -29,7 +29,7 @@ NBox { // Performance NIconButton { icon: "speed" - tooltipText: "Set performance power profile" + tooltipText: "Set performance power profile." enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium colorBg: (enabled && powerProfiles.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant @@ -43,7 +43,7 @@ NBox { // Balanced NIconButton { icon: "balance" - tooltipText: "Set balanced power profile" + tooltipText: "Set balanced power profile." enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium colorBg: (enabled && powerProfiles.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant @@ -57,7 +57,7 @@ NBox { // Eco NIconButton { icon: "eco" - tooltipText: "Set eco power profile" + tooltipText: "Set eco power profile." enabled: hasPP opacity: enabled ? Style.opacityFull : Style.opacityMedium colorBg: (enabled && powerProfiles.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant diff --git a/Modules/SidePanel/Cards/UtilitiesCard.qml b/Modules/SidePanel/Cards/UtilitiesCard.qml index f295224..78fc702 100644 --- a/Modules/SidePanel/Cards/UtilitiesCard.qml +++ b/Modules/SidePanel/Cards/UtilitiesCard.qml @@ -26,7 +26,7 @@ NBox { // Screen Recorder NIconButton { icon: "videocam" - tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording" : "Start screen recording" + tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording." : "Start screen recording." colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary onClicked: { @@ -42,7 +42,7 @@ NBox { // Idle Inhibitor NIconButton { icon: "coffee" - tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake" + tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake." : "Enable keep awake." colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary onClicked: { @@ -54,7 +54,7 @@ NBox { NIconButton { visible: Settings.data.wallpaper.enabled icon: "image" - tooltipText: "Left click: Open wallpaper selector\nRight click: Set random wallpaper" + tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper." onClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector From 0fd9ac15cd7719d67c9eda67349a079ad520aee1 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 22:38:20 -0400 Subject: [PATCH 44/61] One more tooltip --- Modules/Bar/Widgets/Taskbar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml index 0eef32c..103e707 100644 --- a/Modules/Bar/Widgets/Taskbar.qml +++ b/Modules/Bar/Widgets/Taskbar.qml @@ -91,7 +91,7 @@ Rectangle { NTooltip { id: taskbarTooltip - text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App" + text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App." target: taskbarItem positionAbove: Settings.data.bar.position === "bottom" } From 35ca3462464b0239de60ce419c0bf7d205c33e01 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 23:17:18 -0400 Subject: [PATCH 45/61] Tooltip --- Modules/Bar/Widgets/WiFi.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 0917f36..0571c17 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -50,6 +50,6 @@ NIconButton { return "signal_wifi_bad" } } - tooltipText: "Network / Wi-Fi" + tooltipText: "Network / Wi-Fi." onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this) } From 9a6c98c1346c262b6c3648a401591222adbdf9d1 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 23:18:23 -0400 Subject: [PATCH 46/61] WiFi: removed status indicator --- Modules/WiFiPanel/WiFiPanel.qml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 1a77e69..484f9ee 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -45,7 +45,7 @@ NPanel { } NText { - text: "WiFi" + text: "Wi-Fi" font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold color: Color.mOnSurface @@ -53,20 +53,11 @@ NPanel { Layout.leftMargin: Style.marginS * scaling } - // Connection status indicator - Rectangle { - visible: NetworkService.hasActiveConnection - width: 8 * scaling - height: 8 * scaling - radius: 4 * scaling - color: Color.mPrimary - } - NIconButton { icon: "refresh" tooltipText: "Refresh networks" sizeRatio: 0.8 - enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading + enabled: Settings.data.network.wifiEnabled onClicked: NetworkService.refreshNetworks() } From 2f416a87f0dacc27cc3a57bc2744bbacb4187e96 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Fri, 5 Sep 2025 23:42:05 -0400 Subject: [PATCH 47/61] Wifi/Network: refactoring to something simpler to maintain --- Modules/Bar/Widgets/WiFi.qml | 2 - Modules/WiFiPanel/WiFiPanel.qml | 577 +++++++++++------------ Services/NetworkService.qml | 795 +++++++++++--------------------- 3 files changed, 571 insertions(+), 803 deletions(-) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 0571c17..2b06c6f 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -13,8 +13,6 @@ NIconButton { property ShellScreen screen property real scaling: 1.0 - visible: Settings.data.network.wifiEnabled - sizeRatio: 0.8 Component.onCompleted: { diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 484f9ee..98fde74 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -14,16 +14,11 @@ NPanel { panelHeight: 500 * scaling panelKeyboardFocus: true - property string passwordPromptSsid: "" + property string passwordSsid: "" property string passwordInput: "" - property bool showPasswordPrompt: false - property string expandedNetwork: "" // Track which network shows options + property string expandedSsid: "" - onOpened: { - if (Settings.data.network.wifiEnabled) { - NetworkService.refreshNetworks() - } - } + onOpened: NetworkService.scan() panelContent: Rectangle { color: Color.transparent @@ -39,9 +34,9 @@ NPanel { spacing: Style.marginM * scaling NIcon { - text: "wifi" + text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off" font.pointSize: Style.fontSizeXXL * scaling - color: Color.mPrimary + color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant } NText { @@ -50,15 +45,21 @@ NPanel { font.weight: Style.fontWeightBold color: Color.mOnSurface Layout.fillWidth: true - Layout.leftMargin: Style.marginS * scaling + } + + NToggle { + id: wifiSwitch + checked: Settings.data.network.wifiEnabled + onToggled: checked => NetworkService.setWifiEnabled(checked) + baseSize: Style.baseWidgetSize * 0.7 * scaling } NIconButton { icon: "refresh" - tooltipText: "Refresh networks" + tooltipText: "Refresh" sizeRatio: 0.8 - enabled: Settings.data.network.wifiEnabled - onClicked: NetworkService.refreshNetworks() + enabled: Settings.data.network.wifiEnabled && !NetworkService.scanning + onClicked: NetworkService.scan() } NIconButton { @@ -73,17 +74,18 @@ NPanel { Layout.fillWidth: true } - // Error banner + // Error message Rectangle { - visible: NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0 + visible: NetworkService.lastError.length > 0 Layout.fillWidth: true - Layout.preferredHeight: errorText.implicitHeight + (Style.marginM * scaling * 2) + Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * scaling * 2) color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1) radius: Style.radiusS * scaling border.width: Math.max(1, Style.borderS * scaling) border.color: Color.mError RowLayout { + id: errorRow anchors.fill: parent anchors.margins: Style.marginM * scaling spacing: Style.marginS * scaling @@ -95,8 +97,7 @@ NPanel { } NText { - id: errorText - text: NetworkService.connectError + text: NetworkService.lastError color: Color.mError font.pointSize: Style.fontSizeS * scaling wrapMode: Text.Wrap @@ -106,301 +107,287 @@ NPanel { NIconButton { icon: "close" sizeRatio: 0.6 - onClicked: { - NetworkService.connectStatus = "" - NetworkService.connectError = "" - } + onClicked: NetworkService.lastError = "" } } } - ScrollView { + // Main content area + Rectangle { Layout.fillWidth: true Layout.fillHeight: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - clip: true - contentWidth: availableWidth + color: Color.transparent + // WiFi disabled state ColumnLayout { - width: parent.width - spacing: Style.marginM * scaling + visible: !Settings.data.network.wifiEnabled + anchors.fill: parent + spacing: Style.marginL * scaling - // Loading state - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: Settings.data.network.wifiEnabled && NetworkService.isLoading && Object.keys( - NetworkService.networks).length === 0 - spacing: Style.marginM * scaling + Item { Layout.fillHeight: true } - NBusyIndicator { - running: true - color: Color.mPrimary - size: Style.baseWidgetSize * scaling - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "Scanning for networks..." - font.pointSize: Style.fontSizeNormal * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + NIcon { + text: "wifi_off" + font.pointSize: 64 * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter } - // WiFi disabled state + NText { + text: "Wi-Fi is disabled" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Enable Wi-Fi to see available networks" + font.pointSize: Style.fontSizeS * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { Layout.fillHeight: true } + } + + // Scanning state + ColumnLayout { + visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 + anchors.fill: parent + spacing: Style.marginL * scaling + + Item { Layout.fillHeight: true } + + NBusyIndicator { + running: true + color: Color.mPrimary + size: Style.baseWidgetSize * scaling + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Scanning for networks..." + font.pointSize: Style.fontSizeNormal * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { Layout.fillHeight: true } + } + + // Networks list container + ScrollView { + visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0) + anchors.fill: parent + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + clip: true + ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: !Settings.data.network.wifiEnabled + width: parent.width spacing: Style.marginM * scaling - NIcon { - text: "wifi_off" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: "WiFi is disabled" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NButton { - text: "Enable WiFi" - icon: "wifi" - Layout.alignment: Qt.AlignHCenter - onClicked: { - Settings.data.network.wifiEnabled = true - Settings.save() - NetworkService.setWifiEnabled(true) + // Network list + Repeater { + model: { + if (!Settings.data.network.wifiEnabled) return [] + + const nets = Object.values(NetworkService.networks) + return nets.sort((a, b) => { + if (a.connected !== b.connected) return b.connected - a.connected + return b.signal - a.signal + }) } - } - } - - // Network list - Repeater { - model: { - if (!Settings.data.network.wifiEnabled || NetworkService.isLoading) - return [] - - // Sort networks: connected first, then by signal strength - const nets = Object.values(NetworkService.networks) - return nets.sort((a, b) => { - if (a.connected && !b.connected) - return -1 - if (!a.connected && b.connected) - return 1 - return b.signal - a.signal - }) - } - - Item { - Layout.fillWidth: true - implicitHeight: networkRect.implicitHeight Rectangle { - id: networkRect - width: parent.width - implicitHeight: networkContent.implicitHeight + (Style.marginM * scaling * 2) + Layout.fillWidth: true + implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2) radius: Style.radiusM * scaling - color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, - 0.05) : Color.mSurface + color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface border.width: Math.max(1, Style.borderS * scaling) border.color: modelData.connected ? Color.mPrimary : Color.mOutline - clip: true ColumnLayout { - id: networkContent + id: netColumn width: parent.width - (Style.marginM * scaling * 2) x: Style.marginM * scaling y: Style.marginM * scaling - spacing: Style.marginM * scaling + spacing: Style.marginS * scaling - // Main network row + // Main row RowLayout { Layout.fillWidth: true spacing: Style.marginS * scaling - // Signal icon NIcon { text: NetworkService.signalIcon(modelData.signal) font.pointSize: Style.fontSizeXXL * scaling color: modelData.connected ? Color.mPrimary : Color.mOnSurface } - // Network info ColumnLayout { Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - spacing: 0 + spacing: 2 * scaling NText { - text: modelData.ssid || "Unknown Network" + text: modelData.ssid font.pointSize: Style.fontSizeNormal * scaling font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium - elide: Text.ElideRight color: Color.mOnSurface + elide: Text.ElideRight Layout.fillWidth: true } - NText { - text: { - const security = modelData.security - && modelData.security !== "--" ? modelData.security : "Open" - const signal = `${modelData.signal}%` - return `${signal} • ${security}` - } - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnSurfaceVariant - } - } - - // Right-aligned items container - RowLayout { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - spacing: Style.marginS * scaling - - // Connected badge - Rectangle { - visible: modelData.connected - color: Color.mPrimary - radius: width * 0.5 - width: connectedLabel.implicitWidth + (Style.marginS * scaling * 2) - height: connectedLabel.implicitHeight + (Style.marginXS * scaling * 2) + RowLayout { + spacing: Style.marginXS * scaling NText { - id: connectedLabel - anchors.centerIn: parent - text: "Connected" - font.pointSize: Style.fontSizeXXS * scaling - color: Color.mOnPrimary - } - } - - // Saved badge - clickable - Rectangle { - visible: modelData.cached && !modelData.connected - color: Color.mSurfaceVariant - radius: width * 0.5 - width: savedLabel.implicitWidth + (Style.marginS * scaling * 2) - height: savedLabel.implicitHeight + (Style.marginXS * scaling * 2) - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: parent.color = Qt.darker(Color.mSurfaceVariant, 1.1) - onExited: parent.color = Color.mSurfaceVariant - onClicked: { - expandedNetwork = expandedNetwork === modelData.ssid ? "" : modelData.ssid - showPasswordPrompt = false - } - } - - NText { - id: savedLabel - anchors.centerIn: parent - text: "Saved" + text: `${modelData.signal}%` font.pointSize: Style.fontSizeXXS * scaling color: Color.mOnSurfaceVariant } - } - // Loading indicator - NBusyIndicator { - visible: NetworkService.connectingSsid === modelData.ssid - running: NetworkService.connectingSsid === modelData.ssid - color: Color.mPrimary - size: Style.baseWidgetSize * 0.6 * scaling - } + NText { + text: "•" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant + } - // Action buttons - RowLayout { - spacing: Style.marginXS * scaling - visible: NetworkService.connectingSsid !== modelData.ssid + NText { + text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant + } - NButton { - visible: !modelData.connected && (expandedNetwork !== modelData.ssid || !showPasswordPrompt) - outlined: !hovered - fontSize: Style.fontSizeXS * scaling - text: modelData.existing ? "Connect" : (NetworkService.isSecured( - modelData.security) ? "Password" : "Connect") - onClicked: { - if (modelData.existing || !NetworkService.isSecured(modelData.security)) { - NetworkService.connectNetwork(modelData.ssid, modelData.security) - } else { - expandedNetwork = modelData.ssid - passwordPromptSsid = modelData.ssid - showPasswordPrompt = true - passwordInput = "" - Qt.callLater(() => passwordInputField.forceActiveFocus()) - } + Item { + Layout.preferredWidth: Style.marginXXS * scaling + } + + + Rectangle { + visible: modelData.connected + color: Color.mPrimary + radius: height * 0.5 + width: connectedText.implicitWidth + (Style.marginS * scaling * 2) + height: connectedText.implicitHeight + (Style.marginXXS * scaling * 2) + + NText { + id: connectedText + anchors.centerIn: parent + text: "Connected" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnPrimary } } - NButton { - visible: modelData.connected - outlined: !hovered - fontSize: Style.fontSizeXS * scaling - backgroundColor: Color.mError - text: "Disconnect" - onClicked: NetworkService.disconnectNetwork(modelData.ssid) + Rectangle { + visible: modelData.cached && !modelData.connected + color: Color.transparent + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + radius: height * 0.5 + width: savedText.implicitWidth + (Style.marginS * scaling * 2) + height: savedText.implicitHeight + (Style.marginXXS * scaling * 2) + + NText { + id: savedText + anchors.centerIn: parent + text: "Saved" + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnSurfaceVariant + } } } } + + // Action area + RowLayout { + spacing: Style.marginXS * scaling + + NBusyIndicator { + visible: NetworkService.connectingTo === modelData.ssid + running: visible + color: Color.mPrimary + size: Style.baseWidgetSize * 0.5 * scaling + } + + NButton { + visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid + text: { + if (modelData.existing || modelData.cached) return "Connect" + if (!NetworkService.isSecured(modelData.security)) return "Connect" + return "Password" + } + outlined: !hovered + fontSize: Style.fontSizeXS * scaling + onClicked: { + if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) { + NetworkService.connect(modelData.ssid) + } else { + passwordSsid = modelData.ssid + passwordInput = "" + expandedSsid = "" + } + } + } + + NButton { + visible: modelData.connected + text: "Disconnect" + outlined: !hovered + fontSize: Style.fontSizeXS * scaling + backgroundColor: Color.mError + onClicked: NetworkService.disconnect(modelData.ssid) + } + + NIconButton { + visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid + icon: "more_vert" + sizeRatio: 0.7 + onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid + } + } } - // Password input section + // Password input Rectangle { - visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt + visible: passwordSsid === modelData.ssid Layout.fillWidth: true - implicitHeight: visible ? 50 * scaling : 0 + height: 40 * scaling color: Color.mSurfaceVariant radius: Style.radiusS * scaling RowLayout { anchors.fill: parent - anchors.margins: Style.marginS * scaling - spacing: Style.marginS * scaling + anchors.margins: Style.marginXS * scaling + spacing: Style.marginXS * scaling Rectangle { Layout.fillWidth: true Layout.fillHeight: true - radius: Style.radiusS * scaling + radius: Style.radiusXS * scaling color: Color.mSurface - border.color: passwordInputField.activeFocus ? Color.mSecondary : Color.mOutline + border.color: pwdInput.activeFocus ? Color.mPrimary : Color.mOutline border.width: Math.max(1, Style.borderS * scaling) TextInput { - id: passwordInputField + id: pwdInput anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - height: parent.height + anchors.margins: Style.marginS * scaling text: passwordInput - font.pointSize: Style.fontSizeM * scaling + font.pointSize: Style.fontSizeS * scaling color: Color.mOnSurface - verticalAlignment: TextInput.AlignVCenter - clip: true - focus: modelData.ssid === passwordPromptSsid && showPasswordPrompt - selectByMouse: true echoMode: TextInput.Password - passwordCharacter: "●" + selectByMouse: true + focus: visible onTextChanged: passwordInput = text onAccepted: { - if (passwordInput) { - NetworkService.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - expandedNetwork = "" + if (text) { + NetworkService.connect(passwordSsid, text) + passwordSsid = "" + passwordInput = "" } } @@ -416,56 +403,68 @@ NPanel { NButton { text: "Connect" - icon: "check" - fontSize: Style.fontSizeXS * scaling + fontSize: Style.fontSizeXXS * scaling enabled: passwordInput.length > 0 - outlined: !enabled onClicked: { - if (passwordInput) { - NetworkService.submitPassword(passwordPromptSsid, passwordInput) - showPasswordPrompt = false - expandedNetwork = "" - } + NetworkService.connect(passwordSsid, passwordInput) + passwordSsid = "" + passwordInput = "" } } NIconButton { icon: "close" - tooltipText: "Cancel" - sizeRatio: 0.9 + sizeRatio: 0.6 onClicked: { - showPasswordPrompt = false - expandedNetwork = "" + passwordSsid = "" passwordInput = "" } } } } - // Forget network option - appears when saved badge is clicked - RowLayout { - visible: (modelData.existing || modelData.cached) && expandedNetwork === modelData.ssid - && !showPasswordPrompt + // Options menu + Rectangle { + visible: expandedSsid === modelData.ssid Layout.fillWidth: true - Layout.topMargin: Style.marginXS * scaling - spacing: Style.marginS * scaling + height: forgetRow.implicitHeight + Style.marginS * 2 + color: Color.mSurfaceVariant + radius: Style.radiusS * scaling + border.width: Math.max(1, Style.borderS * scaling) + border.color: Color.mError - Item { - Layout.fillWidth: true - } + RowLayout { + id: forgetRow + anchors.fill: parent + anchors.margins: Style.marginS * scaling - NButton { - id: forgetButton - text: "Forget Network" - icon: "delete_outline" - fontSize: Style.fontSizeXXS * scaling - backgroundColor: Color.mError - textColor: !forgetButton.hovered ? Color.mError : Color.mOnTertiary - outlined: !forgetButton.hovered - Layout.preferredHeight: 28 * scaling - onClicked: { - NetworkService.forgetNetwork(modelData.ssid) - expandedNetwork = "" + NIcon { + text: "delete_outline" + font.pointSize: Style.fontSizeM * scaling + color: Color.mError + } + + NText { + text: "Forget this network?" + font.pointSize: Style.fontSizeS * scaling + color: Color.mError + Layout.fillWidth: true + } + + NButton { + text: "Forget" + fontSize: Style.fontSizeXXS * scaling + backgroundColor: Color.mError + onClicked: { + NetworkService.forget(modelData.ssid) + expandedSsid = "" + } + } + + NIconButton { + icon: "close" + sizeRatio: 0.6 + onClicked: expandedSsid = "" } } } @@ -473,38 +472,40 @@ NPanel { } } } + } - // No networks found - ColumnLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading && Object.keys( - NetworkService.networks).length === 0 - spacing: Style.marginM * scaling + // Empty state when no networks + ColumnLayout { + visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 + anchors.fill: parent + spacing: Style.marginL * scaling - NIcon { - text: "wifi_find" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } + Item { Layout.fillHeight: true } - NText { - text: "No networks found" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurfaceVariant - Layout.alignment: Qt.AlignHCenter - } - - NButton { - text: "Refresh" - icon: "refresh" - Layout.alignment: Qt.AlignHCenter - onClicked: NetworkService.refreshNetworks() - } + NIcon { + text: "wifi_find" + font.pointSize: 64 * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter } + + NText { + text: "No networks found" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NButton { + text: "Scan Again" + icon: "refresh" + Layout.alignment: Qt.AlignHCenter + onClicked: NetworkService.scan() + } + + Item { Layout.fillHeight: true } } } } } -} +} \ No newline at end of file diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index fa34523..4e23c8d 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -8,364 +8,329 @@ import qs.Commons Singleton { id: root - // Core properties + // Core state property var networks: ({}) - property string connectingSsid: "" - property string connectStatus: "" - property string connectStatusSsid: "" - property string connectError: "" - property bool isLoading: false - property bool ethernet: false - property int retryCount: 0 - property int maxRetries: 3 - - // File path for persistent storage + property bool scanning: false + property bool connecting: false + property string connectingTo: "" + property string lastError: "" + + // Persistent cache property string cacheFile: Settings.cacheDir + "network.json" + readonly property string cachedLastConnected: cacheAdapter.lastConnected + readonly property var cachedNetworks: cacheAdapter.knownNetworks - // Stable properties for UI - readonly property alias cache: adapter - readonly property string lastConnectedNetwork: adapter.lastConnected - - // File-based persistent storage + // Cache file handling FileView { id: cacheFileView path: root.cacheFile - onAdapterUpdated: saveTimer.start() - onLoaded: { - Logger.log("Network", "Loaded network cache from disk") - // Try to auto-connect on startup if WiFi is enabled - if (Settings.data.network.wifiEnabled && adapter.lastConnected) { - autoConnectTimer.start() - } - } - onLoadFailed: function (error) { - Logger.log("Network", "No existing cache found, creating new one") - // Initialize with empty data - adapter.knownNetworks = ({}) - adapter.lastConnected = "" - } - + JsonAdapter { - id: adapter + id: cacheAdapter property var knownNetworks: ({}) property string lastConnected: "" - property int lastRefresh: 0 + } + + onLoadFailed: { + cacheAdapter.knownNetworks = ({}) + cacheAdapter.lastConnected = "" } } - // Save timer to batch writes + Component.onCompleted: { + Logger.log("Network", "Service initialized") + syncWifiState() + if (Settings.data.network.wifiEnabled) { + scan() + } + } + + // Save cache with debounce Timer { - id: saveTimer - running: false + id: saveDebounce interval: 1000 onTriggered: cacheFileView.writeAdapter() } - Component.onCompleted: { - Logger.log("Network", "Service started") - - if (Settings.data.network.wifiEnabled) { - refreshNetworks() - } - } - - // Signal strength icon mapping - function signalIcon(signal) { - const levels = [{ - "threshold": 80, - "icon": "network_wifi" - }, { - "threshold": 60, - "icon": "network_wifi_3_bar" - }, { - "threshold": 40, - "icon": "network_wifi_2_bar" - }, { - "threshold": 20, - "icon": "network_wifi_1_bar" - }] - - for (const level of levels) { - if (signal >= level.threshold) - return level.icon - } - return "signal_wifi_0_bar" - } - - function isSecured(security) { - return security && security.trim() !== "" && security.trim() !== "--" - } - - // Enhanced refresh with retry logic - function refreshNetworks() { - if (isLoading) - return - - isLoading = true - retryCount = 0 - adapter.lastRefresh = Date.now() - performRefresh() - } - - function performRefresh() { - checkEthernet.running = true - existingNetworkProcess.running = true - } - - // Retry mechanism for failed operations - function retryRefresh() { - if (retryCount < maxRetries) { - retryCount++ - Logger.log("Network", `Retrying refresh (${retryCount}/${maxRetries})`) - retryTimer.start() - } else { - isLoading = false - connectError = "Failed to refresh networks after multiple attempts" - } + function saveCache() { + saveDebounce.restart() } + // Single refresh timer for periodic scans Timer { - id: retryTimer - interval: 1000 * retryCount // Progressive backoff - repeat: false - onTriggered: performRefresh() + id: refreshTimer + interval: 30000 + running: Settings.data.network.wifiEnabled && !scanning + repeat: true + onTriggered: scan() } + // Delayed scan timer for WiFi enable Timer { - id: autoConnectTimer - interval: 3000 - repeat: false - onTriggered: { - if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) { - Logger.log("Network", `Auto-connecting to ${adapter.lastConnected}`) - connectToExisting(adapter.lastConnected) - } - } + id: delayedScanTimer + interval: 7000 + onTriggered: scan() } - // Forget network function - function forgetNetwork(ssid) { - Logger.log("Network", `Forgetting network: ${ssid}`) - - // Remove from cache - let known = adapter.knownNetworks - delete known[ssid] - adapter.knownNetworks = known - - // Clear last connected if it's this network - if (adapter.lastConnected === ssid) { - adapter.lastConnected = "" - } - - // Save changes - saveTimer.restart() - - // Remove NetworkManager profile - forgetProcess.ssid = ssid - forgetProcess.running = true + // Core functions + function syncWifiState() { + wifiStateProcess.running = true } - Process { - id: forgetProcess - property string ssid: "" - running: false - command: ["nmcli", "connection", "delete", "id", ssid] - - stdout: StdioCollector { - onStreamFinished: { - Logger.log("Network", `Successfully forgot network: ${forgetProcess.ssid}`) - refreshNetworks() - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - if (text.includes("no such connection profile")) { - Logger.log("Network", `Network profile not found: ${forgetProcess.ssid}`) - } else { - Logger.warn("Network", `Error forgetting network: ${text}`) - } - refreshNetworks() - } - } - } - } - - // WiFi enable/disable functions function setWifiEnabled(enabled) { - if (enabled) { - isLoading = true - wifiRadioProcess.action = "on" - wifiRadioProcess.running = true + Settings.data.network.wifiEnabled = enabled + + wifiToggleProcess.action = enabled ? "on" : "off" + wifiToggleProcess.running = true + } + + function scan() { + if (scanning) return + + scanning = true + lastError = "" + scanProcess.running = true + } + + function connect(ssid, password = "") { + if (connecting) return + + connecting = true + connectingTo = ssid + lastError = "" + + // Check if we have a saved connection + if (networks[ssid]?.existing || cachedNetworks[ssid]) { + connectProcess.mode = "saved" + connectProcess.ssid = ssid + connectProcess.password = "" } else { - // Save current connection for later - for (const ssid in networks) { - if (networks[ssid].connected) { - adapter.lastConnected = ssid - saveTimer.restart() - disconnectNetwork(ssid) - break - } - } - - wifiRadioProcess.action = "off" - wifiRadioProcess.running = true + connectProcess.mode = "new" + connectProcess.ssid = ssid + connectProcess.password = password } - } - - // Unified WiFi radio control - Process { - id: wifiRadioProcess - property string action: "on" - running: false - command: ["nmcli", "radio", "wifi", action] - - onRunningChanged: { - if (!running) { - if (action === "on") { - wifiEnableTimer.start() - } else { - root.networks = ({}) - root.isLoading = false - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", `Error ${action === "on" ? "enabling" : "disabling"} WiFi: ${text}`) - } - } - } - } - - Timer { - id: wifiEnableTimer - interval: 2000 - repeat: false - onTriggered: { - refreshNetworks() - if (adapter.lastConnected) { - reconnectTimer.start() - } - } - } - - Timer { - id: reconnectTimer - interval: 3000 - repeat: false - onTriggered: { - if (adapter.lastConnected && networks[adapter.lastConnected]?.existing) { - connectToExisting(adapter.lastConnected) - } - } - } - - // Connection management - function connectNetwork(ssid, security) { - connectingSsid = ssid - connectStatus = "" - connectStatusSsid = ssid - connectError = "" - - // Check if profile exists - if (networks[ssid]?.existing) { - connectToExisting(ssid) - return - } - - // Check cache for known network - const known = adapter.knownNetworks[ssid] - if (known?.profileName) { - connectToExisting(known.profileName) - return - } - - // New connection - need password for secured networks - if (isSecured(security)) { - // Password will be provided through submitPassword - return - } - - // Open network - connect directly - createAndConnect(ssid, "", security) - } - - function submitPassword(ssid, password) { - const security = networks[ssid]?.security || "" - createAndConnect(ssid, password, security) - } - - function connectToExisting(ssid) { - connectingSsid = ssid - upConnectionProcess.profileName = ssid - upConnectionProcess.running = true - } - - function createAndConnect(ssid, password, security) { - connectingSsid = ssid - - connectProcess.ssid = ssid - connectProcess.password = password - connectProcess.isSecured = isSecured(security) + connectProcess.running = true } - function disconnectNetwork(ssid) { + function disconnect(ssid) { disconnectProcess.ssid = ssid disconnectProcess.running = true } - // Connection process - Process { - id: connectProcess - property string ssid: "" - property string password: "" - property bool isSecured: false - running: false - - command: { - const cmd = ["nmcli", "device", "wifi", "connect", ssid] - if (isSecured && password) { - cmd.push("password", password) - } - return cmd + function forget(ssid) { + // Remove from cache + let known = cacheAdapter.knownNetworks + delete known[ssid] + cacheAdapter.knownNetworks = known + + if (cacheAdapter.lastConnected === ssid) { + cacheAdapter.lastConnected = "" } + + saveCache() + + // Remove from system + forgetProcess.ssid = ssid + forgetProcess.running = true + } + // Helper functions + function signalIcon(signal) { + if (signal >= 80) return "network_wifi" + if (signal >= 60) return "network_wifi_3_bar" + if (signal >= 40) return "network_wifi_2_bar" + if (signal >= 20) return "network_wifi_1_bar" + return "signal_wifi_0_bar" + } + + function isSecured(security) { + return security && security !== "--" && security.trim() !== "" + } + + // Processes + Process { + id: wifiStateProcess + running: false + command: ["nmcli", "radio", "wifi"] + stdout: StdioCollector { onStreamFinished: { - handleConnectionSuccess(connectProcess.ssid) - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - handleConnectionError(connectProcess.ssid, text) + const enabled = text.trim() === "enabled" + if (Settings.data.network.wifiEnabled !== enabled) { + Settings.data.network.wifiEnabled = enabled } } } } Process { - id: upConnectionProcess - property string profileName: "" + id: wifiToggleProcess + property string action: "on" running: false - command: ["nmcli", "connection", "up", "id", profileName] - - stdout: StdioCollector { - onStreamFinished: { - handleConnectionSuccess(upConnectionProcess.profileName) + command: ["nmcli", "radio", "wifi", action] + + onRunningChanged: { + if (!running) { + if (action === "on") { + // Clear networks immediately and start delayed scan + root.networks = ({}) + delayedScanTimer.restart() + } else { + root.networks = ({}) + } } } - + stderr: StdioCollector { onStreamFinished: { if (text.trim()) { - handleConnectionError(upConnectionProcess.profileName, text) + Logger.warn("Network", "WiFi toggle error: " + text) + } + } + } + } + + Process { + id: scanProcess + running: false + command: ["sh", "-c", ` + # Get existing profiles + profiles=$(nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless' | cut -d: -f1) + + # Get WiFi networks + nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list | while read line; do + ssid=$(echo "$line" | cut -d: -f1) + security=$(echo "$line" | cut -d: -f2) + signal=$(echo "$line" | cut -d: -f3) + in_use=$(echo "$line" | cut -d: -f4) + + # Skip empty SSIDs + if [ -z "$ssid" ]; then + continue + fi + + existing=false + if echo "$profiles" | grep -q "^$ssid$"; then + existing=true + fi + + echo "$ssid|$security|$signal|$in_use|$existing" + done + `] + + stdout: StdioCollector { + onStreamFinished: { + const nets = {} + const lines = text.split("\n").filter(l => l.trim()) + + for (const line of lines) { + const parts = line.split("|") + if (parts.length < 5) continue + + const ssid = parts[0] + if (!ssid || ssid.trim() === "") continue + + const network = { + ssid: ssid, + security: parts[1] || "--", + signal: parseInt(parts[2]) || 0, + connected: parts[3] === "*", + existing: parts[4] === "true", + cached: ssid in cacheAdapter.knownNetworks + } + + // Track connected network + if (network.connected && cacheAdapter.lastConnected !== ssid) { + cacheAdapter.lastConnected = ssid + saveCache() + } + + // Keep best signal for duplicate SSIDs + if (!nets[ssid] || network.signal > nets[ssid].signal) { + nets[ssid] = network + } + } + + root.networks = nets + root.scanning = false + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.scanning = false + if (text.trim()) { + Logger.warn("Network", "Scan error: " + text) + // If scan fails, set a short retry + if (Settings.data.network.wifiEnabled) { + delayedScanTimer.interval = 5000 + delayedScanTimer.restart() + } + } + } + } + } + + Process { + id: connectProcess + property string mode: "new" + property string ssid: "" + property string password: "" + running: false + + command: { + if (mode === "saved") { + return ["nmcli", "connection", "up", "id", ssid] + } else { + const cmd = ["nmcli", "device", "wifi", "connect", ssid] + if (password) { + cmd.push("password", password) + } + return cmd + } + } + + stdout: StdioCollector { + onStreamFinished: { + // Success - update cache + let known = cacheAdapter.knownNetworks + known[connectProcess.ssid] = { + profileName: connectProcess.ssid, + lastConnected: Date.now() + } + cacheAdapter.knownNetworks = known + cacheAdapter.lastConnected = connectProcess.ssid + saveCache() + + root.connecting = false + root.connectingTo = "" + Logger.log("Network", "Connected to " + connectProcess.ssid) + + // Rescan to update status + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.connecting = false + root.connectingTo = "" + + if (text.trim()) { + // Parse common errors + if (text.includes("Secrets were required") || text.includes("no secrets provided")) { + root.lastError = "Incorrect password" + } else if (text.includes("No network with SSID")) { + root.lastError = "Network not found" + } else if (text.includes("Timeout")) { + root.lastError = "Connection timeout" + } else { + root.lastError = text.split("\n")[0].trim() + } + + Logger.warn("Network", "Connect error: " + text) } } } @@ -376,222 +341,26 @@ Singleton { property string ssid: "" running: false command: ["nmcli", "connection", "down", "id", ssid] - + onRunningChanged: { if (!running) { - connectingSsid = "" - connectStatus = "" - connectStatusSsid = "" - connectError = "" - refreshNetworks() - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", `Disconnect warning: ${text}`) - } - } - } - } - - // Connection result handlers - function handleConnectionSuccess(ssid) { - connectingSsid = "" - connectStatus = "success" - connectStatusSsid = ssid - connectError = "" - - // Update cache - let known = adapter.knownNetworks - known[ssid] = { - "profileName": ssid, - "lastConnected": Date.now(), - "autoConnect": true - } - adapter.knownNetworks = known - adapter.lastConnected = ssid - saveTimer.restart() - - Logger.log("Network", `Successfully connected to ${ssid}`) - refreshNetworks() - } - - function handleConnectionError(ssid, error) { - connectingSsid = "" - connectStatus = "error" - connectStatusSsid = ssid - connectError = parseError(error) - - Logger.warn("Network", `Failed to connect to ${ssid}: ${error}`) - } - - function parseError(error) { - // Simplify common error messages - if (error.includes("Secrets were required") || error.includes("no secrets provided")) { - return "Incorrect password" - } - if (error.includes("No network with SSID")) { - return "Network not found" - } - if (error.includes("Connection activation failed")) { - return "Connection failed. Please try again." - } - if (error.includes("Timeout")) { - return "Connection timeout. Network may be out of range." - } - // Return first line only - return error.split("\n")[0].trim() - } - - // Network scanning processes - Process { - id: existingNetworkProcess - running: false - command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] - - stdout: StdioCollector { - onStreamFinished: { - const profiles = {} - const lines = text.split("\n").filter(l => l.trim()) - - for (const line of lines) { - const parts = line.split(":") - const name = parts[0] - const type = parts[1] - if (name && type === "802-11-wireless") { - profiles[name] = { - "ssid": name, - "type": type - } - } - } - - scanProcess.existingProfiles = profiles - scanProcess.running = true - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", "Error listing connections:", text) - retryRefresh() - } + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() } } } Process { - id: scanProcess - property var existingProfiles: ({}) + id: forgetProcess + property string ssid: "" running: false - command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"] - - stdout: StdioCollector { - onStreamFinished: { - const networksMap = {} - const lines = text.split("\n").filter(l => l.trim()) - - for (const line of lines) { - const parts = line.split(":") - if (parts.length < 4) - continue - - const ssid = parts[0] - const security = parts[1] - const signalStr = parts[2] - const inUse = parts[3] - if (!ssid) - continue - - const signal = parseInt(signalStr) || 0 - const connected = inUse === "*" - - // Update last connected if we find the connected network - if (connected && adapter.lastConnected !== ssid) { - adapter.lastConnected = ssid - saveTimer.restart() - } - - // Merge with existing or create new - if (!networksMap[ssid] || signal > networksMap[ssid].signal) { - networksMap[ssid] = { - "ssid": ssid, - "security": security || "--", - "signal": signal, - "connected": connected, - "existing": ssid in scanProcess.existingProfiles, - "cached": ssid in adapter.knownNetworks - } - } - } - - root.networks = networksMap - root.isLoading = false - scanProcess.existingProfiles = {} - - //Logger.log("Network", `Found ${Object.keys(networksMap).length} wireless networks`) - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", "Error scanning networks:", text) - retryRefresh() - } + command: ["nmcli", "connection", "delete", "id", ssid] + + onRunningChanged: { + if (!running) { + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() } } } - - Process { - id: checkEthernet - running: false - command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] - - stdout: StdioCollector { - onStreamFinished: { - root.ethernet = text.split("\n").some(line => { - const parts = line.split(":") - return parts[1] === "ethernet" && parts[2] === "connected" - }) - } - } - } - - // Auto-refresh timer - Timer { - interval: 30000 // 30 seconds - running: Settings.data.network.wifiEnabled && !isLoading - repeat: true - onTriggered: { - // Only refresh if we should - const now = Date.now() - const timeSinceLastRefresh = now - adapter.lastRefresh - - // Refresh if: connected, or it's been more than 30 seconds - if (hasActiveConnection || timeSinceLastRefresh > 30000) { - refreshNetworks() - } - } - } - - property bool hasActiveConnection: { - return Object.values(networks).some(net => net.connected) - } - - // Menu state management - function onMenuOpened() { - if (Settings.data.network.wifiEnabled) { - refreshNetworks() - } - } - - function onMenuClosed() { - // Clean up temporary states - connectStatus = "" - connectError = "" - } -} +} \ No newline at end of file From a57bfeba31ddc87464cf412873100dd49e6cdb24 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 00:36:03 -0400 Subject: [PATCH 48/61] Background: Qt.callLater does not accept a delay as parameter. --- Modules/Background/Background.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 8fed9bf..a1eca8b 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -237,7 +237,7 @@ Variants { transitionProgress = 0.0 Qt.callLater(() => { currentWallpaper.asynchronous = true - }, 100) + }) } } From 2398961473b156c169a0d6a8f129afc8a2d26396 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 00:36:29 -0400 Subject: [PATCH 49/61] Wifi: more clean ups and improvements --- Modules/WiFiPanel/WiFiPanel.qml | 134 ++++++++++++++++---------- Services/NetworkService.qml | 163 +++++++++++++++++++------------- 2 files changed, 181 insertions(+), 116 deletions(-) diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 98fde74..4b8b904 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -51,7 +51,7 @@ NPanel { id: wifiSwitch checked: Settings.data.network.wifiEnabled onToggled: checked => NetworkService.setWifiEnabled(checked) - baseSize: Style.baseWidgetSize * 0.7 * scaling + baseSize: Style.baseWidgetSize * 0.65 * scaling } NIconButton { @@ -122,9 +122,11 @@ NPanel { ColumnLayout { visible: !Settings.data.network.wifiEnabled anchors.fill: parent - spacing: Style.marginL * scaling + spacing: Style.marginM * scaling - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } NIcon { text: "wifi_off" @@ -141,22 +143,27 @@ NPanel { } NText { - text: "Enable Wi-Fi to see available networks" + text: "Enable Wi-Fi to see available networks." font.pointSize: Style.fontSizeS * scaling color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } // Scanning state ColumnLayout { - visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 + visible: Settings.data.network.wifiEnabled && NetworkService.scanning && Object.keys( + NetworkService.networks).length === 0 anchors.fill: parent spacing: Style.marginL * scaling - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } NBusyIndicator { running: true @@ -172,12 +179,15 @@ NPanel { Layout.alignment: Qt.AlignHCenter } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } // Networks list container ScrollView { - visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0) + visible: Settings.data.network.wifiEnabled && (!NetworkService.scanning || Object.keys( + NetworkService.networks).length > 0) anchors.fill: parent ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded @@ -190,20 +200,23 @@ NPanel { // Network list Repeater { model: { - if (!Settings.data.network.wifiEnabled) return [] - + if (!Settings.data.network.wifiEnabled) + return [] + const nets = Object.values(NetworkService.networks) return nets.sort((a, b) => { - if (a.connected !== b.connected) return b.connected - a.connected - return b.signal - a.signal - }) + if (a.connected !== b.connected) + return b.connected - a.connected + return b.signal - a.signal + }) } Rectangle { Layout.fillWidth: true implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2) radius: Style.radiusM * scaling - color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface + color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, + 0.05) : Color.mSurface border.width: Math.max(1, Style.borderS * scaling) border.color: modelData.connected ? Color.mPrimary : Color.mOutline @@ -262,7 +275,6 @@ NPanel { Item { Layout.preferredWidth: Style.marginXXS * scaling } - Rectangle { visible: modelData.connected @@ -302,7 +314,7 @@ NPanel { // Action area RowLayout { - spacing: Style.marginXS * scaling + spacing: Style.marginS * scaling NBusyIndicator { visible: NetworkService.connectingTo === modelData.ssid @@ -311,11 +323,23 @@ NPanel { size: Style.baseWidgetSize * 0.5 * scaling } + NIconButton { + visible: (modelData.existing || modelData.cached) && !modelData.connected + && NetworkService.connectingTo !== modelData.ssid + icon: "delete" + tooltipText: "Forget network" + sizeRatio: 0.7 + onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid + } + NButton { - visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid + visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid + && passwordSsid !== modelData.ssid text: { - if (modelData.existing || modelData.cached) return "Connect" - if (!NetworkService.isSecured(modelData.security)) return "Connect" + if (modelData.existing || modelData.cached) + return "Connect" + if (!NetworkService.isSecured(modelData.security)) + return "Connect" return "Password" } outlined: !hovered @@ -339,13 +363,6 @@ NPanel { backgroundColor: Color.mError onClicked: NetworkService.disconnect(modelData.ssid) } - - NIconButton { - visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid - icon: "more_vert" - sizeRatio: 0.7 - onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid - } } } @@ -353,21 +370,24 @@ NPanel { Rectangle { visible: passwordSsid === modelData.ssid Layout.fillWidth: true - height: 40 * scaling + height: passwordRow.implicitHeight + Style.marginS * scaling * 2 color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) radius: Style.radiusS * scaling RowLayout { + id: passwordRow anchors.fill: parent - anchors.margins: Style.marginXS * scaling - spacing: Style.marginXS * scaling + anchors.margins: Style.marginS * scaling + spacing: Style.marginM * scaling Rectangle { Layout.fillWidth: true Layout.fillHeight: true radius: Style.radiusXS * scaling color: Color.mSurface - border.color: pwdInput.activeFocus ? Color.mPrimary : Color.mOutline + border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline border.width: Math.max(1, Style.borderS * scaling) TextInput { @@ -382,7 +402,10 @@ NPanel { echoMode: TextInput.Password selectByMouse: true focus: visible + passwordCharacter: "●" onTextChanged: passwordInput = text + onVisibleChanged: if (visible) + forceActiveFocus() onAccepted: { if (text) { NetworkService.connect(passwordSsid, text) @@ -405,6 +428,7 @@ NPanel { text: "Connect" fontSize: Style.fontSizeXXS * scaling enabled: passwordInput.length > 0 + outlined: true onClicked: { NetworkService.connect(passwordSsid, passwordInput) passwordSsid = "" @@ -414,7 +438,7 @@ NPanel { NIconButton { icon: "close" - sizeRatio: 0.6 + sizeRatio: 0.8 onClicked: { passwordSsid = "" passwordInput = "" @@ -423,38 +447,43 @@ NPanel { } } - // Options menu + // Forget network Rectangle { visible: expandedSsid === modelData.ssid Layout.fillWidth: true - height: forgetRow.implicitHeight + Style.marginS * 2 + height: forgetRow.implicitHeight + Style.marginS * 2 * scaling color: Color.mSurfaceVariant radius: Style.radiusS * scaling border.width: Math.max(1, Style.borderS * scaling) - border.color: Color.mError + border.color: Color.mOutline RowLayout { id: forgetRow anchors.fill: parent anchors.margins: Style.marginS * scaling + spacing: Style.marginM * scaling - NIcon { - text: "delete_outline" - font.pointSize: Style.fontSizeM * scaling - color: Color.mError - } + RowLayout { + NIcon { + text: "delete_outline" + font.pointSize: Style.fontSizeL * scaling + color: Color.mError + } - NText { - text: "Forget this network?" - font.pointSize: Style.fontSizeS * scaling - color: Color.mError - Layout.fillWidth: true + NText { + text: "Forget this network?" + font.pointSize: Style.fontSizeS * scaling + color: Color.mError + Layout.fillWidth: true + } } NButton { + id: forgetButton text: "Forget" fontSize: Style.fontSizeXXS * scaling backgroundColor: Color.mError + outlined: forgetButton.hovered ? false : true onClicked: { NetworkService.forget(modelData.ssid) expandedSsid = "" @@ -463,7 +492,7 @@ NPanel { NIconButton { icon: "close" - sizeRatio: 0.6 + sizeRatio: 0.8 onClicked: expandedSsid = "" } } @@ -476,11 +505,14 @@ NPanel { // Empty state when no networks ColumnLayout { - visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 + visible: Settings.data.network.wifiEnabled && !NetworkService.scanning && Object.keys( + NetworkService.networks).length === 0 anchors.fill: parent spacing: Style.marginL * scaling - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } NIcon { text: "wifi_find" @@ -503,9 +535,11 @@ NPanel { onClicked: NetworkService.scan() } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } } } -} \ No newline at end of file +} diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 4e23c8d..9eb04eb 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -14,7 +14,8 @@ Singleton { property bool connecting: false property string connectingTo: "" property string lastError: "" - + property bool ethernet: false + // Persistent cache property string cacheFile: Settings.cacheDir + "network.json" readonly property string cachedLastConnected: cacheAdapter.lastConnected @@ -24,13 +25,13 @@ Singleton { FileView { id: cacheFileView path: root.cacheFile - + JsonAdapter { id: cacheAdapter property var knownNetworks: ({}) property string lastConnected: "" } - + onLoadFailed: { cacheAdapter.knownNetworks = ({}) cacheAdapter.lastConnected = "" @@ -79,26 +80,29 @@ Singleton { function setWifiEnabled(enabled) { Settings.data.network.wifiEnabled = enabled - + wifiToggleProcess.action = enabled ? "on" : "off" wifiToggleProcess.running = true } function scan() { - if (scanning) return - + if (scanning) + return + scanning = true lastError = "" scanProcess.running = true + ethernetStateProcess.running = true } function connect(ssid, password = "") { - if (connecting) return - + if (connecting) + return + connecting = true connectingTo = ssid lastError = "" - + // Check if we have a saved connection if (networks[ssid]?.existing || cachedNetworks[ssid]) { connectProcess.mode = "saved" @@ -109,7 +113,7 @@ Singleton { connectProcess.ssid = ssid connectProcess.password = password } - + connectProcess.running = true } @@ -123,13 +127,13 @@ Singleton { let known = cacheAdapter.knownNetworks delete known[ssid] cacheAdapter.knownNetworks = known - + if (cacheAdapter.lastConnected === ssid) { cacheAdapter.lastConnected = "" } - + saveCache() - + // Remove from system forgetProcess.ssid = ssid forgetProcess.running = true @@ -137,10 +141,14 @@ Singleton { // Helper functions function signalIcon(signal) { - if (signal >= 80) return "network_wifi" - if (signal >= 60) return "network_wifi_3_bar" - if (signal >= 40) return "network_wifi_2_bar" - if (signal >= 20) return "network_wifi_1_bar" + if (signal >= 80) + return "network_wifi" + if (signal >= 60) + return "network_wifi_3_bar" + if (signal >= 40) + return "network_wifi_2_bar" + if (signal >= 20) + return "network_wifi_1_bar" return "signal_wifi_0_bar" } @@ -149,14 +157,32 @@ Singleton { } // Processes + Process { + id: ethernetStateProcess + running: false + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + + stdout: StdioCollector { + onStreamFinished: { + + root.ethernet = text.split("\n").some(line => { + const parts = line.split(":") + return parts[1] === "ethernet" && parts[2] === "connected" + }) + Logger.log("Network", "Ethernet connected:", root.ethernet) + } + } + } + Process { id: wifiStateProcess running: false command: ["nmcli", "radio", "wifi"] - + stdout: StdioCollector { onStreamFinished: { const enabled = text.trim() === "enabled" + Logger.log("Network", "Wifi enabled:", enabled) if (Settings.data.network.wifiEnabled !== enabled) { Settings.data.network.wifiEnabled = enabled } @@ -169,19 +195,20 @@ Singleton { property string action: "on" running: false command: ["nmcli", "radio", "wifi", action] - + onRunningChanged: { if (!running) { if (action === "on") { // Clear networks immediately and start delayed scan root.networks = ({}) + delayedScanTimer.interval = 8000 delayedScanTimer.restart() } else { root.networks = ({}) } } } - + stderr: StdioCollector { onStreamFinished: { if (text.trim()) { @@ -197,66 +224,69 @@ Singleton { command: ["sh", "-c", ` # Get existing profiles profiles=$(nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless' | cut -d: -f1) - + # Get WiFi networks nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list | while read line; do - ssid=$(echo "$line" | cut -d: -f1) - security=$(echo "$line" | cut -d: -f2) - signal=$(echo "$line" | cut -d: -f3) - in_use=$(echo "$line" | cut -d: -f4) - - # Skip empty SSIDs - if [ -z "$ssid" ]; then - continue - fi - - existing=false - if echo "$profiles" | grep -q "^$ssid$"; then - existing=true - fi - - echo "$ssid|$security|$signal|$in_use|$existing" + ssid=$(echo "$line" | cut -d: -f1) + security=$(echo "$line" | cut -d: -f2) + signal=$(echo "$line" | cut -d: -f3) + in_use=$(echo "$line" | cut -d: -f4) + + # Skip empty SSIDs + if [ -z "$ssid" ]; then + continue + fi + + existing=false + if echo "$profiles" | grep -q "^$ssid$"; then + existing=true + fi + + echo "$ssid|$security|$signal|$in_use|$existing" done - `] - + `] + stdout: StdioCollector { onStreamFinished: { const nets = {} const lines = text.split("\n").filter(l => l.trim()) - + for (const line of lines) { const parts = line.split("|") - if (parts.length < 5) continue - + if (parts.length < 5) + continue + const ssid = parts[0] - if (!ssid || ssid.trim() === "") continue - + if (!ssid || ssid.trim() === "") + continue + const network = { - ssid: ssid, - security: parts[1] || "--", - signal: parseInt(parts[2]) || 0, - connected: parts[3] === "*", - existing: parts[4] === "true", - cached: ssid in cacheAdapter.knownNetworks + "ssid": ssid, + "security": parts[1] || "--", + "signal": parseInt(parts[2]) || 0, + "connected": parts[3] === "*", + "existing": parts[4] === "true", + "cached": ssid in cacheAdapter.knownNetworks } - + // Track connected network if (network.connected && cacheAdapter.lastConnected !== ssid) { cacheAdapter.lastConnected = ssid saveCache() } - + // Keep best signal for duplicate SSIDs if (!nets[ssid] || network.signal > nets[ssid].signal) { nets[ssid] = network } } - + root.networks = nets root.scanning = false + Logger.log("Network", "Discovered", Object.keys(root.networks).length, "Wi-Fi networks") } } - + stderr: StdioCollector { onStreamFinished: { root.scanning = false @@ -278,7 +308,7 @@ Singleton { property string ssid: "" property string password: "" running: false - + command: { if (mode === "saved") { return ["nmcli", "connection", "up", "id", ssid] @@ -290,38 +320,39 @@ Singleton { return cmd } } - + stdout: StdioCollector { onStreamFinished: { // Success - update cache let known = cacheAdapter.knownNetworks known[connectProcess.ssid] = { - profileName: connectProcess.ssid, - lastConnected: Date.now() + "profileName": connectProcess.ssid, + "lastConnected": Date.now() } cacheAdapter.knownNetworks = known cacheAdapter.lastConnected = connectProcess.ssid saveCache() - + root.connecting = false root.connectingTo = "" Logger.log("Network", "Connected to " + connectProcess.ssid) - + // Rescan to update status delayedScanTimer.interval = 1000 delayedScanTimer.restart() } } - + stderr: StdioCollector { onStreamFinished: { root.connecting = false root.connectingTo = "" - + if (text.trim()) { // Parse common errors if (text.includes("Secrets were required") || text.includes("no secrets provided")) { root.lastError = "Incorrect password" + forget(connectProcess.ssid) } else if (text.includes("No network with SSID")) { root.lastError = "Network not found" } else if (text.includes("Timeout")) { @@ -329,7 +360,7 @@ Singleton { } else { root.lastError = text.split("\n")[0].trim() } - + Logger.warn("Network", "Connect error: " + text) } } @@ -341,7 +372,7 @@ Singleton { property string ssid: "" running: false command: ["nmcli", "connection", "down", "id", ssid] - + onRunningChanged: { if (!running) { delayedScanTimer.interval = 1000 @@ -355,7 +386,7 @@ Singleton { property string ssid: "" running: false command: ["nmcli", "connection", "delete", "id", ssid] - + onRunningChanged: { if (!running) { delayedScanTimer.interval = 1000 @@ -363,4 +394,4 @@ Singleton { } } } -} \ No newline at end of file +} From b3e44866997e15bd507a0b69c578d0aaad137c7b Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 01:14:40 -0400 Subject: [PATCH 50/61] Network: better refresh vs wifi scan --- Services/NetworkService.qml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 9eb04eb..21614f2 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -41,9 +41,7 @@ Singleton { Component.onCompleted: { Logger.log("Network", "Service initialized") syncWifiState() - if (Settings.data.network.wifiEnabled) { - scan() - } + refresh() } // Save cache with debounce @@ -61,9 +59,9 @@ Singleton { Timer { id: refreshTimer interval: 30000 - running: Settings.data.network.wifiEnabled && !scanning + running: true repeat: true - onTriggered: scan() + onTriggered: refresh() } // Delayed scan timer for WiFi enable @@ -85,6 +83,14 @@ Singleton { wifiToggleProcess.running = true } + function refresh() { + ethernetStateProcess.running = true + + if (Settings.data.network.wifiEnabled) { + scan() + } + } + function scan() { if (scanning) return @@ -92,7 +98,6 @@ Singleton { scanning = true lastError = "" scanProcess.running = true - ethernetStateProcess.running = true } function connect(ssid, password = "") { From 8658e11c1d90bf0f6d0ddd0809298b84ec89157e Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 12:16:53 +0200 Subject: [PATCH 51/61] NotificationHistoryPanel: fix layout alignment --- .../Notification/NotificationHistoryPanel.qml | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 46a0ccb..39686df 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -76,30 +76,38 @@ NPanel { ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - Layout.alignment: Qt.AlignCenter + Layout.alignment: Qt.AlignHCenter visible: NotificationService.historyModel.count === 0 - spacing: Style.marginM * scaling + spacing: Style.marginL * scaling + + Item { + Layout.fillHeight: true + } NIcon { text: "notifications_off" - font.pointSize: Style.fontSizeXXXL * scaling - color: Color.mOnSurface + font.pointSize: 64 * scaling + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "No notifications" font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface + color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } NText { text: "Your notifications will show up here as they arrive." - font.pointSize: Style.fontSizeNormal * scaling + font.pointSize: Style.fontSizeS * scaling color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter } + + Item { + Layout.fillHeight: true + } } // Notification list From e76b2c5497cad614cc5cc482d3dcdf5ab0d77300 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 12:18:14 +0200 Subject: [PATCH 52/61] Launcher: fix app2unit execution, implemented #202 --- Modules/Launcher/Plugins/ApplicationsPlugin.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Launcher/Plugins/ApplicationsPlugin.qml b/Modules/Launcher/Plugins/ApplicationsPlugin.qml index 8b88523..05f086f 100644 --- a/Modules/Launcher/Plugins/ApplicationsPlugin.qml +++ b/Modules/Launcher/Plugins/ApplicationsPlugin.qml @@ -85,7 +85,7 @@ Item { if (Settings.data.appLauncher.useApp2Unit && app.id) { Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`) - Quickshell.execDetached(["app2unit", "--", app.id]) + Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"]) } else if (app.execute) { app.execute() } else if (app.exec) { From 977b2d9e7cb918e6cd41952df61fb55f051b74a1 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 12:27:06 +0200 Subject: [PATCH 53/61] Added a Spacer widget so people can add spacing between other widgets (as requested in ##226). Spacer: create variable width invisible rectangle BarWidgetSettingsDialog: add Spacer support BarWidgetRegistry: add Spacer --- Modules/Bar/Widgets/Spacer.qml | 56 +++++++++++++++++++ .../Extras/BarWidgetSettingsDialog.qml | 26 +++++++++ Services/BarWidgetRegistry.qml | 9 +++ 3 files changed, 91 insertions(+) create mode 100644 Modules/Bar/Widgets/Spacer.qml diff --git a/Modules/Bar/Widgets/Spacer.qml b/Modules/Bar/Widgets/Spacer.qml new file mode 100644 index 0000000..5a62372 --- /dev/null +++ b/Modules/Bar/Widgets/Spacer.qml @@ -0,0 +1,56 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services +import qs.Widgets + +Item { + id: root + + // Widget properties passed from Bar.qml + property var screen + property real scaling: 1.0 + + property string barSection: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + // Get user settings from Settings data - make it reactive + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + // Use settings or defaults from BarWidgetRegistry + readonly property int userWidth: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex].width || BarWidgetRegistry.widgetMetadata["Spacer"].width + } + } + return BarWidgetRegistry.widgetMetadata["Spacer"].width + } + + // Set the width based on user settings + implicitWidth: userWidth * scaling + implicitHeight: Style.barHeight * scaling + width: implicitWidth + height: implicitHeight + + // Optional: Add a subtle visual indicator in debug mode + Rectangle { + anchors.fill: parent + color: Qt.rgba(1, 0, 0, 0.1) // Very subtle red tint + visible: Settings.data.general.debugMode || false + radius: 2 * scaling + } +} diff --git a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml index cccacbb..f9aa98d 100644 --- a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml @@ -68,6 +68,8 @@ Popup { sourceComponent: { if (settingsPopup.widgetId === "CustomButton") { return customButtonSettings + } else if (settingsPopup.widgetId === "Spacer") { + return spacerSettings } // Add more widget settings components here as needed return null @@ -157,4 +159,28 @@ Popup { } } } + + // Spacer settings component + Component { + id: spacerSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.width = parseInt(widthInput.text) || 20 + return settings + } + + NTextInput { + id: widthInput + Layout.fillWidth: true + label: "Width (pixels)" + description: "Width of the spacer in pixels." + text: settingsPopup.widgetData.width || "20" + placeholderText: "Enter width in pixels" + } + } + } } diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 65897a6..39afcd3 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -28,6 +28,7 @@ Singleton { "PowerToggle": powerToggleComponent, "ScreenRecorderIndicator": screenRecorderIndicatorComponent, "SidePanelToggle": sidePanelToggleComponent, + "Spacer": spacerComponent, "SystemMonitor": systemMonitorComponent, "Taskbar": taskbarComponent, "Tray": trayComponent, @@ -43,6 +44,11 @@ Singleton { "leftClickExec": "", "rightClickExec": "", "middleClickExec": "" + }, + "Spacer": { + "allowUserSettings": true, + "icon": "space_bar", + "width": 20 } }) @@ -101,6 +107,9 @@ Singleton { property Component sidePanelToggleComponent: Component { SidePanelToggle {} } + property Component spacerComponent: Component { + Spacer {} + } property Component systemMonitorComponent: Component { SystemMonitor {} } From 0aaf78fc51b129d8c83a23d4f67221765f30ea74 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 12:40:29 +0200 Subject: [PATCH 54/61] ActiveWindow: fix hyprland icon display (fixes #201) --- Modules/Bar/Widgets/ActiveWindow.qml | 34 ++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 3a7dd36..65f900e 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import Quickshell.Wayland import Quickshell.Widgets import qs.Commons import qs.Services @@ -9,28 +10,34 @@ import qs.Widgets RowLayout { id: root - property ShellScreen screen property real scaling: 1.0 readonly property real minWidth: 160 readonly property real maxWidth: 400 - Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling visible: getTitle() !== "" function getTitle() { - // Use the service's focusedWindowTitle property which is updated immediately - // when WindowOpenedOrChanged events are received return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" } function getAppIcon() { + // Try CompositorService first const focusedWindow = CompositorService.getFocusedWindow() - if (!focusedWindow || !focusedWindow.appId) - return "" + if (focusedWindow && focusedWindow.appId) { + return Icons.iconForAppId(focusedWindow.appId.toLowerCase()) + } - return Icons.iconForAppId(focusedWindow.appId) + // Fallback to ToplevelManager + if (ToplevelManager && ToplevelManager.activeToplevel) { + const activeToplevel = ToplevelManager.activeToplevel + if (activeToplevel.appId) { + return Icons.iconForAppId(activeToplevel.appId.toLowerCase()) + } + } + + return "" } // A hidden text element to safely measure the full title width @@ -81,8 +88,6 @@ RowLayout { NText { id: titleText - - // For short titles, show full. For long titles, truncate and expand on hover Layout.preferredWidth: { if (mouseArea.containsMouse) { return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling)) @@ -91,7 +96,6 @@ RowLayout { } } Layout.alignment: Qt.AlignVCenter - horizontalAlignment: Text.AlignLeft text: getTitle() font.pointSize: Style.fontSizeS * scaling @@ -119,4 +123,14 @@ RowLayout { } } } + + Connections { + target: CompositorService + function onActiveWindowChanged() { + windowIcon.source = Qt.binding(getAppIcon) + } + function onWindowListChanged() { + windowIcon.source = Qt.binding(getAppIcon) + } + } } From 4131e6503bb8a106743b7cf93e20af4eaf279702 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 12:44:19 +0200 Subject: [PATCH 55/61] Implement keyboard controls for PowerPanel as requested in ##227 PowerPanel: add support for keyboard controls --- Modules/PowerPanel/PowerPanel.qml | 241 ++++++++++++++++++++++-------- 1 file changed, 180 insertions(+), 61 deletions(-) diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml index e383c1f..efb475e 100644 --- a/Modules/PowerPanel/PowerPanel.qml +++ b/Modules/PowerPanel/PowerPanel.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Controls import QtQuick.Effects import QtQuick.Layouts import Quickshell @@ -16,6 +17,7 @@ NPanel { panelHeight: 380 * scaling panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true + panelKeyboardFocus: true // Timer properties property int timerDuration: 9000 // 9 seconds @@ -23,9 +25,44 @@ NPanel { property bool timerActive: false property int timeRemaining: 0 - // Cancel timer when panel is closing + // Navigation properties + property int selectedIndex: 0 + readonly property var powerOptions: [{ + "action": "lock", + "icon": "lock_outline", + "title": "Lock", + "subtitle": "Lock your session" + }, { + "action": "suspend", + "icon": "bedtime", + "title": "Suspend", + "subtitle": "Put the system to sleep" + }, { + "action": "reboot", + "icon": "refresh", + "title": "Reboot", + "subtitle": "Restart the system" + }, { + "action": "logout", + "icon": "exit_to_app", + "title": "Logout", + "subtitle": "End your session" + }, { + "action": "shutdown", + "icon": "power_settings_new", + "title": "Shutdown", + "subtitle": "Turn off the system", + "isShutdown": true + }] + + // Lifecycle handlers + onOpened: { + selectedIndex = 0 + } + onClosed: { cancelTimer() + selectedIndex = 0 } // Timer management @@ -79,6 +116,38 @@ NPanel { root.close() } + // Navigation functions + function selectNext() { + if (powerOptions.length > 0) { + selectedIndex = Math.min(selectedIndex + 1, powerOptions.length - 1) + } + } + + function selectPrevious() { + if (powerOptions.length > 0) { + selectedIndex = Math.max(selectedIndex - 1, 0) + } + } + + function selectFirst() { + selectedIndex = 0 + } + + function selectLast() { + if (powerOptions.length > 0) { + selectedIndex = powerOptions.length - 1 + } else { + selectedIndex = 0 + } + } + + function activate() { + if (powerOptions.length > 0 && powerOptions[selectedIndex]) { + const option = powerOptions[selectedIndex] + startTimer(option.action) + } + } + // Countdown timer Timer { id: countdownTimer @@ -93,8 +162,92 @@ NPanel { } panelContent: Rectangle { + id: ui color: Color.transparent + // Keyboard shortcuts + Shortcut { + sequence: "Ctrl+K" + onActivated: ui.selectPrevious() + enabled: root.opened + } + + Shortcut { + sequence: "Ctrl+J" + onActivated: ui.selectNext() + enabled: root.opened + } + + Shortcut { + sequence: "Up" + onActivated: ui.selectPrevious() + enabled: root.opened + } + + Shortcut { + sequence: "Down" + onActivated: ui.selectNext() + enabled: root.opened + } + + Shortcut { + sequence: "Home" + onActivated: ui.selectFirst() + enabled: root.opened + } + + Shortcut { + sequence: "End" + onActivated: ui.selectLast() + enabled: root.opened + } + + Shortcut { + sequence: "Return" + onActivated: ui.activate() + enabled: root.opened + } + + Shortcut { + sequence: "Enter" + onActivated: ui.activate() + enabled: root.opened + } + + Shortcut { + sequence: "Escape" + onActivated: { + if (timerActive) { + cancelTimer() + } else { + cancelTimer() + root.close() + } + } + enabled: root.opened + } + + // Navigation functions + function selectNext() { + root.selectNext() + } + + function selectPrevious() { + root.selectPrevious() + } + + function selectFirst() { + root.selectFirst() + } + + function selectLast() { + root.selectLast() + } + + function activate() { + root.activate() + } + ColumnLayout { anchors.fill: parent anchors.topMargin: Style.marginL * scaling @@ -144,55 +297,21 @@ NPanel { Layout.fillWidth: true spacing: Style.marginM * scaling - // Lock Screen - PowerButton { - Layout.fillWidth: true - icon: "lock_outline" - title: "Lock" - subtitle: "Lock your session" - onClicked: startTimer("lock") - pending: timerActive && pendingAction === "lock" - } - - // Suspend - PowerButton { - Layout.fillWidth: true - icon: "bedtime" - title: "Suspend" - subtitle: "Put the system to sleep" - onClicked: startTimer("suspend") - pending: timerActive && pendingAction === "suspend" - } - - // Reboot - PowerButton { - Layout.fillWidth: true - icon: "refresh" - title: "Reboot" - subtitle: "Restart the system" - onClicked: startTimer("reboot") - pending: timerActive && pendingAction === "reboot" - } - - // Logout - PowerButton { - Layout.fillWidth: true - icon: "exit_to_app" - title: "Logout" - subtitle: "End your session" - onClicked: startTimer("logout") - pending: timerActive && pendingAction === "logout" - } - - // Shutdown - PowerButton { - Layout.fillWidth: true - icon: "power_settings_new" - title: "Shutdown" - subtitle: "Turn off the system" - onClicked: startTimer("shutdown") - pending: timerActive && pendingAction === "shutdown" - isShutdown: true + Repeater { + model: powerOptions + delegate: PowerButton { + Layout.fillWidth: true + icon: modelData.icon + title: modelData.title + subtitle: modelData.subtitle + isShutdown: modelData.isShutdown || false + isSelected: index === selectedIndex + onClicked: { + selectedIndex = index + startTimer(modelData.action) + } + pending: timerActive && pendingAction === modelData.action + } } } } @@ -207,6 +326,7 @@ NPanel { property string subtitle: "" property bool pending: false property bool isShutdown: false + property bool isSelected: false signal clicked @@ -216,7 +336,7 @@ NPanel { if (pending) { return Qt.alpha(Color.mPrimary, 0.08) } - if (mouseArea.containsMouse) { + if (isSelected || mouseArea.containsMouse) { return Color.mSecondary } return Color.transparent @@ -242,13 +362,12 @@ NPanel { anchors.verticalCenter: parent.verticalCenter text: buttonRoot.icon color: { - if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && !mouseArea.containsMouse) + if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) return Color.mError - if (mouseArea.containsMouse) - return Color.mOnTertiary + if (buttonRoot.isSelected || mouseArea.containsMouse) + return Color.mOnSecondary return Color.mOnSurface } font.pointSize: Style.fontSizeXXXL * scaling @@ -279,10 +398,10 @@ NPanel { color: { if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && !mouseArea.containsMouse) + if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) return Color.mError - if (mouseArea.containsMouse) - return Color.mOnTertiary + if (buttonRoot.isSelected || mouseArea.containsMouse) + return Color.mOnSecondary return Color.mOnSurface } @@ -304,10 +423,10 @@ NPanel { color: { if (buttonRoot.pending) return Color.mPrimary - if (buttonRoot.isShutdown && !mouseArea.containsMouse) + if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse) return Color.mError - if (mouseArea.containsMouse) - return Color.mOnTertiary + if (buttonRoot.isSelected || mouseArea.containsMouse) + return Color.mOnSecondary return Color.mOnSurfaceVariant } opacity: Style.opacityHeavy From 0e53ce3ac030068c487e23b9cce8bb4739f61b70 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 14:17:12 +0200 Subject: [PATCH 56/61] Release v2.6.0 SettingsPanel: added keyboard navigation BluetoothPanel: UI enhancements WiFiPanel: UI enhancements NotificationPanel: UI enhancements ColorPicker: UI enhancements Toast: handle switching between toasts much better Notification: add DND option Notification: add actions LauncherTab: add app2unit toggle Spacer: added spacer widget with configurable width ActiveWindow: fix hyprland icon display PowerPanel: add keybind controls NetworkService: make it way more reliable More QoL fixes & changes --- Services/UpdateService.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index 7937b26..7318a68 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -8,8 +8,8 @@ Singleton { id: root // Public properties - property string baseVersion: "2.5.1" - property bool isDevelopment: true + property string baseVersion: "2.6.0" + property bool isDevelopment: false property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` From 3d9ef8c2ed6c9f6f10d7994a68716442942b045b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sat, 6 Sep 2025 14:20:31 +0200 Subject: [PATCH 57/61] switch to dev version --- Services/UpdateService.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index 7318a68..c4ec7d2 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -9,7 +9,7 @@ Singleton { // Public properties property string baseVersion: "2.6.0" - property bool isDevelopment: false + property bool isDevelopment: true property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` From 5bc8f410e77ec71cb69728d0459dd6779244f749 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 09:32:02 -0400 Subject: [PATCH 58/61] Network/Wi-Fi: smarter logging to avoid flood --- Modules/Bar/Widgets/WiFi.qml | 2 +- Services/NetworkService.qml | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 2b06c6f..cf5ddba 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -30,7 +30,7 @@ NIconButton { icon: { try { - if (NetworkService.ethernet) { + if (NetworkService.ethernetConnected) { return "lan" } let connected = false diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 21614f2..91fbca9 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -14,7 +14,7 @@ Singleton { property bool connecting: false property string connectingTo: "" property string lastError: "" - property bool ethernet: false + property bool ethernetConnected: false // Persistent cache property string cacheFile: Settings.cacheDir + "network.json" @@ -169,12 +169,14 @@ Singleton { stdout: StdioCollector { onStreamFinished: { - - root.ethernet = text.split("\n").some(line => { + const connected = text.split("\n").some(line => { const parts = line.split(":") return parts[1] === "ethernet" && parts[2] === "connected" }) - Logger.log("Network", "Ethernet connected:", root.ethernet) + if (root.ethernetConnected !== connected) { + root.ethernetConnected = connected + Logger.log("Network", "Ethernet connected:", root.ethernetConnected) + } } } } @@ -286,9 +288,12 @@ Singleton { } } + if (JSON.stringify(root.networks) !== JSON.stringify(nets)) { + Logger.log("Network", "Discovered", Object.keys(nets).length, "Wi-Fi networks") + } root.networks = nets root.scanning = false - Logger.log("Network", "Discovered", Object.keys(root.networks).length, "Wi-Fi networks") + } } From fc1ee9fb2fae032587f840d466010c6069b69ceb Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 13:03:22 -0400 Subject: [PATCH 59/61] Network/WiFi: improve UI with more immediate feedback on operations. + proper deletion of profiles when forgetting a network --- Modules/WiFiPanel/WiFiPanel.qml | 63 +++++++++++- Services/NetworkService.qml | 170 ++++++++++++++++++++++++++++---- 2 files changed, 210 insertions(+), 23 deletions(-) diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 4b8b904..c6c2427 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -215,11 +215,23 @@ NPanel { Layout.fillWidth: true implicitHeight: netColumn.implicitHeight + (Style.marginM * scaling * 2) radius: Style.radiusM * scaling + + // Add opacity for operations in progress + opacity: (NetworkService.disconnectingFrom === modelData.ssid + || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1.0 + color: modelData.connected ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.05) : Color.mSurface border.width: Math.max(1, Style.borderS * scaling) border.color: modelData.connected ? Color.mPrimary : Color.mOutline + // Smooth opacity animation + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + } + } + ColumnLayout { id: netColumn width: parent.width - (Style.marginM * scaling * 2) @@ -276,8 +288,9 @@ NPanel { Layout.preferredWidth: Style.marginXXS * scaling } + // Update the status badges area (around line 237) Rectangle { - visible: modelData.connected + visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid color: Color.mPrimary radius: height * 0.5 width: connectedText.implicitWidth + (Style.marginS * scaling * 2) @@ -292,8 +305,42 @@ NPanel { } } + Rectangle { + visible: NetworkService.disconnectingFrom === modelData.ssid + color: Color.mError + radius: height * 0.5 + width: disconnectingText.implicitWidth + (Style.marginS * scaling * 2) + height: disconnectingText.implicitHeight + (Style.marginXXS * scaling * 2) + + NText { + id: disconnectingText + anchors.centerIn: parent + text: "Disconnecting..." + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnPrimary + } + } + + Rectangle { + visible: NetworkService.forgettingNetwork === modelData.ssid + color: Color.mError + radius: height * 0.5 + width: forgettingText.implicitWidth + (Style.marginS * scaling * 2) + height: forgettingText.implicitHeight + (Style.marginXXS * scaling * 2) + + NText { + id: forgettingText + anchors.centerIn: parent + text: "Forgetting..." + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnPrimary + } + } + Rectangle { visible: modelData.cached && !modelData.connected + && NetworkService.forgettingNetwork !== modelData.ssid + && NetworkService.disconnectingFrom !== modelData.ssid color: Color.transparent border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) @@ -318,6 +365,8 @@ NPanel { NBusyIndicator { visible: NetworkService.connectingTo === modelData.ssid + || NetworkService.disconnectingFrom === modelData.ssid + || NetworkService.forgettingNetwork === modelData.ssid running: visible color: Color.mPrimary size: Style.baseWidgetSize * 0.5 * scaling @@ -326,6 +375,8 @@ NPanel { NIconButton { visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid + && NetworkService.disconnectingFrom !== modelData.ssid icon: "delete" tooltipText: "Forget network" sizeRatio: 0.7 @@ -335,6 +386,8 @@ NPanel { NButton { visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid + && NetworkService.disconnectingFrom !== modelData.ssid text: { if (modelData.existing || modelData.cached) return "Connect" @@ -356,7 +409,7 @@ NPanel { } NButton { - visible: modelData.connected + visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid text: "Disconnect" outlined: !hovered fontSize: Style.fontSizeXS * scaling @@ -368,7 +421,8 @@ NPanel { // Password input Rectangle { - visible: passwordSsid === modelData.ssid + visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid Layout.fillWidth: true height: passwordRow.implicitHeight + Style.marginS * scaling * 2 color: Color.mSurfaceVariant @@ -449,7 +503,8 @@ NPanel { // Forget network Rectangle { - visible: expandedSsid === modelData.ssid + visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid + && NetworkService.forgettingNetwork !== modelData.ssid Layout.fillWidth: true height: forgetRow.implicitHeight + Style.marginS * 2 * scaling color: Color.mSurfaceVariant diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 91fbca9..f46e12a 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -15,6 +15,8 @@ Singleton { property string connectingTo: "" property string lastError: "" property bool ethernetConnected: false + property string disconnectingFrom: "" + property string forgettingNetwork: "" // Persistent cache property string cacheFile: Settings.cacheDir + "network.json" @@ -123,11 +125,14 @@ Singleton { } function disconnect(ssid) { + disconnectingFrom = ssid disconnectProcess.ssid = ssid disconnectProcess.running = true } function forget(ssid) { + forgettingNetwork = ssid + // Remove from cache let known = cacheAdapter.knownNetworks delete known[ssid] @@ -144,6 +149,40 @@ Singleton { forgetProcess.running = true } + // Helper function to immediately update network status + function updateNetworkStatus(ssid, connected) { + let nets = networks + + // Update all networks connected status + for (let key in nets) { + if (nets[key].connected && key !== ssid) { + nets[key].connected = false + } + } + + // Update the target network if it exists + if (nets[ssid]) { + nets[ssid].connected = connected + nets[ssid].existing = true + nets[ssid].cached = true + } else if (connected) { + // Create a temporary entry if network doesn't exist yet + nets[ssid] = { + "ssid": ssid, + "security": "--", + "signal": 100, + "connected"// Default to good signal until real scan + : true, + "existing": true, + "cached": true + } + } + + // Trigger property change notification + networks = ({}) + networks = nets + } + // Helper functions function signalIcon(signal) { if (signal >= 80) @@ -170,9 +209,9 @@ Singleton { stdout: StdioCollector { onStreamFinished: { const connected = text.split("\n").some(line => { - const parts = line.split(":") - return parts[1] === "ethernet" && parts[2] === "connected" - }) + const parts = line.split(":") + return parts[1] === "ethernet" && parts[2] === "connected" + }) if (root.ethernetConnected !== connected) { root.ethernetConnected = connected Logger.log("Network", "Ethernet connected:", root.ethernetConnected) @@ -189,7 +228,7 @@ Singleton { stdout: StdioCollector { onStreamFinished: { const enabled = text.trim() === "enabled" - Logger.log("Network", "Wifi enabled:", enabled) + Logger.log("Network", "Wi-Fi enabled:", enabled) if (Settings.data.network.wifiEnabled !== enabled) { Settings.data.network.wifiEnabled = enabled } @@ -229,11 +268,11 @@ Singleton { id: scanProcess running: false command: ["sh", "-c", ` - # Get existing profiles - profiles=$(nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless' | cut -d: -f1) + # Get list of saved connection profiles (just the names) + profiles=$(nmcli -t -f NAME connection show | tr '\n' '|') # Get WiFi networks - nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list | while read line; do + nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do ssid=$(echo "$line" | cut -d: -f1) security=$(echo "$line" | cut -d: -f2) signal=$(echo "$line" | cut -d: -f3) @@ -244,8 +283,10 @@ Singleton { continue fi + # Check if SSID matches any profile name (simple check) + # This covers most cases where profile name equals or contains the SSID existing=false - if echo "$profiles" | grep -q "^$ssid$"; then + if echo "$profiles" | grep -qF "$ssid|"; then existing=true fi @@ -288,12 +329,24 @@ Singleton { } } - if (JSON.stringify(root.networks) !== JSON.stringify(nets)) { - Logger.log("Network", "Discovered", Object.keys(nets).length, "Wi-Fi networks") + // For logging purpose only + const oldSSIDs = Object.keys(root.networks) + const newSSIDs = Object.keys(nets) + const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid)) + const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid)) + if (newNetworks.length > 0 || lostNetworks.length > 0) { + if (newNetworks.length > 0) { + Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", ")) + } + if (lostNetworks.length > 0) { + Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", ")) + } + Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(nets).length) } + + // Assign the results root.networks = nets root.scanning = false - } } @@ -343,11 +396,14 @@ Singleton { cacheAdapter.lastConnected = connectProcess.ssid saveCache() + // Immediately update the UI before scanning + root.updateNetworkStatus(connectProcess.ssid, true) + root.connecting = false root.connectingTo = "" - Logger.log("Network", "Connected to " + connectProcess.ssid) + Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`) - // Rescan to update status + // Still do a scan to get accurate signal and security info delayedScanTimer.interval = 1000 delayedScanTimer.restart() } @@ -383,8 +439,27 @@ Singleton { running: false command: ["nmcli", "connection", "down", "id", ssid] - onRunningChanged: { - if (!running) { + stdout: StdioCollector { + onStreamFinished: { + Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`) + + // Immediately update UI on successful disconnect + root.updateNetworkStatus(disconnectProcess.ssid, false) + root.disconnectingFrom = "" + + // Do a scan to refresh the list + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.disconnectingFrom = "" + if (text.trim()) { + Logger.warn("Network", "Disconnect error: " + text) + } + // Still trigger a scan even on error delayedScanTimer.interval = 1000 delayedScanTimer.restart() } @@ -395,11 +470,68 @@ Singleton { id: forgetProcess property string ssid: "" running: false - command: ["nmcli", "connection", "delete", "id", ssid] - onRunningChanged: { - if (!running) { - delayedScanTimer.interval = 1000 + // Try multiple common profile name patterns + command: ["sh", "-c", ` + ssid="$1" + deleted=false + + # Try exact SSID match first + if nmcli connection delete id "$ssid" 2>/dev/null; then + echo "Deleted profile: $ssid" + deleted=true + fi + + # Try "Auto " pattern + if nmcli connection delete id "Auto $ssid" 2>/dev/null; then + echo "Deleted profile: Auto $ssid" + deleted=true + fi + + # Try " 1", " 2", etc. patterns + for i in 1 2 3; do + if nmcli connection delete id "$ssid $i" 2>/dev/null; then + echo "Deleted profile: $ssid $i" + deleted=true + fi + done + + if [ "$deleted" = "false" ]; then + echo "No profiles found for SSID: $ssid" + fi + `, "--", ssid] + + stdout: StdioCollector { + onStreamFinished: { + Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`) + Logger.log("Network", text.trim().replace(/[\r\n]/g, " ")) + + // Update both cached and existing status immediately + let nets = root.networks + if (nets[forgetProcess.ssid]) { + nets[forgetProcess.ssid].cached = false + nets[forgetProcess.ssid].existing = false + // Trigger property change + root.networks = ({}) + root.networks = nets + } + + root.forgettingNetwork = "" + + // Quick scan to verify the profile is gone + delayedScanTimer.interval = 500 + delayedScanTimer.restart() + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.forgettingNetwork = "" + if (text.trim() && !text.includes("No profiles found")) { + Logger.warn("Network", "Forget error: " + text) + } + // Still Trigger a scan even on error + delayedScanTimer.interval = 500 delayedScanTimer.restart() } } From 7860c41959de3d6b30db598a5b9ff10bfef507fe Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 14:14:47 -0400 Subject: [PATCH 60/61] Network/Wi-Fi: Removed auto polling every 30sec. Factorized more code and cleaned logs --- Modules/Bar/Widgets/WiFi.qml | 10 +--------- Modules/SettingsPanel/Tabs/NetworkTab.qml | 16 ++++----------- Modules/WiFiPanel/WiFiPanel.qml | 4 ++-- Services/NetworkService.qml | 24 +++++++++++++---------- 4 files changed, 21 insertions(+), 33 deletions(-) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index cf5ddba..77f8664 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -15,14 +15,6 @@ NIconButton { sizeRatio: 0.8 - Component.onCompleted: { - Logger.log("WiFi", "Widget component completed") - Logger.log("WiFi", "NetworkService available:", !!NetworkService) - if (NetworkService) { - Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks) - } - } - colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface colorBorder: Color.transparent @@ -44,7 +36,7 @@ NIconButton { } return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find" } catch (error) { - Logger.error("WiFi", "Error getting icon:", error) + Logger.error("Wi-Fi", "Error getting icon:", error) return "signal_wifi_bad" } } diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index 4b56ea7..0e1fd0d 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -12,22 +12,14 @@ ColumnLayout { spacing: Style.marginL * scaling NToggle { - label: "WiFi Enabled" - description: "Enable WiFi connectivity." + label: "Enable Wi-Fi" + description: "Enable Wi-Fi connectivity." checked: Settings.data.network.wifiEnabled - onToggled: checked => { - Settings.data.network.wifiEnabled = checked - NetworkService.setWifiEnabled(checked) - if (checked) { - ToastService.showNotice("WiFi", "Enabled") - } else { - ToastService.showNotice("WiFi", "Disabled") - } - } + onToggled: checked => NetworkService.setWifiEnabled(checked) } NToggle { - label: "Bluetooth Enabled" + label: "Enable Bluetooth" description: "Enable Bluetooth connectivity." checked: Settings.data.network.bluetoothEnabled onToggled: checked => { diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index c6c2427..1fa5f18 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -173,7 +173,7 @@ NPanel { } NText { - text: "Scanning for networks..." + text: "Searching for nearby networks..." font.pointSize: Style.fontSizeNormal * scaling color: Color.mOnSurfaceVariant Layout.alignment: Qt.AlignHCenter @@ -584,7 +584,7 @@ NPanel { } NButton { - text: "Scan Again" + text: "Scan again" icon: "refresh" Layout.alignment: Qt.AlignHCenter onClicked: NetworkService.scan() diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index f46e12a..c4b5820 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -40,6 +40,17 @@ Singleton { } } + Connections { + target: Settings.data.network + function onWifiEnabledChanged() { + if (Settings.data.network.wifiEnabled) { + ToastService.showNotice("Wi-Fi", "Enabled") + } else { + ToastService.showNotice("Wi-Fi", "Disabled") + } + } + } + Component.onCompleted: { Logger.log("Network", "Service initialized") syncWifiState() @@ -57,16 +68,7 @@ Singleton { saveDebounce.restart() } - // Single refresh timer for periodic scans - Timer { - id: refreshTimer - interval: 30000 - running: true - repeat: true - onTriggered: refresh() - } - - // Delayed scan timer for WiFi enable + // Delayed scan timer Timer { id: delayedScanTimer interval: 7000 @@ -100,6 +102,7 @@ Singleton { scanning = true lastError = "" scanProcess.running = true + Logger.log("Network", "Wi-Fi scan in progress...") } function connect(ssid, password = "") { @@ -330,6 +333,7 @@ Singleton { } // For logging purpose only + Logger.log("Network", "Wi-Fi scan completed") const oldSSIDs = Object.keys(root.networks) const newSSIDs = Object.keys(nets) const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid)) From 13111922357f13ef86e43d5b3867ba520d1c594e Mon Sep 17 00:00:00 2001 From: Never Gude Date: Sat, 6 Sep 2025 22:00:38 +0200 Subject: [PATCH 61/61] make NSpinBox border width not hardcoded --- Widgets/NSpinBox.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Widgets/NSpinBox.qml b/Widgets/NSpinBox.qml index 8049cf6..828aa56 100644 --- a/Widgets/NSpinBox.qml +++ b/Widgets/NSpinBox.qml @@ -44,7 +44,7 @@ RowLayout { radius: height * 0.5 // Fully rounded like toggle color: Color.mSurfaceVariant border.color: root.hovering ? Color.mPrimary : Color.mOutline - border.width: 1 + border.width: Math.max(1, Style.borderS * scaling) Behavior on border.color { ColorAnimation {