From c8860a3a9d328de0ff5ccb56ec4349507941e5f9 Mon Sep 17 00:00:00 2001 From: quadbyte Date: Thu, 28 Aug 2025 08:37:44 -0400 Subject: [PATCH 01/19] Volume/Bar: better touchpad support for volume inc/dec --- Modules/Bar/Widgets/Volume.qml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index e115102..fc2314c 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -14,10 +14,12 @@ Item { // Used to avoid opening the pill on Quickshell startup property bool firstVolumeReceived: false + property int wheelAccumulator: 0 implicitWidth: pill.width implicitHeight: pill.height + function getIcon() { if (AudioService.muted) { return "volume_off" @@ -59,10 +61,13 @@ Item { tooltipText: "Volume: " + Math.round( AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." - onWheel: function (angle) { - if (angle > 0) { + onWheel: function (delta) { + wheelAccumulator += delta + if (wheelAccumulator >= 120) { + wheelAccumulator = 0 AudioService.increaseVolume() - } else if (angle < 0) { + } else if (wheelAccumulator <= -120) { + wheelAccumulator = 0 AudioService.decreaseVolume() } } From cdc3b180712753d6a0f56915d10829a5e00a991e Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Thu, 28 Aug 2025 08:58:24 -0400 Subject: [PATCH 02/19] ArchUpdater: better icons --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 4 ++-- Modules/Bar/Widgets/ArchUpdater.qml | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index 3ce2656..c4c9539 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -190,7 +190,7 @@ NPanel { } NIconButton { - icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update" + icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "filter_none" tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages" enabled: !ArchUpdaterService.updateInProgress onClicked: { @@ -203,7 +203,7 @@ NPanel { } NIconButton { - icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings" + icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "check_box" tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages" enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0 onClicked: { diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 9047c1b..9fc9522 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -29,15 +29,11 @@ NIconButton { // Icon states icon: { - if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) + if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) { return "sync" + } if (ArchUpdaterService.totalUpdates > 0) { - const count = ArchUpdaterService.totalUpdates - if (count > 50) - return "system_update_alt" - if (count > 10) - return "system_update" - return "system_update" + return "filter_none" } return "task_alt" } From 2a686b55c4e495f2325c949ba822474b0102076c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 28 Aug 2025 15:34:47 +0200 Subject: [PATCH 03/19] Replace our NightLight solution with wlsunset. NightLight: add temperature solution NTextInput: add input hint support --- Commons/Settings.qml | 3 + Modules/Bar/Widgets/NightLight.qml | 23 ++-- Modules/NightLight/NightLightOverlay.qml | 46 -------- Modules/SettingsPanel/Tabs/DisplayTab.qml | 64 ++++++++++- README.md | 2 +- Services/NightLightService.qml | 129 ++++++++++++++-------- Widgets/NCheckbox.qml | 26 ++--- Widgets/NTextInput.qml | 1 + shell.qml | 3 +- 9 files changed, 167 insertions(+), 130 deletions(-) delete mode 100644 Modules/NightLight/NightLightOverlay.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 8f8c65f..8f4b74b 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -275,6 +275,9 @@ Singleton { property string startTime: "20:00" property string stopTime: "07:00" property bool autoSchedule: false + // wlsunset temperatures (Kelvin) + property int lowTemp: 3500 + property int highTemp: 6500 } } } diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 845e85f..5fe70da 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -17,21 +17,19 @@ Item { NPill { id: pill - icon: NightLightService.isActive ? "bedtime" : "bedtime_off" - iconCircleColor: NightLightService.isActive ? Color.mSecondary : Color.mOnSurfaceVariant - collapsedIconColor: NightLightService.isActive ? Color.mOnSecondary : Color.mOnSurface + icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off" + iconCircleColor: Settings.data.nightLight.enabled ? Color.mSecondary : Color.mOnSurfaceVariant + collapsedIconColor: Settings.data.nightLight.enabled ? Color.mOnSecondary : Color.mOnSurface autoHide: false - text: NightLightService.isActive ? "On" : "Off" + text: Settings.data.nightLight.enabled ? "On" : "Off" tooltipText: { if (!Settings.isLoaded || !Settings.data.nightLight.enabled) { return "Night Light: Disabled\nLeft click to open settings.\nRight click to enable." } - var status = NightLightService.isActive ? "Active" : "Inactive (outside schedule)" var intensity = Math.round(Settings.data.nightLight.intensity * 100) - var schedule = Settings.data.nightLight.autoSchedule ? `Schedule: ${Settings.data.nightLight.startTime} - ${Settings.data.nightLight.stopTime}` : "Manual mode" - - return `Intensity: ${intensity}%\n${schedule}\nLeft click to open settings.\nRight click to toggle.` + var schedule = Settings.data.nightLight.autoSchedule ? `Auto schedule` : `Manual: ${Settings.data.nightLight.startTime} - ${Settings.data.nightLight.stopTime}` + return `Night Light: Enabled\nIntensity: ${intensity}%\n${schedule}\nLeft click to open settings.\nRight click to toggle.` } onClicked: { @@ -42,14 +40,11 @@ Item { } onRightClicked: { - // Right click - toggle night light + // Right click - toggle night light (debounced apply handled by service) Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled + NightLightService.apply() } - onWheel: delta => { - var diff = delta > 0 ? 0.05 : -0.05 - Settings.data.nightLight.intensity = Math.max(0, Math.min(1.0, - Settings.data.nightLight.intensity + diff)) - } + // Wheel handler removed to avoid frequent rapid restarts/flicker } } diff --git a/Modules/NightLight/NightLightOverlay.qml b/Modules/NightLight/NightLightOverlay.qml deleted file mode 100644 index 8bb74bc..0000000 --- a/Modules/NightLight/NightLightOverlay.qml +++ /dev/null @@ -1,46 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Wayland -import qs.Commons -import qs.Services - -Variants { - model: Quickshell.screens - - delegate: Loader { - required property ShellScreen modelData - readonly property real scaling: ScalingService.scale(modelData) - - active: NightLightService.isActive - - sourceComponent: PanelWindow { - screen: modelData - color: Color.transparent - anchors { - top: true - bottom: true - left: true - right: true - } - - // Ensure a full click through - mask: Region {} - - WlrLayershell.layer: WlrLayershell.Overlay - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - WlrLayershell.namespace: "noctalia-nightlight" - - Rectangle { - anchors.fill: parent - color: NightLightService.overlayColor - - Behavior on color { - ColorAnimation { - duration: Style.animationSlow - } - } - } - } - } -} diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index 3236bcb..7f2c217 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -239,7 +239,10 @@ ColumnLayout { label: "Enable Night Light" description: "Apply a warm color filter to reduce blue light emission." checked: Settings.data.nightLight.enabled - onToggled: checked => Settings.data.nightLight.enabled = checked + onToggled: checked => { + Settings.data.nightLight.enabled = checked + NightLightService.apply() + } } // Intensity settings @@ -257,7 +260,10 @@ ColumnLayout { to: 1 stepSize: 0.01 value: Settings.data.nightLight.intensity - onMoved: Settings.data.nightLight.intensity = value + onMoved: { + Settings.data.nightLight.intensity = value + NightLightService.apply() + } Layout.fillWidth: true Layout.minimumWidth: 150 * scaling } @@ -271,11 +277,59 @@ ColumnLayout { } } + // Temperature settings (inline like schedule) + RowLayout { + visible: Settings.data.nightLight.enabled + Layout.fillWidth: false + spacing: Style.marginM * scaling + + NText { + text: "Low" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + } + NTextInput { + text: Settings.data.nightLight.lowTemp.toString() + inputMethodHints: Qt.ImhDigitsOnly + Layout.preferredWidth: 100 * scaling + onEditingFinished: { + var v = parseInt(text) + if (!isNaN(v)) { + Settings.data.nightLight.lowTemp = Math.max(1000, Math.min(6500, v)) + NightLightService.apply() + } + } + } + + Item {} + + NText { + text: "High" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + } + NTextInput { + text: Settings.data.nightLight.highTemp.toString() + inputMethodHints: Qt.ImhDigitsOnly + Layout.preferredWidth: 100 * scaling + onEditingFinished: { + var v = parseInt(text) + if (!isNaN(v)) { + Settings.data.nightLight.highTemp = Math.max(1000, Math.min(10000, v)) + NightLightService.apply() + } + } + } + } + NToggle { label: "Auto Schedule" description: "Automatically enable night light based on time schedule." checked: Settings.data.nightLight.autoSchedule - onToggled: checked => Settings.data.nightLight.autoSchedule = checked + onToggled: checked => { + Settings.data.nightLight.autoSchedule = checked + NightLightService.apply() + } visible: Settings.data.nightLight.enabled } @@ -303,7 +357,7 @@ ColumnLayout { model: timeOptions currentKey: Settings.data.nightLight.startTime placeholder: "Select start time" - onSelected: key => Settings.data.nightLight.startTime = key + onSelected: key => { Settings.data.nightLight.startTime = key; NightLightService.apply() } preferredWidth: 120 * scaling } @@ -319,7 +373,7 @@ ColumnLayout { model: timeOptions currentKey: Settings.data.nightLight.stopTime placeholder: "Select stop time" - onSelected: key => Settings.data.nightLight.stopTime = key + onSelected: key => { Settings.data.nightLight.stopTime = key; NightLightService.apply() } preferredWidth: 120 * scaling } } diff --git a/README.md b/README.md index f8649cf..916b2f5 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,6 @@ Features a modern modular architecture with a status bar, notification system, c - `gpu-screen-recorder` - Screen recording functionality - `brightnessctl` - For internal/laptop monitor brightness - `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors) -- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal) ### Optional @@ -79,6 +78,7 @@ Features a modern modular architecture with a status bar, notification system, c - `swww` - Wallpaper animations and effects - `matugen` - Material You color scheme generation - `cava` - Audio visualizer component +- `wlsunset` - To be able to use NightLight > There are 2 more optional dependencies. > Any `polkit agent` to be able to use the ArchUpdater widget. diff --git a/Services/NightLightService.qml b/Services/NightLightService.qml index f94e409..d2fe5a8 100644 --- a/Services/NightLightService.qml +++ b/Services/NightLightService.qml @@ -4,63 +4,106 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons +import qs.Services Singleton { id: root // Night Light properties - directly bound to settings readonly property var params: Settings.data.nightLight + // Deprecated overlay flag removed; service only manages wlsunset now + property bool isActive: false + property bool isRunning: false + property string lastCommand: "" + property var nextCommand: [] - // Computed properties - readonly property color overlayColor: params.enabled ? calculateOverlayColor() : "transparent" - property bool isActive: params.enabled && (params.autoSchedule ? isWithinSchedule() : true) + Component.onCompleted: apply() - Component.onCompleted: { - Logger.log("NightLight", "Service started") - } - - function calculateOverlayColor() { - if (!isActive) { - return "transparent" - } - - // More vibrant color formula - stronger effect at high warmth - var red = 1.0 - var green = 1.0 - (0.43 * params.intensity) - var blue = 1.0 - (0.84 * params.intensity) - var alpha = (params.intensity * 0.25) // Higher alpha for more noticeable effect - - return Qt.rgba(red, green, blue, alpha) - } - - function isWithinSchedule() { - if (!params.autoSchedule) { - return true - } - - var now = new Date() - var currentTime = now.getHours() * 60 + now.getMinutes() - - var startParts = params.startTime.split(":") - var stopParts = params.stopTime.split(":") - var startMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1]) - var stopMinutes = parseInt(stopParts[0]) * 60 + parseInt(stopParts[1]) - - // Handle overnight schedule (e.g., 20:00 to 07:00) - if (stopMinutes < startMinutes) { - return currentTime >= startMinutes || currentTime <= stopMinutes + function buildCommand() { + var cmd = ["wlsunset"] + // Use user-configured temps; if intensity is used, bias lowTemp towards user low + var i = Math.max(0, Math.min(1, params.intensity)) + var loCfg = params.lowTemp || 3500 + var hiCfg = params.highTemp || 6500 + var lowTemp = Math.round(hiCfg - (hiCfg - loCfg) * Math.pow(i, 0.6)) + cmd.push("-t", lowTemp.toString()) + cmd.push("-T", hiCfg.toString()) + if (params.autoSchedule && LocationService.data.coordinatesReady && LocationService.data.stableLatitude !== "" && LocationService.data.stableLongitude !== "") { + cmd.push("-l", LocationService.data.stableLatitude) + cmd.push("-L", LocationService.data.stableLongitude) } else { - return currentTime >= startMinutes && currentTime <= stopMinutes + // Manual schedule + if (params.startTime && params.stopTime) { + cmd.push("-S", params.startTime) + cmd.push("-s", params.stopTime) + } + // Optional: do not pass duration, use wlsunset defaults } + return cmd } - // Timer to check schedule changes + function stopIfRunning() { + // Best-effort stop; wlsunset runs as foreground, so pkill is simplest + Quickshell.execDetached(["pkill", "-x", "wlsunset"]) + isRunning = false + } + + function apply() { + if (!params.enabled) { + // Disable immediately + debounceStart.stop() + nextCommand = [] + stopIfRunning() + return + } + // Debounce rapid changes (slider) + nextCommand = buildCommand() + lastCommand = nextCommand.join(" ") + stopIfRunning() + debounceStart.restart() + } + + // Observe setting changes and location readiness + Connections { + target: Settings.data.nightLight + function onEnabledChanged() { apply() } + function onIntensityChanged() { apply() } + function onAutoScheduleChanged() { apply() } + function onStartTimeChanged() { apply() } + function onStopTimeChanged() { apply() } + } + + Connections { + target: LocationService.data + function onCoordinatesReadyChanged() { if (params.enabled && params.autoSchedule) apply() } + function onStableLatitudeChanged() { if (params.enabled && params.autoSchedule) apply() } + function onStableLongitudeChanged() { if (params.enabled && params.autoSchedule) apply() } + } + + // Foreground process runner + Process { + id: runner + running: false + onStarted: { isRunning = true; Logger.log("NightLight", "Started wlsunset:", root.lastCommand) } + onExited: function (code, status) { + isRunning = false + Logger.log("NightLight", "wlsunset exited:", code, status) + // Do not auto-restart here; debounceStart handles starts + } + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Debounce timer to avoid flicker when moving sliders Timer { - interval: 60000 // Check every minute - running: params.enabled && params.autoSchedule - repeat: true + id: debounceStart + interval: 300 + repeat: false onTriggered: { - isActive = isWithinSchedule() + if (params.enabled && nextCommand.length > 0) { + runner.command = nextCommand + runner.running = true + } } } } diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index ee2beaf..3b85d0d 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -12,7 +12,7 @@ RowLayout { property bool checked: false property bool hovering: false // Smaller default footprint than NToggle - property int baseSize: Math.max(Style.baseWidgetSize * 0.8, Math.round(14 / scaling)) + property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14) signal toggled(bool checked) signal entered @@ -47,26 +47,14 @@ RowLayout { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: { - hovering = true - root.entered() - } - onExited: { - hovering = false - root.exited() - } + onEntered: { hovering = true; root.entered() } + onExited: { hovering = false; root.exited() } onClicked: root.toggled(!root.checked) } - Behavior on color { - ColorAnimation { - duration: Style.animationFast - } - } - Behavior on border.color { - ColorAnimation { - duration: Style.animationFast - } - } + Behavior on color { ColorAnimation { duration: Style.animationFast } } + Behavior on border.color { ColorAnimation { duration: Style.animationFast } } } } + + diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index bc1a955..6fd56a9 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -14,6 +14,7 @@ Item { property alias text: input.text property alias placeholderText: input.placeholderText + property alias inputMethodHints: input.inputMethodHints signal editingFinished diff --git a/shell.qml b/shell.qml index c62e8a2..4b5f314 100644 --- a/shell.qml +++ b/shell.qml @@ -21,7 +21,6 @@ import qs.Modules.Calendar import qs.Modules.Dock import qs.Modules.IPC import qs.Modules.LockScreen -import qs.Modules.NightLight import qs.Modules.Notification import qs.Modules.SettingsPanel import qs.Modules.PowerPanel @@ -51,7 +50,7 @@ ShellRoot { ToastOverlay {} - NightLightOverlay {} + // Night light handled by wlsunset via NightLightService; no overlay needed IPCManager {} From d0b7ccf302b6c7cb1189d6f55c0f11d9b7a548a7 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 28 Aug 2025 15:35:52 +0200 Subject: [PATCH 04/19] Autoformat --- Modules/Bar/Widgets/Volume.qml | 1 - Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 31 +++++++------ Modules/SettingsPanel/Tabs/DisplayTab.qml | 22 +++++---- Services/NightLightService.qml | 45 ++++++++++++++----- Widgets/NCheckbox.qml | 24 +++++++--- shell.qml | 1 - 6 files changed, 81 insertions(+), 43 deletions(-) diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index fc2314c..c3c8c3d 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -19,7 +19,6 @@ Item { implicitWidth: pill.width implicitHeight: pill.height - function getIcon() { if (AudioService.muted) { return "volume_off" diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index ee2f739..7ada0bb 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -344,25 +344,24 @@ ColumnLayout { visible: Settings.data.colorSchemes.useWallpaperColors ColumnLayout { - spacing: Style.marginS * scaling - Layout.fillWidth: true + spacing: Style.marginS * scaling + Layout.fillWidth: true - NText { - text: "Matugen Templates" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } - - NText { - text: "Select which external components Matugen should apply theming to." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - Layout.fillWidth: true - wrapMode: Text.WordWrap - } + NText { + text: "Matugen Templates" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary } + NText { + text: "Select which external components Matugen should apply theming to." + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + } NCheckbox { label: "GTK 4 (libadwaita)" diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index 7f2c217..bbb7da8 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -240,9 +240,9 @@ ColumnLayout { description: "Apply a warm color filter to reduce blue light emission." checked: Settings.data.nightLight.enabled onToggled: checked => { - Settings.data.nightLight.enabled = checked - NightLightService.apply() - } + Settings.data.nightLight.enabled = checked + NightLightService.apply() + } } // Intensity settings @@ -327,9 +327,9 @@ ColumnLayout { description: "Automatically enable night light based on time schedule." checked: Settings.data.nightLight.autoSchedule onToggled: checked => { - Settings.data.nightLight.autoSchedule = checked - NightLightService.apply() - } + Settings.data.nightLight.autoSchedule = checked + NightLightService.apply() + } visible: Settings.data.nightLight.enabled } @@ -357,7 +357,10 @@ ColumnLayout { model: timeOptions currentKey: Settings.data.nightLight.startTime placeholder: "Select start time" - onSelected: key => { Settings.data.nightLight.startTime = key; NightLightService.apply() } + onSelected: key => { + Settings.data.nightLight.startTime = key + NightLightService.apply() + } preferredWidth: 120 * scaling } @@ -373,7 +376,10 @@ ColumnLayout { model: timeOptions currentKey: Settings.data.nightLight.stopTime placeholder: "Select stop time" - onSelected: key => { Settings.data.nightLight.stopTime = key; NightLightService.apply() } + onSelected: key => { + Settings.data.nightLight.stopTime = key + NightLightService.apply() + } preferredWidth: 120 * scaling } } diff --git a/Services/NightLightService.qml b/Services/NightLightService.qml index d2fe5a8..4395b76 100644 --- a/Services/NightLightService.qml +++ b/Services/NightLightService.qml @@ -28,7 +28,8 @@ Singleton { var lowTemp = Math.round(hiCfg - (hiCfg - loCfg) * Math.pow(i, 0.6)) cmd.push("-t", lowTemp.toString()) cmd.push("-T", hiCfg.toString()) - if (params.autoSchedule && LocationService.data.coordinatesReady && LocationService.data.stableLatitude !== "" && LocationService.data.stableLongitude !== "") { + if (params.autoSchedule && LocationService.data.coordinatesReady && LocationService.data.stableLatitude !== "" + && LocationService.data.stableLongitude !== "") { cmd.push("-l", LocationService.data.stableLatitude) cmd.push("-L", LocationService.data.stableLongitude) } else { @@ -44,7 +45,7 @@ Singleton { function stopIfRunning() { // Best-effort stop; wlsunset runs as foreground, so pkill is simplest - Quickshell.execDetached(["pkill", "-x", "wlsunset"]) + Quickshell.execDetached(["pkill", "-x", "wlsunset"]) isRunning = false } @@ -66,25 +67,47 @@ Singleton { // Observe setting changes and location readiness Connections { target: Settings.data.nightLight - function onEnabledChanged() { apply() } - function onIntensityChanged() { apply() } - function onAutoScheduleChanged() { apply() } - function onStartTimeChanged() { apply() } - function onStopTimeChanged() { apply() } + function onEnabledChanged() { + apply() + } + function onIntensityChanged() { + apply() + } + function onAutoScheduleChanged() { + apply() + } + function onStartTimeChanged() { + apply() + } + function onStopTimeChanged() { + apply() + } } Connections { target: LocationService.data - function onCoordinatesReadyChanged() { if (params.enabled && params.autoSchedule) apply() } - function onStableLatitudeChanged() { if (params.enabled && params.autoSchedule) apply() } - function onStableLongitudeChanged() { if (params.enabled && params.autoSchedule) apply() } + function onCoordinatesReadyChanged() { + if (params.enabled && params.autoSchedule) + apply() + } + function onStableLatitudeChanged() { + if (params.enabled && params.autoSchedule) + apply() + } + function onStableLongitudeChanged() { + if (params.enabled && params.autoSchedule) + apply() + } } // Foreground process runner Process { id: runner running: false - onStarted: { isRunning = true; Logger.log("NightLight", "Started wlsunset:", root.lastCommand) } + onStarted: { + isRunning = true + Logger.log("NightLight", "Started wlsunset:", root.lastCommand) + } onExited: function (code, status) { isRunning = false Logger.log("NightLight", "wlsunset exited:", code, status) diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index 3b85d0d..8bb3552 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -47,14 +47,26 @@ RowLayout { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true - onEntered: { hovering = true; root.entered() } - onExited: { hovering = false; root.exited() } + onEntered: { + hovering = true + root.entered() + } + onExited: { + hovering = false + root.exited() + } onClicked: root.toggled(!root.checked) } - Behavior on color { ColorAnimation { duration: Style.animationFast } } - Behavior on border.color { ColorAnimation { duration: Style.animationFast } } + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } } } - - diff --git a/shell.qml b/shell.qml index 4b5f314..99d9ac4 100644 --- a/shell.qml +++ b/shell.qml @@ -51,7 +51,6 @@ ShellRoot { ToastOverlay {} // Night light handled by wlsunset via NightLightService; no overlay needed - IPCManager {} // ------------------------------ From 6c4b495a758894250c733ef0ac1623c4ff41da54 Mon Sep 17 00:00:00 2001 From: wer-zen Date: Thu, 28 Aug 2025 16:53:34 +0200 Subject: [PATCH 05/19] readme_fix3 --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 916b2f5..14ccde6 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ Alternatively, you can add it to your NixOS configuration or flake: noctalia = { url = "github:noctalia-dev/noctalia-shell"; inputs.nixpkgs.follows = "nixpkgs"; + inputs.quickshell.follows = "quickshell" }; quickshell = { @@ -156,10 +157,7 @@ Alternatively, you can add it to your NixOS configuration or flake: }; outputs = { self, nixpkgs, noctalia, quickshell, ... }: - let - system = "x86_64-linux"; - pkgs = import nixpkgs { inherit system; }; - in { + { nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { modules = [ ./configuration.nix @@ -173,8 +171,8 @@ Alternatively, you can add it to your NixOS configuration or flake: ```nix { environment.systemPackages = with pkgs; [ - noctalia.packages.${system}.default - quickshell.packages.${system}.default + inputs.noctalia.packages.${system}.default + inputs.quickshell.packages.${system}.default ]; } ``` From d57092feae7c2809660fa5a724e215d3968e4a87 Mon Sep 17 00:00:00 2001 From: wer-zen Date: Thu, 28 Aug 2025 16:54:08 +0200 Subject: [PATCH 06/19] readme_fix3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14ccde6..6647ffa 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Features a modern modular architecture with a status bar, notification system, c - `gpu-screen-recorder` - Screen recording functionality - `brightnessctl` - For internal/laptop monitor brightness - `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors) +- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal) ### Optional @@ -78,7 +79,6 @@ Features a modern modular architecture with a status bar, notification system, c - `swww` - Wallpaper animations and effects - `matugen` - Material You color scheme generation - `cava` - Audio visualizer component -- `wlsunset` - To be able to use NightLight > There are 2 more optional dependencies. > Any `polkit agent` to be able to use the ArchUpdater widget. From 92b24c6eb2aa2299987f8dc0d28a78df0c126e37 Mon Sep 17 00:00:00 2001 From: wer-zen Date: Thu, 28 Aug 2025 16:59:36 +0200 Subject: [PATCH 07/19] readme_fix4 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6647ffa..518cc87 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,6 @@ Features a modern modular architecture with a status bar, notification system, c - `gpu-screen-recorder` - Screen recording functionality - `brightnessctl` - For internal/laptop monitor brightness - `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors) -- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal) ### Optional @@ -79,6 +78,7 @@ Features a modern modular architecture with a status bar, notification system, c - `swww` - Wallpaper animations and effects - `matugen` - Material You color scheme generation - `cava` - Audio visualizer component +- `wlsunset` - To be able to use NightLight > There are 2 more optional dependencies. > Any `polkit agent` to be able to use the ArchUpdater widget. @@ -147,17 +147,17 @@ Alternatively, you can add it to your NixOS configuration or flake: noctalia = { url = "github:noctalia-dev/noctalia-shell"; inputs.nixpkgs.follows = "nixpkgs"; - inputs.quickshell.follows = "quickshell" }; quickshell = { url = "github:outfoxxed/quickshell"; inputs.nixpkgs.follows = "nixpkgs"; + inputs.quickshell.follows = "quickshell" }; }; outputs = { self, nixpkgs, noctalia, quickshell, ... }: - { + { nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { modules = [ ./configuration.nix From 82d71d65fa0616c5e40428b56e985dbf1e9528b3 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 28 Aug 2025 17:03:28 +0200 Subject: [PATCH 08/19] Replaced the old checkboxes in ArchUpdaterPanel with NCheckbox ArchUpdater: use NCheckbox to make things more uniform --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 16 +++++++++------- Widgets/NCheckbox.qml | 9 ++++++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index c4c9539..67b6827 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -106,14 +106,16 @@ NPanel { anchors.margins: Style.marginS * scaling spacing: Style.marginS * scaling - // Checkbox for selection (pure bindings; no imperative state) - NIconButton { + // Checkbox for selection + NCheckbox { id: checkbox - icon: ArchUpdaterService.isPackageSelected(modelData.name) ? "check_box" : "check_box_outline_blank" - onClicked: ArchUpdaterService.togglePackageSelection(modelData.name) - colorBg: Color.transparent - colorFg: ArchUpdaterService.isPackageSelected( - modelData.name) ? ((modelData.source === "aur") ? Color.mSecondary : Color.mPrimary) : Color.mOnSurfaceVariant + label: "" + description: "" + checked: (ArchUpdaterService.selectedPackagesCount, ArchUpdaterService.isPackageSelected(modelData.name)) + onToggled: ArchUpdaterService.togglePackageSelection(modelData.name) + activeColor: (modelData.source === "aur") ? Color.mSecondary : Color.mPrimary + activeOnColor: (modelData.source === "aur") ? Color.mOnSecondary : Color.mOnPrimary + Layout.fillWidth: false Layout.preferredWidth: 30 * scaling Layout.preferredHeight: 30 * scaling } diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index 8bb3552..0f0a8e9 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -11,6 +11,9 @@ RowLayout { property string description: "" property bool checked: false property bool hovering: false + // Active state colors (allow override per-usage) + property color activeColor: Color.mPrimary + property color activeOnColor: Color.mOnPrimary // Smaller default footprint than NToggle property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14) @@ -31,15 +34,15 @@ RowLayout { implicitWidth: root.baseSize * scaling implicitHeight: root.baseSize * scaling radius: Math.max(2 * scaling, Style.radiusXS * scaling) - color: root.checked ? Color.mPrimary : Color.mSurface - border.color: root.checked ? Color.mPrimary : Color.mOutline + color: root.checked ? root.activeColor : Color.mSurface + border.color: root.checked ? root.activeColor : Color.mOutline border.width: Math.max(1, Style.borderM * scaling) NIcon { visible: root.checked anchors.centerIn: parent text: "check" - color: Color.mOnPrimary + color: root.activeOnColor font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling } From a719db4d0d59193b1262602700059befa209c797 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Thu, 28 Aug 2025 11:06:31 -0400 Subject: [PATCH 09/19] better comments --- shell.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shell.qml b/shell.qml index 99d9ac4..0d85497 100644 --- a/shell.qml +++ b/shell.qml @@ -50,11 +50,10 @@ ShellRoot { ToastOverlay {} - // Night light handled by wlsunset via NightLightService; no overlay needed IPCManager {} // ------------------------------ - // All the panels + // All the NPanels Launcher { id: launcherPanel objectName: "launcherPanel" From 39d8d8bcfa8290463235a2ceb50b2282cbe0be89 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 28 Aug 2025 17:06:53 +0200 Subject: [PATCH 10/19] Added a check to see if wlsunset is enabled, if it isn't you can change the NightLight settings. DisplayTab: add wlsunsetCheck process --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 2 +- Modules/SettingsPanel/Tabs/DisplayTab.qml | 32 +++++++++++++++++-- Widgets/NCheckbox.qml | 2 -- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index 67b6827..f1cb5eb 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -111,7 +111,7 @@ NPanel { id: checkbox label: "" description: "" - checked: (ArchUpdaterService.selectedPackagesCount, ArchUpdaterService.isPackageSelected(modelData.name)) + checked: (ArchUpdaterService.selectedPackagesCountArchUpdaterService.isPackageSelected(modelData.name)) onToggled: ArchUpdaterService.togglePackageSelection(modelData.name) activeColor: (modelData.source === "aur") ? Color.mSecondary : Color.mPrimary activeOnColor: (modelData.source === "aur") ? Color.mOnSecondary : Color.mOnPrimary diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index bbb7da8..f7ea808 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import Quickshell +import Quickshell.Io import qs.Commons import qs.Services import qs.Widgets @@ -27,6 +28,27 @@ ColumnLayout { } } + // Check for wlsunset availability when enabling Night Light + Process { + id: wlsunsetCheck + command: ["which", "wlsunset"] + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + Settings.data.nightLight.enabled = true + NightLightService.apply() + ToastService.showNotice("Night Light", "Enabled") + } else { + Settings.data.nightLight.enabled = false + ToastService.showWarning("Night Light", "wlsunset not installed") + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + // Helper functions to update arrays immutably function addMonitor(list, name) { const arr = (list || []).slice() @@ -240,8 +262,14 @@ ColumnLayout { description: "Apply a warm color filter to reduce blue light emission." checked: Settings.data.nightLight.enabled onToggled: checked => { - Settings.data.nightLight.enabled = checked - NightLightService.apply() + if (checked) { + // Verify wlsunset exists before enabling + wlsunsetCheck.running = true + } else { + Settings.data.nightLight.enabled = false + NightLightService.apply() + ToastService.showNotice("Night Light", "Disabled") + } } } diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index 0f0a8e9..24944e2 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -11,10 +11,8 @@ RowLayout { property string description: "" property bool checked: false property bool hovering: false - // Active state colors (allow override per-usage) property color activeColor: Color.mPrimary property color activeOnColor: Color.mOnPrimary - // Smaller default footprint than NToggle property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14) signal toggled(bool checked) From e86e7344f32ba0213365a2198b757eda34eaa899 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Thu, 28 Aug 2025 11:10:55 -0400 Subject: [PATCH 11/19] ArchUpdater: better icons (take2) --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 4 ++-- Modules/Bar/Widgets/ArchUpdater.qml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index f1cb5eb..609ee3f 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -34,7 +34,7 @@ NPanel { spacing: Style.marginM * scaling NIcon { - text: "system_update" + text: "system_update_alt" font.pointSize: Style.fontSizeXXL * scaling color: Color.mPrimary } @@ -192,7 +192,7 @@ NPanel { } NIconButton { - icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "filter_none" + icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update_alt" tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages" enabled: !ArchUpdaterService.updateInProgress onClicked: { diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 9fc9522..90cedb7 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -33,7 +33,7 @@ NIconButton { return "sync" } if (ArchUpdaterService.totalUpdates > 0) { - return "filter_none" + return "system_update_alt" } return "task_alt" } From 156146fd9a050f8af92c7dcd63ebb1502c4d63a9 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 28 Aug 2025 17:48:02 +0200 Subject: [PATCH 12/19] Add audio IPC options AudioService: add a few functions to AudioService IPCManager: Add 4 Audio IPC calls README: Add information about new IPC calls --- Modules/IPC/IPCManager.qml | 18 ++++++++++ Modules/SettingsPanel/Tabs/AudioTab.qml | 48 +++++++++++++++++++++++++ README.md | 4 +++ Services/AudioService.qml | 42 ++++++++++++++++++++++ 4 files changed, 112 insertions(+) diff --git a/Modules/IPC/IPCManager.qml b/Modules/IPC/IPCManager.qml index 78ed64b..771aea4 100644 --- a/Modules/IPC/IPCManager.qml +++ b/Modules/IPC/IPCManager.qml @@ -99,6 +99,24 @@ Item { } } + IpcHandler { + target: "volume" + function increase() { + AudioService.increaseVolume() + } + function decrease() { + AudioService.decreaseVolume() + } + function muteOutput() { + AudioService.setMuted(!AudioService.muted) + } + function muteInput() { + if (AudioService.source?.ready && AudioService.source?.audio) { + AudioService.source.audio.muted = !AudioService.source.audio.muted + } + } + } + IpcHandler { target: "powerPanel" function toggle() { diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index 1a6f4b9..a81c2c8 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -81,6 +81,54 @@ ColumnLayout { } } + // Input Volume + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginM * scaling + + NLabel { + label: "Input Volume" + description: "Microphone input volume level." + } + + RowLayout { + NSlider { + Layout.fillWidth: true + from: 0 + to: 1.0 + value: AudioService.inputVolume + stepSize: 0.01 + onMoved: { + AudioService.setInputVolume(value) + } + } + + NText { + text: Math.floor(AudioService.inputVolume * 100) + "%" + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS * scaling + color: Color.mOnSurface + } + } + } + + // Input Mute Toggle + ColumnLayout { + spacing: Style.marginS * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginM * scaling + + NToggle { + label: "Mute Audio Input" + description: "Mute or unmute the default audio input (microphone)." + checked: AudioService.inputMuted + onToggled: checked => { + AudioService.setInputMuted(checked) + } + } + } + // Volume Step Size ColumnLayout { spacing: Style.marginS * scaling diff --git a/README.md b/README.md index 518cc87..12ffa43 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,10 @@ The following commands apply to the Nix flake and also the AUR package installat | Open Calculator | `noctalia-shell ipc call launcher calculator` | | Increase Brightness | `noctalia-shell ipc call brightness increase` | | Decrease Brightness | `noctalia-shell ipc call brightness decrease` | +| Increase Output Volume | `noctalia-shell ipc call volume increase` | +| Decrease Output Volume | `noctalia-shell ipc call volume decrease` | +| Toggle Mute Audio Output | `noctalia-shell ipc call volume muteOutput` | +| Toggle Mute Audio Input | `noctalia-shell ipc call volume muteInput` | | Toggle Power Panel | `noctalia-shell ipc call powerPanel toggle` | | Toggle Idle Inhibitor | `noctalia-shell ipc call idleInhibitor toggle` | | Toggle Settings Window | `noctalia-shell ipc call settings toggle` | diff --git a/Services/AudioService.qml b/Services/AudioService.qml index 0dd6fc9..c6ec05c 100644 --- a/Services/AudioService.qml +++ b/Services/AudioService.qml @@ -35,6 +35,13 @@ Singleton { readonly property alias muted: root._muted property bool _muted: !!sink?.audio?.muted + // Input volume [0..1] is readonly from outside + readonly property alias inputVolume: root._inputVolume + property real _inputVolume: source?.audio?.volume ?? 0 + + readonly property alias inputMuted: root._inputMuted + property bool _inputMuted: !!source?.audio?.muted + readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0 PwObjectTracker { @@ -58,6 +65,23 @@ Singleton { } } + Connections { + target: source?.audio ? source?.audio : null + + function onVolumeChanged() { + var vol = (source?.audio.volume ?? 0) + if (isNaN(vol)) { + vol = 0 + } + root._inputVolume = vol + } + + function onMutedChanged() { + root._inputMuted = (source?.audio.muted ?? true) + Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted) + } + } + function increaseVolume() { setVolume(volume + stepVolume) } @@ -85,6 +109,24 @@ Singleton { } } + function setInputVolume(newVolume: real) { + if (source?.ready && source?.audio) { + // Clamp it accordingly + source.audio.muted = false + source.audio.volume = Math.max(0, Math.min(1, newVolume)) + } else { + Logger.warn("AudioService", "No source available") + } + } + + function setInputMuted(muted: bool) { + if (source?.ready && source?.audio) { + source.audio.muted = muted + } else { + Logger.warn("AudioService", "No source available") + } + } + function setAudioSink(newSink: PwNode): void { Pipewire.preferredDefaultAudioSink = newSink } From 3f4cec171947b046e1e9f3238aecc22a7bb288dd Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Thu, 28 Aug 2025 12:22:42 -0400 Subject: [PATCH 13/19] NTextInput: improved layout and adapted calling code all over the shell. --- Modules/SettingsPanel/SettingsPanel.qml | 1 + Modules/SettingsPanel/Tabs/AudioTab.qml | 4 - Modules/SettingsPanel/Tabs/DisplayTab.qml | 89 +++++++++-------- Modules/SettingsPanel/Tabs/GeneralTab.qml | 3 +- .../SettingsPanel/Tabs/ScreenRecorderTab.qml | 3 +- Modules/SettingsPanel/Tabs/TimeWeatherTab.qml | 2 + Modules/SettingsPanel/Tabs/WallpaperTab.qml | 14 +-- Widgets/NTextInput.qml | 95 +++++++++---------- 8 files changed, 106 insertions(+), 105 deletions(-) diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index a1992f9..440daec 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -301,6 +301,7 @@ NPanel { ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded padding: Style.marginL * scaling + clip: true Loader { active: true diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index 1a6f4b9..cce7a0b 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -216,8 +216,6 @@ ColumnLayout { } // Preferred player (persistent) NTextInput { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop label: "Preferred Player" description: "Substring to match MPRIS player (identity/bus/desktop)." placeholderText: "e.g. spotify, vlc, mpv" @@ -239,8 +237,6 @@ ColumnLayout { NTextInput { id: blacklistInput - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop label: "Blacklist player" description: "Substring, e.g. plex, shim, mpv." placeholderText: "type substring and press +" diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index f7ea808..eb9a766 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -74,7 +74,6 @@ ColumnLayout { color: Color.mOnSurfaceVariant wrapMode: Text.WordWrap Layout.fillWidth: true - Layout.preferredWidth: parent.width - (Style.marginL * 2 * scaling) } ColumnLayout { @@ -253,7 +252,6 @@ ColumnLayout { color: Color.mOnSurfaceVariant wrapMode: Text.WordWrap Layout.fillWidth: true - Layout.preferredWidth: parent.width - (Style.marginL * 2 * scaling) } } @@ -278,7 +276,7 @@ ColumnLayout { visible: Settings.data.nightLight.enabled NLabel { label: "Intensity" - description: "Higher values create warmer light." + description: "Higher values create warmer tones." } RowLayout { spacing: Style.marginS * scaling @@ -305,46 +303,61 @@ ColumnLayout { } } - // Temperature settings (inline like schedule) - RowLayout { - visible: Settings.data.nightLight.enabled - Layout.fillWidth: false - spacing: Style.marginM * scaling - - NText { - text: "Low" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant + // Temperature + ColumnLayout { + spacing: Style.marginXS * scaling + Layout.alignment: Qt.AlignVCenter + + NLabel { + label: "Color temperature" + description: "Select two temperatures in Kelvin" } - NTextInput { - text: Settings.data.nightLight.lowTemp.toString() - inputMethodHints: Qt.ImhDigitsOnly - Layout.preferredWidth: 100 * scaling - onEditingFinished: { - var v = parseInt(text) - if (!isNaN(v)) { - Settings.data.nightLight.lowTemp = Math.max(1000, Math.min(6500, v)) - NightLightService.apply() + + RowLayout { + visible: Settings.data.nightLight.enabled + spacing: Style.marginM * scaling + Layout.fillWidth: false + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + + NText { + text: "Low" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + + NTextInput { + text: Settings.data.nightLight.lowTemp.toString() + inputMethodHints: Qt.ImhDigitsOnly + Layout.alignment: Qt.AlignVCenter + onEditingFinished: { + var v = parseInt(text) + if (!isNaN(v)) { + Settings.data.nightLight.lowTemp = Math.max(1000, Math.min(6500, v)) + NightLightService.apply() + } } } - } - Item {} + Item {} - NText { - text: "High" - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - } - NTextInput { - text: Settings.data.nightLight.highTemp.toString() - inputMethodHints: Qt.ImhDigitsOnly - Layout.preferredWidth: 100 * scaling - onEditingFinished: { - var v = parseInt(text) - if (!isNaN(v)) { - Settings.data.nightLight.highTemp = Math.max(1000, Math.min(10000, v)) - NightLightService.apply() + NText { + text: "High" + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + NTextInput { + text: Settings.data.nightLight.highTemp.toString() + inputMethodHints: Qt.ImhDigitsOnly + Layout.alignment: Qt.AlignVCenter + onEditingFinished: { + var v = parseInt(text) + if (!isNaN(v)) { + Settings.data.nightLight.highTemp = Math.max(1000, Math.min(10000, v)) + NightLightService.apply() + } } } } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 473a5ce..b80faa2 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -25,10 +25,9 @@ ColumnLayout { NTextInput { label: "Profile Picture" - description: "Your profile picture displayed in various places throughout the shell." + description: "Your profile picture that appears throughout the interface." text: Settings.data.general.avatarImage placeholderText: "/home/user/.face" - Layout.fillWidth: true onEditingFinished: { Settings.data.general.avatarImage = text } diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index 2a000c1..900b971 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -24,7 +24,8 @@ ColumnLayout { onEditingFinished: { Settings.data.screenRecorder.directory = text } - Layout.fillWidth: true + + Layout.maximumWidth: 420 * scaling } ColumnLayout { diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml index 519177f..50ea298 100644 --- a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml @@ -10,6 +10,7 @@ ColumnLayout { // Location section RowLayout { + Layout.fillWidth: true spacing: Style.marginL * scaling NTextInput { @@ -25,6 +26,7 @@ ColumnLayout { LocationService.resetWeather() } } + Layout.maximumWidth: 420 * scaling } NText { diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 6f85cb0..e96b5ef 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -35,10 +35,10 @@ ColumnLayout { label: "Wallpaper Directory" description: "Path to your wallpaper directory." text: Settings.data.wallpaper.directory - Layout.fillWidth: true onEditingFinished: { Settings.data.wallpaper.directory = text } + Layout.maximumWidth: 420 * scaling } NDivider { @@ -79,12 +79,7 @@ ColumnLayout { NText { // Show friendly H:MM format from current settings - text: { - const s = Settings.data.wallpaper.randomInterval - const h = Math.floor(s / 3600) - const m = Math.floor((s % 3600) / 60) - return (h > 0 ? (h + "h ") : "") + (m > 0 ? (m + "m") : (h === 0 ? "0m" : "")) - } + text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomInterval) Layout.alignment: Qt.AlignBottom | Qt.AlignRight } } @@ -284,14 +279,15 @@ ColumnLayout { NTextInput { label: "Custom Interval" - description: "Enter time as HH:MM (e.g., 1:30)." + description: "Enter time as HH:MM (e.g., 01:30)." + inputMaxWidth: 100 * scaling text: { const s = Settings.data.wallpaper.randomInterval const h = Math.floor(s / 3600) const m = Math.floor((s % 3600) / 60) return h + ":" + (m < 10 ? ("0" + m) : m) } - Layout.fillWidth: true + onEditingFinished: { const m = text.trim().match(/^(\d{1,2}):(\d{2})$/) if (m) { diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 6fd56a9..78fcd11 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -4,13 +4,14 @@ import QtQuick.Layouts import qs.Commons import qs.Services -Item { +ColumnLayout { id: root property string label: "" property string description: "" property bool readOnly: false property bool enabled: true + property int inputMaxWidth: 420 * scaling property alias text: input.text property alias placeholderText: input.placeholderText @@ -18,61 +19,53 @@ Item { signal editingFinished - // Sizing - implicitWidth: Style.sliderWidth * 1.6 * scaling - implicitHeight: Style.baseWidgetSize * 2.75 * scaling + spacing: Style.marginS * scaling + implicitHeight: frame.height - ColumnLayout { - spacing: Style.marginXXS * scaling - Layout.fillWidth: true + NLabel { + label: root.label + description: root.description + visible: root.label !== "" || root.description !== "" + } - NLabel { - label: root.label - description: root.description + // Container + Rectangle { + id: frame + implicitWidth: parent.width + implicitHeight: Style.baseWidgetSize * 1.1 * scaling + Layout.minimumWidth: 80 * scaling + Layout.maximumWidth: root.inputMaxWidth + radius: Style.radiusM * scaling + color: Color.mSurface + border.color: 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 } - // Container - Rectangle { - id: frame - Layout.topMargin: Style.marginXS * scaling - implicitWidth: root.width - implicitHeight: Style.baseWidgetSize * 1.35 * scaling - radius: Style.radiusM * scaling - color: Color.mSurface - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM * scaling + anchors.rightMargin: Style.marginM * scaling + spacing: Style.marginS * 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 - } - - RowLayout { - anchors.fill: parent - anchors.leftMargin: Style.marginM * scaling - anchors.rightMargin: Style.marginM * scaling - spacing: Style.marginS * scaling - - // Optional leading icon slot in the future - // Item { Layout.preferredWidth: 0 } - TextField { - id: input - Layout.fillWidth: true - echoMode: TextInput.Normal - readOnly: root.readOnly - enabled: root.enabled - color: Color.mOnSurface - placeholderTextColor: Color.mOnSurface - background: null - font.pointSize: Style.fontSizeXS * scaling - onEditingFinished: root.editingFinished() - // Text changes are observable via the aliased 'text' property (root.text) and its 'textChanged' signal. - // No additional callback is invoked here to avoid conflicts with QML's onTextChanged handler semantics. - } + TextField { + id: input + Layout.fillWidth: true + echoMode: TextInput.Normal + readOnly: root.readOnly + enabled: root.enabled + color: Color.mOnSurface + placeholderTextColor: Color.mOnSurfaceVariant + background: null + font.pointSize: Style.fontSizeS * scaling + onEditingFinished: root.editingFinished() } } } From 6ac172fe0239821a111cedd458d203c4751593e6 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 28 Aug 2025 19:17:50 +0200 Subject: [PATCH 14/19] ArchUpdaterPanel: Fix typo --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index 609ee3f..43203fc 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -111,7 +111,7 @@ NPanel { id: checkbox label: "" description: "" - checked: (ArchUpdaterService.selectedPackagesCountArchUpdaterService.isPackageSelected(modelData.name)) + checked: ArchUpdaterService.isPackageSelected(modelData.name) onToggled: ArchUpdaterService.togglePackageSelection(modelData.name) activeColor: (modelData.source === "aur") ? Color.mSecondary : Color.mPrimary activeOnColor: (modelData.source === "aur") ? Color.mOnSecondary : Color.mOnPrimary From cbd71bec4945aa68d9c08d059b6811a2e35e7a78 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Thu, 28 Aug 2025 19:48:20 +0200 Subject: [PATCH 15/19] Fix ArchUpdater NCheckbox binding ArchUpdater: Create proper binding, make selective update more robust --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 6 ++- Modules/SettingsPanel/Tabs/DisplayTab.qml | 6 +-- Services/ArchUpdaterService.qml | 52 ++++++++++++++----- Widgets/NTextInput.qml | 6 +-- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index 43203fc..9152ddd 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -112,7 +112,11 @@ NPanel { label: "" description: "" checked: ArchUpdaterService.isPackageSelected(modelData.name) - onToggled: ArchUpdaterService.togglePackageSelection(modelData.name) + onToggled: function (checked) { + ArchUpdaterService.togglePackageSelection(modelData.name) + // Force refresh of the checked property + checkbox.checked = ArchUpdaterService.isPackageSelected(modelData.name) + } activeColor: (modelData.source === "aur") ? Color.mSecondary : Color.mPrimary activeOnColor: (modelData.source === "aur") ? Color.mOnSecondary : Color.mOnPrimary Layout.fillWidth: false diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index eb9a766..5dfcbfe 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -303,11 +303,11 @@ ColumnLayout { } } - // Temperature + // Temperature ColumnLayout { spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - + NLabel { label: "Color temperature" description: "Select two temperatures in Kelvin" @@ -317,7 +317,7 @@ ColumnLayout { visible: Settings.data.nightLight.enabled spacing: Style.marginM * scaling Layout.fillWidth: false - Layout.fillHeight: true + Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter NText { diff --git a/Services/ArchUpdaterService.qml b/Services/ArchUpdaterService.qml index 84c8228..de3b3af 100644 --- a/Services/ArchUpdaterService.qml +++ b/Services/ArchUpdaterService.qml @@ -142,28 +142,40 @@ Singleton { return updateInProgress = true + // Split selected packages by source - const repoPkgs = selectedPackages.filter(pkg => { - const repoPkg = repoPackages.find(p => p.name === pkg) - return repoPkg && repoPkg.source === "repo" - }) - const aurPkgs = selectedPackages.filter(pkg => { - const aurPkg = aurPackages.find(p => p.name === pkg) - return aurPkg && aurPkg.source === "aur" - }) + const repoPkgs = [] + const aurPkgs = [] + + for (const pkgName of selectedPackages) { + const repoPkg = repoPackages.find(p => p.name === pkgName) + if (repoPkg && repoPkg.source === "repo") { + repoPkgs.push(pkgName) + } + + const aurPkg = aurPackages.find(p => p.name === pkgName) + if (aurPkg && aurPkg.source === "aur") { + aurPkgs.push(pkgName) + } + } // Update repo packages if (repoPkgs.length > 0) { const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs) + Logger.log("ArchUpdater", "Running repo command:", repoCommand.join(" ")) Quickshell.execDetached(repoCommand) } // Update AUR packages if (aurPkgs.length > 0) { - const aurCommand = ["sh", "-c", `command -v yay >/dev/null 2>&1 && yay -S ${aurPkgs.join( - ' ')} --noconfirm || command -v paru >/dev/null 2>&1 && paru -S ${aurPkgs.join( - ' ')} --noconfirm || true`] - Quickshell.execDetached(aurCommand) + const aurHelper = getAurHelper() + if (aurHelper) { + const aurCommand = [aurHelper, "-S", "--noconfirm"].concat(aurPkgs) + Logger.log("ArchUpdater", "Running AUR command:", aurCommand.join(" ")) + Quickshell.execDetached(aurCommand) + } else { + Logger.warn("ArchUpdater", "No AUR helper found for packages:", aurPkgs.join(", ")) + } } // Clear selection and refresh @@ -172,6 +184,22 @@ Singleton { refreshAfterUpdate() } + // Helper function to detect AUR helper + function getAurHelper() { + // Check for yay first, then paru + const yayCheck = Quickshell.exec("command -v yay", true) + if (yayCheck.exitCode === 0 && yayCheck.stdout.trim()) { + return "yay" + } + + const paruCheck = Quickshell.exec("command -v paru", true) + if (paruCheck.exitCode === 0 && paruCheck.stdout.trim()) { + return "paru" + } + + return null + } + // Package selection functions function togglePackageSelection(packageName) { const index = selectedPackages.indexOf(packageName) diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 78fcd11..6533af9 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -25,16 +25,16 @@ ColumnLayout { NLabel { label: root.label description: root.description - visible: root.label !== "" || root.description !== "" + visible: root.label !== "" || root.description !== "" } // Container Rectangle { id: frame implicitWidth: parent.width - implicitHeight: Style.baseWidgetSize * 1.1 * scaling + implicitHeight: Style.baseWidgetSize * 1.1 * scaling Layout.minimumWidth: 80 * scaling - Layout.maximumWidth: root.inputMaxWidth + Layout.maximumWidth: root.inputMaxWidth radius: Style.radiusM * scaling color: Color.mSurface border.color: Color.mOutline From bc28b117638f5d9d7fe8cdef120ed9be5fa14a2a Mon Sep 17 00:00:00 2001 From: Lemmy Date: Thu, 28 Aug 2025 14:33:03 -0400 Subject: [PATCH 16/19] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 12ffa43..b7d81b2 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,11 @@ Features a modern modular architecture with a status bar, notification system, c ## Preview -![Launcher](https://assets.noctalia.dev/screenshots/launcher.png) +![Launcher](https://assets.noctalia.dev/screenshots/launcher.png?v=2) -![SettingsPanel](https://assets.noctalia.dev/screenshots/settings-panel.png) +![SettingsPanel](https://assets.noctalia.dev/screenshots/settings-panel.png?v=2) -![SidePanel](https://assets.noctalia.dev/screenshots/light-mode.png) +![SidePanel](https://assets.noctalia.dev/screenshots/light-mode.png?v=2) --- From b2e9058a2f4d87108e5ceb55a31bc3669be05266 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Thu, 28 Aug 2025 15:01:23 -0400 Subject: [PATCH 17/19] Auto-formatting --- Modules/SettingsPanel/Tabs/DisplayTab.qml | 6 +++--- Widgets/NTextInput.qml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/DisplayTab.qml b/Modules/SettingsPanel/Tabs/DisplayTab.qml index eb9a766..5dfcbfe 100644 --- a/Modules/SettingsPanel/Tabs/DisplayTab.qml +++ b/Modules/SettingsPanel/Tabs/DisplayTab.qml @@ -303,11 +303,11 @@ ColumnLayout { } } - // Temperature + // Temperature ColumnLayout { spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - + NLabel { label: "Color temperature" description: "Select two temperatures in Kelvin" @@ -317,7 +317,7 @@ ColumnLayout { visible: Settings.data.nightLight.enabled spacing: Style.marginM * scaling Layout.fillWidth: false - Layout.fillHeight: true + Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter NText { diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 78fcd11..6533af9 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -25,16 +25,16 @@ ColumnLayout { NLabel { label: root.label description: root.description - visible: root.label !== "" || root.description !== "" + visible: root.label !== "" || root.description !== "" } // Container Rectangle { id: frame implicitWidth: parent.width - implicitHeight: Style.baseWidgetSize * 1.1 * scaling + implicitHeight: Style.baseWidgetSize * 1.1 * scaling Layout.minimumWidth: 80 * scaling - Layout.maximumWidth: root.inputMaxWidth + Layout.maximumWidth: root.inputMaxWidth radius: Style.radiusM * scaling color: Color.mSurface border.color: Color.mOutline From c3956c58946a54f8350495c4b5f8661e28914e45 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Thu, 28 Aug 2025 15:01:43 -0400 Subject: [PATCH 18/19] Bluetooth: revamped a lot of code --- Modules/Bar/Widgets/Bluetooth.qml | 11 +- .../BluetoothPanel/BluetoothDevicesList.qml | 262 +++++++++++++++ Modules/BluetoothPanel/BluetoothPanel.qml | 298 +++--------------- Services/BluetoothService.qml | 127 +++++--- 4 files changed, 379 insertions(+), 319 deletions(-) create mode 100644 Modules/BluetoothPanel/BluetoothDevicesList.qml diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index 38c9050..0b8b4f9 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -20,16 +20,7 @@ NIconButton { colorBorder: Color.transparent colorBorderHover: Color.transparent - icon: { - // Show different icons based on connection status - if (BluetoothService.pairedDevices.length > 0) { - return "bluetooth_connected" - } else if (BluetoothService.discovering) { - return "bluetooth_searching" - } else { - return "bluetooth" - } - } + icon: "bluetooth" tooltipText: "Bluetooth Devices" onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this) } diff --git a/Modules/BluetoothPanel/BluetoothDevicesList.qml b/Modules/BluetoothPanel/BluetoothDevicesList.qml new file mode 100644 index 0000000..9d4f981 --- /dev/null +++ b/Modules/BluetoothPanel/BluetoothDevicesList.qml @@ -0,0 +1,262 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +ColumnLayout { + id: root + + property string label: "" + property var model: { + + } + + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: root.label + font.pointSize: Style.fontSizeL * scaling + color: Color.mSecondary + font.weight: Style.fontWeightMedium + Layout.fillWidth: true + visible: root.model.length > 0 + } + + Repeater { + Layout.fillWidth: true + model: root.model + visible: BluetoothService.adapter && BluetoothService.adapter.enabled + + Rectangle { + property bool canConnect: BluetoothService.canConnect(modelData) + property bool isBusy: BluetoothService.isDeviceBusy(modelData) + + Layout.fillWidth: true + Layout.preferredHeight: 64 * scaling + (10 * scaling * modelData.batteryAvailable) + radius: Style.radiusM * scaling + + color: { + if (availableDeviceArea.containsMouse && !isBusy) + return Color.mTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mPrimary + + if (modelData.blocked) + return Color.mError + + return Color.mSurfaceVariant + } + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS * scaling) + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginM * scaling + spacing: Style.marginS * scaling + Layout.alignment: Qt.AlignVCenter + + // One device BT icon + NIcon { + text: BluetoothService.getDeviceIcon(modelData) + font.pointSize: Style.fontSizeXXL * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + Layout.alignment: Qt.AlignVCenter + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS * scaling + + // Device name + NText { + text: modelData.name || modelData.deviceName + font.pointSize: Style.fontSizeM * scaling + elide: Text.ElideRight + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + font.weight: Style.fontWeightMedium + Layout.fillWidth: true + } + + // Signal Strength + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS * scaling + + // Device signal strength - "Unknown" when not connected + NText { + text: BluetoothService.getSignalStrength(modelData) + font.pointSize: Style.fontSizeXS * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurfaceVariant + } + } + + NIcon { + text: BluetoothService.getSignalIcon(modelData) + font.pointSize: Style.fontSizeXS * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing + && !modelData.blocked + } + + NText { + text: (modelData.signalStrength !== undefined + && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" + font.pointSize: Style.fontSizeXS * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurface + } + visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing + && !modelData.blocked + } + } + + NText { + visible: modelData.batteryAvailable + text: BluetoothService.getBattery(modelData) + font.pointSize: Style.fontSizeXS * scaling + color: { + if (availableDeviceArea.containsMouse) + return Color.mOnTertiary + + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mOnPrimary + + if (modelData.blocked) + return Color.mOnError + + return Color.mOnSurfaceVariant + } + } + } + + // Spacer to push connect button to the right + Item { + Layout.fillWidth: true + } + + // Call to action + Rectangle { + Layout.preferredWidth: 80 * scaling + Layout.preferredHeight: 28 * scaling + radius: Style.radiusM * scaling + visible: (modelData.state !== BluetoothDeviceState.Connecting) + color: Color.transparent + + border.color: { + if (availableDeviceArea.containsMouse) { + return Color.mOnTertiary + } else { + return Color.mPrimary + } + } + border.width: Math.max(1, Style.borderS * scaling) + opacity: canConnect || isBusy ? 1 : 0.5 + + NText { + anchors.centerIn: parent + text: { + if (modelData.pairing) { + return "Pairing..." + } + if (modelData.blocked) { + return "Blocked" + } + if (modelData.paired || modelData.trusted) { + return "Disconnect" + } + return "Connect" + } + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightMedium + color: { + if (availableDeviceArea.containsMouse) { + return Color.mOnTertiary + } else { + return Color.mPrimary + } + } + } + } + } + + MouseArea { + id: availableDeviceArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) + enabled: canConnect && !isBusy + onClicked: { + if (!modelData || modelData.pairing) { + return + } + + if (modelData.paired || modelData.trusted) { + BluetoothService.disconnectDevice(modelData) + } else { + BluetoothService.connectDeviceWithTrust(modelData) + } + } + } + } + } +} diff --git a/Modules/BluetoothPanel/BluetoothPanel.qml b/Modules/BluetoothPanel/BluetoothPanel.qml index 3518d9b..77ed79c 100644 --- a/Modules/BluetoothPanel/BluetoothPanel.qml +++ b/Modules/BluetoothPanel/BluetoothPanel.qml @@ -68,259 +68,56 @@ NPanel { } ScrollView { - id: scrollView - Layout.fillWidth: true Layout.fillHeight: true - clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded + clip: true + contentWidth: availableWidth - // Available devices - Column { - id: column - + ColumnLayout { + visible: BluetoothService.adapter && BluetoothService.adapter.enabled width: parent.width spacing: Style.marginM * scaling - visible: BluetoothService.adapter && BluetoothService.adapter.enabled - RowLayout { - width: parent.width - spacing: Style.marginM * scaling - - NText { - text: "Available Devices" - font.pointSize: Style.fontSizeL * scaling - color: Color.mOnSurface - font.weight: Style.fontWeightMedium - } - } - - Repeater { + // Connected devices + BluetoothDevicesList { + label: "Connected devices" model: { - if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + if (!BluetoothService.adapter || !Bluetooth.devices) return [] var filtered = Bluetooth.devices.values.filter(dev => { - return dev && !dev.paired && !dev.pairing && !dev.blocked - && (dev.signalStrength === undefined - || dev.signalStrength > 0) + return dev && !dev.blocked && (dev.paired || dev.trusted) }) return BluetoothService.sortDevices(filtered) } - - Rectangle { - property bool canConnect: BluetoothService.canConnect(modelData) - property bool isBusy: BluetoothService.isDeviceBusy(modelData) - - width: parent.width - height: 70 - radius: Style.radiusM * scaling - color: { - if (availableDeviceArea.containsMouse && !isBusy) - return Color.mTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mPrimary - - if (modelData.blocked) - return Color.mError - - return Color.mSurfaceVariant - } - border.color: Color.mOutline - border.width: Math.max(1, Style.borderS * scaling) - - Row { - anchors.left: parent.left - anchors.leftMargin: Style.marginM * scaling - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS * scaling - - // One device BT icon - NIcon { - text: BluetoothService.getDeviceIcon(modelData) - font.pointSize: Style.fontSizeXXL * scaling - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - anchors.verticalCenter: parent.verticalCenter - } - - Column { - spacing: Style.marginXXS * scaling - anchors.verticalCenter: parent.verticalCenter - - // One device name - NText { - text: modelData.name || modelData.deviceName - font.pointSize: Style.fonttSizeMedium * scaling - elide: Text.ElideRight - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - font.weight: Style.fontWeightMedium - } - - Row { - spacing: Style.marginXS * scaling - - Row { - spacing: Style.marginS * spacing - - // One device signal strength - "Unknown" when not connected - NText { - text: { - if (modelData.pairing) - return "Pairing..." - - if (modelData.blocked) - return "Blocked" - - return BluetoothService.getSignalStrength(modelData) - } - font.pointSize: Style.fontSizeXS * scaling - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - } - - NIcon { - text: BluetoothService.getSignalIcon(modelData) - font.pointSize: Style.fontSizeXS * scaling - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 - && !modelData.pairing && !modelData.blocked - } - - NText { - text: (modelData.signalStrength !== undefined - && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" - font.pointSize: Style.fontSizeXS * scaling - color: { - if (availableDeviceArea.containsMouse) - return Color.mOnTertiary - - if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) - return Color.mOnPrimary - - if (modelData.blocked) - return Color.mOnError - - return Color.mOnSurface - } - visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 - && !modelData.pairing && !modelData.blocked - } - } - } - } - } - - Rectangle { - width: 80 * scaling - height: 28 * scaling - radius: Style.radiusM * scaling - anchors.right: parent.right - anchors.rightMargin: Style.marginM * scaling - anchors.verticalCenter: parent.verticalCenter - visible: modelData.state !== BluetoothDeviceState.Connecting - color: Color.transparent - - border.color: { - if (availableDeviceArea.containsMouse) { - return Color.mOnTertiary - } else { - return Color.mPrimary - } - } - border.width: Math.max(1, Style.borderS * scaling) - opacity: canConnect || isBusy ? 1 : 0.5 - - // On device connect button - NText { - anchors.centerIn: parent - text: { - if (modelData.pairing) - return "Pairing..." - - if (modelData.blocked) - return "Blocked" - - return "Connect" - } - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightMedium - color: { - if (availableDeviceArea.containsMouse) { - return Color.mOnTertiary - } else { - return Color.mPrimary - } - } - } - } - - MouseArea { - id: availableDeviceArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) - enabled: canConnect && !isBusy - onClicked: { - if (modelData) - BluetoothService.connectDeviceWithTrust(modelData) - } - } - } + Layout.fillWidth: true } - // Fallback if nothing available - Column { - width: parent.width + // Available devices + BluetoothDevicesList { + label: "Available devices" + model: { + if (!BluetoothService.adapter || !Bluetooth.devices) + return [] + + var filtered = Bluetooth.devices.values.filter(dev => { + return dev && !dev.blocked && !dev.paired && !dev.trusted + }) + return BluetoothService.sortDevices(filtered) + } + Layout.fillWidth: true + } + + // Fallback + ColumnLayout { + Layout.fillWidth: true spacing: Style.marginM * scaling visible: { - if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) { return false + } var availableCount = Bluetooth.devices.values.filter(dev => { return dev && !dev.paired && !dev.pairing @@ -328,18 +125,17 @@ NPanel { && (dev.signalStrength === undefined || dev.signalStrength > 0) }).length - return availableCount === 0 + return (availableCount === 0) } - Row { - anchors.horizontalCenter: parent.horizontalCenter + RowLayout { + Layout.alignment: Qt.AlignHCenter spacing: Style.marginM * scaling NIcon { text: "sync" font.pointSize: Style.fontSizeXLL * 1.5 * scaling color: Color.mPrimary - anchors.verticalCenter: parent.verticalCenter RotationAnimation on rotation { running: true @@ -355,7 +151,6 @@ NPanel { font.pointSize: Style.fontSizeL * scaling color: Color.mOnSurface font.weight: Style.fontWeightMedium - anchors.verticalCenter: parent.verticalCenter } } @@ -363,36 +158,15 @@ NPanel { text: "Make sure your device is in pairing mode" font.pointSize: Style.fontSizeM * scaling color: Color.mOnSurfaceVariant - anchors.horizontalCenter: parent.horizontalCenter + Layout.alignment: Qt.AlignHCenter } } - NText { - text: "No devices found. Put your device in pairing mode and click Start Scanning." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - visible: { - if (!BluetoothService.adapter || !Bluetooth.devices) - return true - - var availableCount = Bluetooth.devices.values.filter(dev => { - return dev && !dev.paired && !dev.pairing - && !dev.blocked - && (dev.signalStrength === undefined - || dev.signalStrength > 0) - }).length - return availableCount === 0 && !BluetoothService.adapter.discovering - } - wrapMode: Text.WordWrap - width: parent.width - horizontalAlignment: Text.AlignHCenter + Item { + Layout.fillHeight: true } } } - // This item takes up all the remaining vertical space. - Item { - Layout.fillHeight: true - } } } } diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index aac2b53..368534c 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -13,17 +13,17 @@ Singleton { readonly property bool discovering: (adapter && adapter.discovering) ?? false readonly property var devices: adapter ? adapter.devices : null readonly property var pairedDevices: { - if (!adapter || !adapter.devices) - return [] - + if (!adapter || !adapter.devices) { + return [] + } return adapter.devices.values.filter(dev => { return dev && (dev.paired || dev.trusted) }) } readonly property var allDevicesWithBattery: { - if (!adapter || !adapter.devices) - return [] - + if (!adapter || !adapter.devices) { + return [] + } return adapter.devices.values.filter(dev => { return dev && dev.batteryAvailable && dev.battery > 0 }) @@ -49,34 +49,36 @@ Singleton { } function getDeviceIcon(device) { - if (!device) + if (!device) { return "bluetooth" + } var name = (device.name || device.deviceName || "").toLowerCase() var icon = (device.icon || "").toLowerCase() if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") - || name.includes("headset") || name.includes("arctis")) + || name.includes("headset") || name.includes("arctis")) { return "headset" + } - if (icon.includes("mouse") || name.includes("mouse")) + if (icon.includes("mouse") || name.includes("mouse")) { return "mouse" - - if (icon.includes("keyboard") || name.includes("keyboard")) + } + if (icon.includes("keyboard") || name.includes("keyboard")) { return "keyboard" - + } if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") - || name.includes("samsung")) + || name.includes("samsung")) { return "smartphone" - - if (icon.includes("watch") || name.includes("watch")) + } + if (icon.includes("watch") || name.includes("watch")) { return "watch" - - if (icon.includes("speaker") || name.includes("speaker")) + } + if (icon.includes("speaker") || name.includes("speaker")) { return "speaker" - - if (icon.includes("display") || name.includes("tv")) + } + if (icon.includes("display") || name.includes("tv")) { return "tv" - + } return "bluetooth" } @@ -88,60 +90,91 @@ Singleton { } function getSignalStrength(device) { - if (!device || device.signalStrength === undefined || device.signalStrength <= 0) - return "Unknown" - + if (device.pairing) { + return "Pairing..." + } + if (device.blocked) { + return "Blocked" + } + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) { + return "Signal: Unknown" + } var signal = device.signalStrength - if (signal >= 80) - return "Excellent" + if (signal >= 80) { + return "Signal: Excellent" + } + if (signal >= 60) { + return "Signal: Good" + } + if (signal >= 40) { + return "Signal: Fair" + } + if (signal >= 20) { + return "Signal: Poor" + } + return "Signal: Very Poor" + } - if (signal >= 60) - return "Good" - - if (signal >= 40) - return "Fair" - - if (signal >= 20) - return "Poor" - - return "Very Poor" + function getBattery(device) { + return `Battery: ${Math.round(device.battery * 100)}` } function getSignalIcon(device) { - if (!device || device.signalStrength === undefined || device.signalStrength <= 0) + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) { return "signal_cellular_null" - + } var signal = device.signalStrength - if (signal >= 80) + if (signal >= 80) { return "signal_cellular_4_bar" - - if (signal >= 60) + } + if (signal >= 60) { return "signal_cellular_3_bar" - - if (signal >= 40) + } + if (signal >= 40) { return "signal_cellular_2_bar" - - if (signal >= 20) + } + if (signal >= 20) { return "signal_cellular_1_bar" - + } return "signal_cellular_0_bar" } function isDeviceBusy(device) { - if (!device) + if (!device) { return false + } + return device.pairing || device.state === BluetoothDeviceState.Disconnecting || device.state === BluetoothDeviceState.Connecting } function connectDeviceWithTrust(device) { - if (!device) + if (!device) { return + } device.trusted = true device.connect() } + function disconnectDevice(device) { + if (!device) { + return + } + + device.trusted = false + device.disconnect() + } + + function forgetDevice(device) { + if (!device) { + return + } + + device.trusted = false + device.forget() + } + function setBluetoothEnabled(enabled) { if (!adapter) { Logger.warn("Bluetooth", "No adapter available") From 3cc8c8fb03f1dea33dc4d77555e7d30023a62396 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Thu, 28 Aug 2025 15:55:03 -0400 Subject: [PATCH 19/19] ArchUpdater: improved the look --- Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml | 88 ++++++++----------- Modules/Bar/Widgets/ArchUpdater.qml | 57 +++++------- Widgets/NCheckbox.qml | 3 +- 3 files changed, 59 insertions(+), 89 deletions(-) diff --git a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml index 9152ddd..a4759ca 100644 --- a/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml +++ b/Modules/ArchUpdaterPanel/ArchUpdaterPanel.qml @@ -77,11 +77,9 @@ NPanel { } // Unified list - Rectangle { + NBox { Layout.fillWidth: true Layout.fillHeight: true - color: Color.mSurfaceVariant - radius: Style.radiusM * scaling // Combine repo and AUR lists in order: repos first, then AUR property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || []) @@ -89,21 +87,21 @@ NPanel { ListView { id: unifiedList anchors.fill: parent - anchors.margins: Style.marginS * scaling + anchors.margins: Style.marginM * scaling + cacheBuffer: Math.round(300 * scaling) clip: true + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } model: parent.items - spacing: Style.marginXS * scaling - cacheBuffer: 300 * scaling - delegate: Rectangle { width: unifiedList.width - height: 56 * scaling + height: 44 * scaling color: Color.transparent radius: Style.radiusS * scaling RowLayout { anchors.fill: parent - anchors.margins: Style.marginS * scaling spacing: Style.marginS * scaling // Checkbox for selection @@ -112,16 +110,12 @@ NPanel { label: "" description: "" checked: ArchUpdaterService.isPackageSelected(modelData.name) + baseSize: Math.max(Style.baseWidgetSize * 0.7, 14) onToggled: function (checked) { ArchUpdaterService.togglePackageSelection(modelData.name) // Force refresh of the checked property checkbox.checked = ArchUpdaterService.isPackageSelected(modelData.name) } - activeColor: (modelData.source === "aur") ? Color.mSecondary : Color.mPrimary - activeOnColor: (modelData.source === "aur") ? Color.mOnSecondary : Color.mOnPrimary - Layout.fillWidth: false - Layout.preferredWidth: 30 * scaling - Layout.preferredHeight: 30 * scaling } // Package info @@ -129,51 +123,43 @@ NPanel { Layout.fillWidth: true spacing: Style.marginXXS * scaling - RowLayout { + NText { + text: modelData.name + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface Layout.fillWidth: true - spacing: Style.marginXS * scaling - - NText { - text: modelData.name - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightMedium - color: Color.mOnSurface - Layout.fillWidth: true - } - - // Source badge (custom rectangle) - Rectangle { - visible: !!modelData.source - radius: 9999 - color: modelData.source === "aur" ? Color.mSecondary : Color.mPrimary - Layout.alignment: Qt.AlignVCenter - implicitHeight: Math.max(Style.fontSizeXS * 1.7 * scaling, 16 * scaling) - // Width based on label content + horizontal padding - implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling) - - NText { - id: badgeText - anchors.centerIn: parent - text: modelData.source === "aur" ? "AUR" : "Repo" - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightBold - color: modelData.source === "aur" ? Color.mOnSecondary : Color.mOnPrimary - } - } + Layout.alignment: Qt.AlignVCenter } NText { text: modelData.oldVersion + " → " + modelData.newVersion - font.pointSize: Style.fontSizeS * scaling + font.pointSize: Style.fontSizeXXS * scaling color: Color.mOnSurfaceVariant Layout.fillWidth: true } } - } - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded + // Source tag (AUR vs PAC) + Rectangle { + visible: !!modelData.source + radius: width * 0.5 + color: modelData.source === "aur" ? Color.mTertiary : Color.mSecondary + Layout.alignment: Qt.AlignVCenter + implicitHeight: Style.fontSizeS * 1.8 * scaling + // Width based on label content + horizontal padding + implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling) + + NText { + id: badgeText + anchors.centerIn: parent + text: modelData.source === "aur" ? "AUR" : "PAC" + font.pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightBold + color: modelData.source === "aur" ? Color.mOnTertiary : Color.mOnSecondary + } + } + } } } } @@ -219,9 +205,9 @@ NPanel { } } colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount - > 0 ? Color.mSecondary : Color.mSurfaceVariant) + > 0 ? Color.mPrimary : Color.mSurfaceVariant) colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount - > 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant) + > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant) Layout.fillWidth: true } } diff --git a/Modules/Bar/Widgets/ArchUpdater.qml b/Modules/Bar/Widgets/ArchUpdater.qml index 90cedb7..106b167 100644 --- a/Modules/Bar/Widgets/ArchUpdater.qml +++ b/Modules/Bar/Widgets/ArchUpdater.qml @@ -14,18 +14,9 @@ NIconButton { sizeRatio: 0.8 colorBg: Color.mSurfaceVariant - // Highlight color based on update source - colorFg: { - if (ArchUpdaterService.totalUpdates === 0) - return Color.mOnSurface - if (ArchUpdaterService.updates > 0 && ArchUpdaterService.aurUpdates > 0) - return Color.mPrimary - if (ArchUpdaterService.updates > 0) - return Color.mPrimary - return Color.mSecondary - } colorBorder: Color.transparent colorBorderHover: Color.transparent + colorFg: (ArchUpdaterService.totalUpdates === 0) ? Color.mOnSurface : Color.mPrimary // Icon states icon: { @@ -40,42 +31,34 @@ NIconButton { // Tooltip with repo vs AUR breakdown and sample lists tooltipText: { - if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) + if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) { return "Checking for updates…" - - const repoCount = ArchUpdaterService.updates - const aurCount = ArchUpdaterService.aurUpdates - const total = ArchUpdaterService.totalUpdates - - if (total === 0) - return "System is up to date ✓" - - let header = total === 1 ? "One package can be upgraded:" : (total + " packages can be upgraded:") - - function sampleList(arr, n, colorLabel) { - const limit = Math.min(arr.length, n) - let s = "" - for (var i = 0; i < limit; ++i) { - const p = arr[i] - s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion) - } - if (arr.length > limit) - s += "\n… and " + (arr.length - limit) + " more" - return (colorLabel ? (colorLabel + "\n") : "") + (s || "None") } - const repoHeader = repoCount > 0 ? ("Repo (" + repoCount + "):") : "Repo: 0" - const aurHeader = aurCount > 0 ? ("AUR (" + aurCount + "):") : "AUR: 0" + const total = ArchUpdaterService.totalUpdates + if (total === 0) { + return "System is up to date ✓" + } + let header = (total === 1) ? "1 package can be updated" : (total + " packages can be updated") - const repoBlock = repoCount > 0 ? (repoHeader + "\n\n" + sampleList(ArchUpdaterService.repoPackages, - 5)) : repoHeader - const aurBlock = aurCount > 0 ? (aurHeader + "\n\n" + sampleList(ArchUpdaterService.aurPackages, 5)) : aurHeader + const pacCount = ArchUpdaterService.updates + const aurCount = ArchUpdaterService.aurUpdates + const pacmanTooltip = (pacCount > 0) ? ((pacCount === 1) ? "1 system package" : pacCount + " system packages") : "" + const aurTooltip = (aurCount > 0) ? ((aurCount === 1) ? "1 AUR package" : aurCount + " AUR packages") : "" - return header + "\n\n" + repoBlock + "\n\n" + aurBlock + "\n\nClick to update system" + let tooltip = header + if (pacmanTooltip !== "") { + tooltip += "\n" + pacmanTooltip + } + if (aurTooltip !== "") { + tooltip += "\n" + aurTooltip + } + return tooltip } onClicked: { if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) { + ToastService.showNotice("ArchUpdater", "Still fetching updates...") return } diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index 24944e2..962b303 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -24,6 +24,7 @@ RowLayout { NLabel { label: root.label description: root.description + visible: root.label !== "" || root.description !== "" } Rectangle { @@ -31,7 +32,7 @@ RowLayout { implicitWidth: root.baseSize * scaling implicitHeight: root.baseSize * scaling - radius: Math.max(2 * scaling, Style.radiusXS * scaling) + radius: Style.radiusXS * scaling color: root.checked ? root.activeColor : Color.mSurface border.color: root.checked ? root.activeColor : Color.mOutline border.width: Math.max(1, Style.borderM * scaling)